@teardown/cli 1.2.38 → 2.0.41

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.
Files changed (182) hide show
  1. package/bin/teardown.js +11 -1
  2. package/package.json +77 -57
  3. package/src/cli/commands/init.ts +254 -0
  4. package/src/cli/commands/plugins.ts +93 -0
  5. package/src/cli/commands/prebuild.ts +168 -0
  6. package/src/cli/commands/run.ts +727 -0
  7. package/src/cli/commands/start.ts +87 -0
  8. package/src/cli/commands/validate.ts +62 -0
  9. package/src/cli/index.ts +59 -0
  10. package/src/config/index.ts +45 -0
  11. package/src/config/loader.ts +366 -0
  12. package/src/config/schema.ts +235 -0
  13. package/src/config/types.ts +322 -0
  14. package/src/index.ts +177 -0
  15. package/src/pipeline/cache.ts +179 -0
  16. package/src/pipeline/index.ts +10 -0
  17. package/src/pipeline/stages.ts +692 -0
  18. package/src/plugins/base.ts +370 -0
  19. package/src/plugins/capabilities/biometrics.ts +64 -0
  20. package/src/plugins/capabilities/bluetooth.ts +86 -0
  21. package/src/plugins/capabilities/calendar.ts +57 -0
  22. package/src/plugins/capabilities/camera.ts +77 -0
  23. package/src/plugins/capabilities/contacts.ts +57 -0
  24. package/src/plugins/capabilities/deep-linking.ts +124 -0
  25. package/src/plugins/capabilities/firebase.ts +138 -0
  26. package/src/plugins/capabilities/index.ts +96 -0
  27. package/src/plugins/capabilities/location.ts +87 -0
  28. package/src/plugins/capabilities/photo-library.ts +80 -0
  29. package/src/plugins/capabilities/push-notifications.ts +98 -0
  30. package/src/plugins/capabilities/sign-in-with-apple.ts +53 -0
  31. package/src/plugins/context.ts +220 -0
  32. package/src/plugins/index.ts +26 -0
  33. package/src/plugins/resolver.ts +321 -0
  34. package/src/templates/generator.ts +507 -0
  35. package/src/templates/index.ts +9 -0
  36. package/src/templates/paths.ts +25 -0
  37. package/src/transformers/android/gradle.ts +400 -0
  38. package/src/transformers/android/index.ts +19 -0
  39. package/src/transformers/android/manifest.ts +506 -0
  40. package/src/transformers/index.ts +39 -0
  41. package/src/transformers/ios/entitlements.ts +283 -0
  42. package/src/transformers/ios/index.ts +10 -0
  43. package/src/transformers/ios/pbxproj.ts +267 -0
  44. package/src/transformers/ios/plist.ts +198 -0
  45. package/src/utils/fs.ts +429 -0
  46. package/src/utils/index.ts +21 -0
  47. package/src/utils/logger.ts +203 -0
  48. package/templates/.gitignore +63 -0
  49. package/templates/Gemfile +3 -0
  50. package/templates/android/app/build.gradle.kts +97 -0
  51. package/templates/android/app/proguard-rules.pro +10 -0
  52. package/templates/android/app/src/main/AndroidManifest.xml +26 -0
  53. package/templates/android/app/src/main/java/com/appname/MainActivity.kt +22 -0
  54. package/templates/android/app/src/main/java/com/appname/MainApplication.kt +44 -0
  55. package/templates/android/app/src/main/res/values/strings.xml +3 -0
  56. package/templates/android/app/src/main/res/values/styles.xml +7 -0
  57. package/templates/android/build.gradle.kts +44 -0
  58. package/templates/android/gradle.properties +39 -0
  59. package/templates/android/settings.gradle.kts +12 -0
  60. package/templates/babel.config.js +15 -0
  61. package/templates/index.js +7 -0
  62. package/templates/ios/.xcode.env +11 -0
  63. package/templates/ios/AppName/AppDelegate.swift +25 -0
  64. package/templates/ios/AppName/AppName-Bridging-Header.h +4 -0
  65. package/templates/ios/AppName/AppName.entitlements +6 -0
  66. package/templates/ios/AppName/Images.xcassets/AppIcon.appiconset/Contents.json +35 -0
  67. package/templates/ios/AppName/Images.xcassets/Contents.json +6 -0
  68. package/templates/ios/AppName/Info.plist +49 -0
  69. package/templates/ios/AppName/LaunchScreen.storyboard +38 -0
  70. package/templates/ios/AppName.xcodeproj/project.pbxproj +402 -0
  71. package/templates/ios/AppName.xcodeproj/xcshareddata/xcschemes/AppName.xcscheme +78 -0
  72. package/templates/ios/Podfile +35 -0
  73. package/templates/metro.config.js +41 -0
  74. package/templates/package.json +57 -0
  75. package/templates/react-native.config.js +8 -0
  76. package/templates/src/app/index.tsx +34 -0
  77. package/templates/src/assets/fonts/.gitkeep +1 -0
  78. package/templates/src/assets/images/.gitkeep +1 -0
  79. package/templates/src/components/ui/accordion.tsx +114 -0
  80. package/templates/src/components/ui/avatar.tsx +75 -0
  81. package/templates/src/components/ui/button.tsx +93 -0
  82. package/templates/src/components/ui/card.tsx +120 -0
  83. package/templates/src/components/ui/checkbox.tsx +133 -0
  84. package/templates/src/components/ui/chip.tsx +95 -0
  85. package/templates/src/components/ui/dialog.tsx +134 -0
  86. package/templates/src/components/ui/divider.tsx +67 -0
  87. package/templates/src/components/ui/error-view.tsx +82 -0
  88. package/templates/src/components/ui/form-field.tsx +101 -0
  89. package/templates/src/components/ui/index.ts +100 -0
  90. package/templates/src/components/ui/popover.tsx +92 -0
  91. package/templates/src/components/ui/pressable-feedback.tsx +88 -0
  92. package/templates/src/components/ui/radio-group.tsx +153 -0
  93. package/templates/src/components/ui/scroll-shadow.tsx +108 -0
  94. package/templates/src/components/ui/select.tsx +165 -0
  95. package/templates/src/components/ui/skeleton-group.tsx +97 -0
  96. package/templates/src/components/ui/skeleton.tsx +87 -0
  97. package/templates/src/components/ui/spinner.tsx +87 -0
  98. package/templates/src/components/ui/surface.tsx +95 -0
  99. package/templates/src/components/ui/switch.tsx +124 -0
  100. package/templates/src/components/ui/tabs.tsx +154 -0
  101. package/templates/src/components/ui/text-field.tsx +106 -0
  102. package/templates/src/components/ui/toast.tsx +129 -0
  103. package/templates/src/contexts/.gitkeep +2 -0
  104. package/templates/src/core/clients/api/api.client.ts +113 -0
  105. package/templates/src/core/clients/api/index.ts +1 -0
  106. package/templates/src/core/clients/storage/index.ts +1 -0
  107. package/templates/src/core/clients/storage/storage.client.ts +121 -0
  108. package/templates/src/core/constants/index.ts +19 -0
  109. package/templates/src/core/core.ts +40 -0
  110. package/templates/src/core/index.ts +10 -0
  111. package/templates/src/global.css +87 -0
  112. package/templates/src/hooks/index.ts +6 -0
  113. package/templates/src/hooks/use-debounce.ts +23 -0
  114. package/templates/src/hooks/use-mounted.ts +21 -0
  115. package/templates/src/index.ts +28 -0
  116. package/templates/src/lib/index.ts +5 -0
  117. package/templates/src/lib/utils.ts +115 -0
  118. package/templates/src/modules/.gitkeep +6 -0
  119. package/templates/src/navigation/index.ts +8 -0
  120. package/templates/src/navigation/navigation-provider.tsx +36 -0
  121. package/templates/src/navigation/router.tsx +137 -0
  122. package/templates/src/providers/app.provider.tsx +29 -0
  123. package/templates/src/providers/index.ts +5 -0
  124. package/templates/src/routes/(tabs)/_layout.tsx +42 -0
  125. package/templates/src/routes/(tabs)/explore.tsx +161 -0
  126. package/templates/src/routes/(tabs)/home.tsx +138 -0
  127. package/templates/src/routes/(tabs)/profile.tsx +151 -0
  128. package/templates/src/routes/_layout.tsx +18 -0
  129. package/templates/src/routes/settings.tsx +194 -0
  130. package/templates/src/screens/auth/index.ts +6 -0
  131. package/templates/src/screens/auth/login.tsx +165 -0
  132. package/templates/src/screens/auth/register.tsx +203 -0
  133. package/templates/src/screens/home.tsx +204 -0
  134. package/templates/src/screens/index.ts +17 -0
  135. package/templates/src/screens/profile.tsx +210 -0
  136. package/templates/src/screens/settings.tsx +216 -0
  137. package/templates/src/screens/welcome.tsx +101 -0
  138. package/templates/src/styles/index.ts +103 -0
  139. package/templates/src/types/common.ts +71 -0
  140. package/templates/src/types/index.ts +5 -0
  141. package/templates/tsconfig.json +14 -0
  142. package/README.md +0 -15
  143. package/assets/favicon.ico +0 -0
  144. package/dist/commands/dev/dev.js +0 -55
  145. package/dist/commands/init/init-teardown.js +0 -26
  146. package/dist/index.js +0 -20
  147. package/dist/modules/dev/dev-menu/keyboard-handler.js +0 -138
  148. package/dist/modules/dev/dev-menu/open-debugger-keyboard-handler.js +0 -105
  149. package/dist/modules/dev/dev-server/cdp/cdp.adapter.js +0 -12
  150. package/dist/modules/dev/dev-server/cdp/index.js +0 -18
  151. package/dist/modules/dev/dev-server/cdp/types.js +0 -2
  152. package/dist/modules/dev/dev-server/dev-server-checker.js +0 -72
  153. package/dist/modules/dev/dev-server/dev-server.js +0 -269
  154. package/dist/modules/dev/dev-server/inspector/device.event-reporter.js +0 -165
  155. package/dist/modules/dev/dev-server/inspector/device.js +0 -577
  156. package/dist/modules/dev/dev-server/inspector/inspector.js +0 -204
  157. package/dist/modules/dev/dev-server/inspector/types.js +0 -2
  158. package/dist/modules/dev/dev-server/inspector/wss/servers/debugger-connection.server.js +0 -61
  159. package/dist/modules/dev/dev-server/inspector/wss/servers/device-connection.server.js +0 -64
  160. package/dist/modules/dev/dev-server/plugins/devtools.plugin.js +0 -50
  161. package/dist/modules/dev/dev-server/plugins/favicon.plugin.js +0 -19
  162. package/dist/modules/dev/dev-server/plugins/multipart.plugin.js +0 -62
  163. package/dist/modules/dev/dev-server/plugins/systrace.plugin.js +0 -28
  164. package/dist/modules/dev/dev-server/plugins/types.js +0 -2
  165. package/dist/modules/dev/dev-server/plugins/wss/index.js +0 -19
  166. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-api.server.js +0 -66
  167. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-debugger.server.js +0 -128
  168. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-dev-client.server.js +0 -75
  169. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-events.server.js +0 -198
  170. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-hmr.server.js +0 -120
  171. package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-message.server.js +0 -357
  172. package/dist/modules/dev/dev-server/plugins/wss/types.js +0 -2
  173. package/dist/modules/dev/dev-server/plugins/wss/web-socket-router.js +0 -57
  174. package/dist/modules/dev/dev-server/plugins/wss/web-socket-server-adapter.js +0 -26
  175. package/dist/modules/dev/dev-server/plugins/wss/web-socket-server.js +0 -46
  176. package/dist/modules/dev/dev-server/plugins/wss/wss.plugin.js +0 -55
  177. package/dist/modules/dev/dev-server/sybmolicate/sybmolicate.plugin.js +0 -36
  178. package/dist/modules/dev/dev-server/sybmolicate/types.js +0 -2
  179. package/dist/modules/dev/terminal/base.terminal.reporter.js +0 -78
  180. package/dist/modules/dev/terminal/terminal.reporter.js +0 -76
  181. package/dist/modules/dev/types.js +0 -2
  182. package/dist/modules/dev/utils/log.js +0 -73
@@ -0,0 +1,727 @@
1
+ /**
2
+ * Run command - runs the app on iOS or Android devices/simulators
3
+ */
4
+
5
+ import { exec, spawn } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { createInterface } from "node:readline";
9
+ import { promisify } from "node:util";
10
+ import chalk from "chalk";
11
+ import { Command } from "commander";
12
+ import ora from "ora";
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ /**
17
+ * Raw device info from xcrun simctl list devices --json
18
+ */
19
+ interface SimctlDevice {
20
+ name: string;
21
+ udid: string;
22
+ state: "Booted" | "Shutdown";
23
+ isAvailable: boolean;
24
+ deviceTypeIdentifier: string;
25
+ }
26
+
27
+ /**
28
+ * Simctl devices JSON structure
29
+ */
30
+ interface SimctlDevicesOutput {
31
+ devices: Record<string, SimctlDevice[]>;
32
+ }
33
+
34
+ /**
35
+ * iOS Simulator device info
36
+ */
37
+ interface iOSDevice {
38
+ name: string;
39
+ udid: string;
40
+ state: "Booted" | "Shutdown";
41
+ isAvailable: boolean;
42
+ deviceTypeIdentifier: string;
43
+ runtime: string;
44
+ }
45
+
46
+ /**
47
+ * Android device/emulator info
48
+ */
49
+ interface AndroidDevice {
50
+ id: string;
51
+ name: string;
52
+ type: "emulator" | "device";
53
+ state: "online" | "offline" | "available";
54
+ }
55
+
56
+ /**
57
+ * Check if native project exists for platform
58
+ */
59
+ function hasNativeProject(platform: "ios" | "android"): boolean {
60
+ const projectRoot = process.cwd();
61
+ if (platform === "ios") {
62
+ return existsSync(join(projectRoot, "ios"));
63
+ }
64
+ return existsSync(join(projectRoot, "android"));
65
+ }
66
+
67
+ /**
68
+ * Check if iOS Pods are installed
69
+ */
70
+ function hasPodsInstalled(): boolean {
71
+ const projectRoot = process.cwd();
72
+ const podsDir = join(projectRoot, "ios", "Pods");
73
+ const workspacePath = join(projectRoot, "ios");
74
+
75
+ // Check if Pods directory exists
76
+ if (!existsSync(podsDir)) {
77
+ return false;
78
+ }
79
+
80
+ // Check if there's a .xcworkspace file (created by pod install)
81
+ try {
82
+ const iosContents = require("node:fs").readdirSync(workspacePath);
83
+ return iosContents.some((file: string) => file.endsWith(".xcworkspace"));
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Run pod install manually
91
+ */
92
+ async function runPodInstall(): Promise<boolean> {
93
+ const spinner = ora("Installing CocoaPods dependencies...").start();
94
+ const iosDir = join(process.cwd(), "ios");
95
+
96
+ return new Promise((resolve) => {
97
+ const proc = spawn("pod", ["install", "--repo-update"], {
98
+ cwd: iosDir,
99
+ stdio: ["ignore", "pipe", "pipe"],
100
+ env: {
101
+ ...process.env,
102
+ LANG: "en_US.UTF-8",
103
+ LC_ALL: "en_US.UTF-8",
104
+ },
105
+ });
106
+
107
+ let stderr = "";
108
+
109
+ proc.stdout?.on("data", () => {
110
+ // Ignore stdout, just wait for completion
111
+ });
112
+
113
+ proc.stderr?.on("data", (data) => {
114
+ stderr += data.toString();
115
+ });
116
+
117
+ proc.on("close", (code) => {
118
+ if (code === 0) {
119
+ spinner.succeed("CocoaPods dependencies installed");
120
+ resolve(true);
121
+ } else {
122
+ spinner.fail("Failed to install CocoaPods dependencies");
123
+ console.error(chalk.red(stderr));
124
+ console.log(chalk.yellow("\nTry running manually:"));
125
+ console.log(chalk.gray(" cd ios && pod install --repo-update"));
126
+ resolve(false);
127
+ }
128
+ });
129
+
130
+ proc.on("error", (err) => {
131
+ spinner.fail(`Failed to run pod install: ${err.message}`);
132
+ resolve(false);
133
+ });
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Run prebuild if native project doesn't exist
139
+ */
140
+ async function ensureNativeProject(platform: "ios" | "android"): Promise<boolean> {
141
+ if (hasNativeProject(platform)) {
142
+ // For iOS, also check if Pods are installed
143
+ if (platform === "ios" && !hasPodsInstalled()) {
144
+ console.log(chalk.yellow("\niOS Pods not installed. Running pod install...\n"));
145
+ const podResult = await runPodInstall();
146
+ if (!podResult) {
147
+ return false;
148
+ }
149
+ }
150
+ return true;
151
+ }
152
+
153
+ console.log(chalk.yellow(`\nNo ${platform} project found. Running prebuild first...\n`));
154
+
155
+ const prebuildSuccess = await new Promise<boolean>((resolve) => {
156
+ const proc = spawn("npx", ["teardown", "prebuild", "--platform", platform], {
157
+ stdio: "inherit",
158
+ shell: true,
159
+ cwd: process.cwd(),
160
+ env: {
161
+ ...process.env,
162
+ LANG: "en_US.UTF-8",
163
+ LC_ALL: "en_US.UTF-8",
164
+ },
165
+ });
166
+
167
+ proc.on("close", (code) => {
168
+ if (code === 0) {
169
+ console.log();
170
+ resolve(true);
171
+ } else {
172
+ console.error(chalk.red("\nPrebuild failed. Please fix the issues and try again."));
173
+ resolve(false);
174
+ }
175
+ });
176
+
177
+ proc.on("error", () => {
178
+ resolve(false);
179
+ });
180
+ });
181
+
182
+ if (!prebuildSuccess) {
183
+ return false;
184
+ }
185
+
186
+ // After prebuild, verify Pods are installed for iOS
187
+ if (platform === "ios" && !hasPodsInstalled()) {
188
+ console.log(chalk.yellow("\nPods not installed after prebuild. Trying pod install...\n"));
189
+ return runPodInstall();
190
+ }
191
+
192
+ return true;
193
+ }
194
+
195
+ /**
196
+ * Start Metro bundler in foreground (runs after build completes)
197
+ */
198
+ function startBundlerInForeground(port: string): void {
199
+ console.log(chalk.blue(`\nStarting Metro bundler on port ${port} with --reset-cache...\n`));
200
+
201
+ const proc = spawn("npx", ["react-native", "start", "--port", port, "--reset-cache"], {
202
+ stdio: "inherit",
203
+ shell: true,
204
+ cwd: process.cwd(),
205
+ env: {
206
+ ...process.env,
207
+ LANG: "en_US.UTF-8",
208
+ },
209
+ });
210
+
211
+ proc.on("close", (code) => {
212
+ if (code !== 0) {
213
+ console.error(chalk.red(`Metro bundler exited with code ${code}`));
214
+ }
215
+ });
216
+
217
+ proc.on("error", (err) => {
218
+ console.error(chalk.red(`Failed to start Metro bundler: ${err.message}`));
219
+ });
220
+ }
221
+
222
+ interface RunOptions {
223
+ device?: string;
224
+ picker: boolean;
225
+ release?: boolean;
226
+ port: string;
227
+ bundler: boolean;
228
+ /** iOS: Build configuration (Debug/Release) */
229
+ configuration?: string;
230
+ /** iOS: Xcode scheme to build */
231
+ scheme?: string;
232
+ /** Android: Build variant (e.g., debug, release) */
233
+ variant?: string;
234
+ /** Clean build before running */
235
+ clean?: boolean;
236
+ }
237
+
238
+ /**
239
+ * Create the run command
240
+ */
241
+ export function createRunCommand(): Command {
242
+ const run = new Command("run")
243
+ .description("Run the app on a device or simulator")
244
+ .argument("<platform>", "Platform to run on (ios or android)")
245
+ .option("-d, --device <device>", "Specific device name or ID")
246
+ .option("--no-picker", "Skip device picker, use first available device")
247
+ .option("--release", "Run in release mode")
248
+ .option("--port <port>", "Metro bundler port", "8081")
249
+ .option("--no-bundler", "Skip starting the Metro bundler")
250
+ .option("--configuration <config>", "iOS build configuration (Debug/Release)")
251
+ .option("--scheme <scheme>", "iOS Xcode scheme to build")
252
+ .option("--variant <variant>", "Android build variant (e.g., debug, release)")
253
+ .option("--clean", "Clean build before running", false)
254
+ .action(async (platform: string, options: RunOptions) => {
255
+ if (platform !== "ios" && platform !== "android") {
256
+ console.error(chalk.red(`Invalid platform: ${platform}. Use 'ios' or 'android'.`));
257
+ process.exit(1);
258
+ }
259
+
260
+ try {
261
+ // Ensure native project exists (auto-prebuild if needed)
262
+ const hasProject = await ensureNativeProject(platform);
263
+ if (!hasProject) {
264
+ process.exit(1);
265
+ }
266
+
267
+ if (platform === "ios") {
268
+ await runIOS(options);
269
+ } else {
270
+ await runAndroid(options);
271
+ }
272
+
273
+ // Start bundler after build completes (unless --no-bundler or --release)
274
+ if (options.bundler && !options.release) {
275
+ startBundlerInForeground(options.port);
276
+ }
277
+ } catch (error) {
278
+ console.error(chalk.red(`Failed to run app: ${error instanceof Error ? error.message : error}`));
279
+ process.exit(1);
280
+ }
281
+ });
282
+
283
+ return run;
284
+ }
285
+
286
+ /**
287
+ * Run on iOS
288
+ */
289
+ async function runIOS(options: RunOptions): Promise<void> {
290
+ const spinner = ora("Detecting iOS devices...").start();
291
+
292
+ try {
293
+ const devices = await getIOSDevices();
294
+ spinner.stop();
295
+
296
+ if (devices.length === 0) {
297
+ console.log(chalk.yellow("\nNo iOS simulators found."));
298
+ console.log(chalk.gray("Open Xcode and create a simulator, or install Xcode Command Line Tools."));
299
+ process.exit(1);
300
+ }
301
+
302
+ let selectedDevice: iOSDevice;
303
+
304
+ if (options.device) {
305
+ // Find device by name or udid
306
+ const found = devices.find(
307
+ (d) => d.name.toLowerCase() === options.device?.toLowerCase() || d.udid === options.device
308
+ );
309
+ if (!found) {
310
+ console.error(chalk.red(`Device not found: ${options.device}`));
311
+ console.log(chalk.gray("\nAvailable devices:"));
312
+ for (const d of devices) {
313
+ console.log(chalk.gray(` - ${d.name} (${d.state})`));
314
+ }
315
+ process.exit(1);
316
+ }
317
+ selectedDevice = found;
318
+ } else if (!options.picker) {
319
+ // Use first booted device, or first available
320
+ selectedDevice = devices.find((d) => d.state === "Booted") || devices[0];
321
+ } else {
322
+ // Show device picker
323
+ selectedDevice = await pickDevice(devices, "iOS");
324
+ }
325
+
326
+ console.log(chalk.blue(`\nSelected device: ${selectedDevice.name}`));
327
+
328
+ // Boot device if needed
329
+ if (selectedDevice.state !== "Booted") {
330
+ const bootSpinner = ora(`Booting ${selectedDevice.name}...`).start();
331
+ await bootIOSSimulator(selectedDevice.udid);
332
+ bootSpinner.succeed(`${selectedDevice.name} is now running`);
333
+ }
334
+
335
+ // Run the app
336
+ console.log(chalk.blue("\nStarting app on iOS simulator...\n"));
337
+
338
+ const args = [
339
+ "react-native",
340
+ "run-ios",
341
+ "--simulator",
342
+ selectedDevice.name,
343
+ "--port",
344
+ options.port,
345
+ "--no-packager",
346
+ ];
347
+
348
+ // Handle configuration/release mode
349
+ const configuration = options.configuration || (options.release ? "Release" : "Debug");
350
+ args.push("--mode", configuration);
351
+
352
+ // Handle scheme if specified
353
+ if (options.scheme) {
354
+ args.push("--scheme", options.scheme);
355
+ }
356
+
357
+ // Handle clean build
358
+ if (options.clean) {
359
+ console.log(chalk.blue("Cleaning iOS build..."));
360
+ try {
361
+ await execAsync("xcodebuild clean", { cwd: join(process.cwd(), "ios") });
362
+ } catch {
363
+ // Ignore clean errors
364
+ }
365
+ }
366
+
367
+ await runCommand("npx", args);
368
+ } catch (error) {
369
+ spinner.fail("Failed to detect iOS devices");
370
+ throw error;
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Run on Android
376
+ */
377
+ async function runAndroid(options: RunOptions): Promise<void> {
378
+ const spinner = ora("Detecting Android devices...").start();
379
+
380
+ try {
381
+ const devices = await getAndroidDevices();
382
+ spinner.stop();
383
+
384
+ if (devices.length === 0) {
385
+ console.log(chalk.yellow("\nNo Android devices or emulators found."));
386
+
387
+ // Check for available AVDs
388
+ const avds = await getAndroidAVDs();
389
+ if (avds.length > 0) {
390
+ console.log(chalk.gray("\nAvailable emulators (not running):"));
391
+ for (const avd of avds) {
392
+ console.log(chalk.gray(` - ${avd}`));
393
+ }
394
+
395
+ const selectedAVD = await pickAVD(avds);
396
+ if (selectedAVD) {
397
+ const bootSpinner = ora(`Starting ${selectedAVD}...`).start();
398
+ await startAndroidEmulator(selectedAVD);
399
+ bootSpinner.succeed(`${selectedAVD} is starting`);
400
+
401
+ // Wait for device to be ready
402
+ await waitForAndroidDevice();
403
+
404
+ // Re-fetch devices
405
+ const newDevices = await getAndroidDevices();
406
+ if (newDevices.length > 0) {
407
+ await launchAndroidApp(newDevices[0], {
408
+ release: options.release,
409
+ port: options.port,
410
+ variant: options.variant,
411
+ clean: options.clean,
412
+ });
413
+ return;
414
+ }
415
+ }
416
+ }
417
+
418
+ console.log(chalk.gray("\nTo create an emulator, run: Android Studio > Tools > Device Manager"));
419
+ process.exit(1);
420
+ }
421
+
422
+ let selectedDevice: AndroidDevice;
423
+
424
+ if (options.device) {
425
+ const found = devices.find(
426
+ (d) => d.name.toLowerCase() === options.device?.toLowerCase() || d.id === options.device
427
+ );
428
+ if (!found) {
429
+ console.error(chalk.red(`Device not found: ${options.device}`));
430
+ console.log(chalk.gray("\nAvailable devices:"));
431
+ for (const d of devices) {
432
+ console.log(chalk.gray(` - ${d.name} (${d.type})`));
433
+ }
434
+ process.exit(1);
435
+ }
436
+ selectedDevice = found;
437
+ } else if (!options.picker || devices.length === 1) {
438
+ selectedDevice = devices[0];
439
+ } else {
440
+ selectedDevice = await pickAndroidDevice(devices);
441
+ }
442
+
443
+ await launchAndroidApp(selectedDevice, {
444
+ release: options.release,
445
+ port: options.port,
446
+ variant: options.variant,
447
+ clean: options.clean,
448
+ });
449
+ } catch (error) {
450
+ spinner.fail("Failed to detect Android devices");
451
+ throw error;
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Launch Android app on device
457
+ */
458
+ async function launchAndroidApp(
459
+ device: AndroidDevice,
460
+ options: { release?: boolean; port: string; variant?: string; clean?: boolean }
461
+ ): Promise<void> {
462
+ console.log(chalk.blue(`\nSelected device: ${device.name} (${device.type})`));
463
+ console.log(chalk.blue("\nStarting app on Android...\n"));
464
+
465
+ // Handle clean build
466
+ if (options.clean) {
467
+ console.log(chalk.blue("Cleaning Android build..."));
468
+ try {
469
+ await execAsync("./gradlew clean", { cwd: join(process.cwd(), "android") });
470
+ } catch {
471
+ // Ignore clean errors
472
+ }
473
+ }
474
+
475
+ const args = ["react-native", "run-android", "--port", options.port, "--no-packager"];
476
+
477
+ if (device.type === "device") {
478
+ args.push("--deviceId", device.id);
479
+ }
480
+
481
+ // Handle variant or release mode
482
+ if (options.variant) {
483
+ args.push("--variant", options.variant);
484
+ } else if (options.release) {
485
+ args.push("--mode", "release");
486
+ }
487
+
488
+ await runCommand("npx", args);
489
+ }
490
+
491
+ /**
492
+ * Get list of iOS simulators
493
+ */
494
+ async function getIOSDevices(): Promise<iOSDevice[]> {
495
+ try {
496
+ const { stdout } = await execAsync("xcrun simctl list devices --json");
497
+ const data = JSON.parse(stdout) as SimctlDevicesOutput;
498
+
499
+ const devices: iOSDevice[] = [];
500
+
501
+ for (const [runtime, runtimeDevices] of Object.entries(data.devices)) {
502
+ if (!runtime.includes("iOS")) continue;
503
+
504
+ for (const device of runtimeDevices) {
505
+ if (device.isAvailable) {
506
+ devices.push({
507
+ name: device.name,
508
+ udid: device.udid,
509
+ state: device.state,
510
+ isAvailable: device.isAvailable,
511
+ deviceTypeIdentifier: device.deviceTypeIdentifier,
512
+ runtime: runtime.replace("com.apple.CoreSimulator.SimRuntime.", "").replace("-", " "),
513
+ });
514
+ }
515
+ }
516
+ }
517
+
518
+ // Sort: booted first, then by name
519
+ devices.sort((a, b) => {
520
+ if (a.state === "Booted" && b.state !== "Booted") return -1;
521
+ if (a.state !== "Booted" && b.state === "Booted") return 1;
522
+ return a.name.localeCompare(b.name);
523
+ });
524
+
525
+ return devices;
526
+ } catch {
527
+ return [];
528
+ }
529
+ }
530
+
531
+ /**
532
+ * Boot an iOS simulator
533
+ */
534
+ async function bootIOSSimulator(udid: string): Promise<void> {
535
+ await execAsync(`xcrun simctl boot ${udid}`);
536
+ // Open Simulator app
537
+ await execAsync("open -a Simulator");
538
+ }
539
+
540
+ /**
541
+ * Get list of Android devices
542
+ */
543
+ async function getAndroidDevices(): Promise<AndroidDevice[]> {
544
+ try {
545
+ const { stdout } = await execAsync("adb devices -l");
546
+ const lines = stdout.trim().split("\n").slice(1);
547
+
548
+ const devices: AndroidDevice[] = [];
549
+
550
+ for (const line of lines) {
551
+ if (!line.trim()) continue;
552
+
553
+ const parts = line.split(/\s+/);
554
+ const id = parts[0];
555
+ const status = parts[1];
556
+
557
+ if (status !== "device") continue;
558
+
559
+ // Get device name
560
+ const modelMatch = line.match(/model:(\S+)/);
561
+ const productMatch = line.match(/product:(\S+)/);
562
+ const name = modelMatch?.[1] || productMatch?.[1] || id;
563
+
564
+ devices.push({
565
+ id,
566
+ name: name.replace(/_/g, " "),
567
+ type: id.startsWith("emulator") ? "emulator" : "device",
568
+ state: "online",
569
+ });
570
+ }
571
+
572
+ return devices;
573
+ } catch {
574
+ return [];
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Get list of available Android AVDs
580
+ */
581
+ async function getAndroidAVDs(): Promise<string[]> {
582
+ try {
583
+ const { stdout } = await execAsync("emulator -list-avds");
584
+ return stdout.trim().split("\n").filter(Boolean);
585
+ } catch {
586
+ return [];
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Start an Android emulator
592
+ */
593
+ async function startAndroidEmulator(avdName: string): Promise<void> {
594
+ // Start emulator in background
595
+ spawn("emulator", ["-avd", avdName], {
596
+ detached: true,
597
+ stdio: "ignore",
598
+ }).unref();
599
+ }
600
+
601
+ /**
602
+ * Wait for Android device to be ready
603
+ */
604
+ async function waitForAndroidDevice(timeout = 60000): Promise<void> {
605
+ const startTime = Date.now();
606
+
607
+ while (Date.now() - startTime < timeout) {
608
+ try {
609
+ const { stdout } = await execAsync("adb devices");
610
+ if (stdout.includes("device") && !stdout.includes("offline")) {
611
+ // Wait a bit more for device to be fully ready
612
+ await new Promise((resolve) => setTimeout(resolve, 2000));
613
+ return;
614
+ }
615
+ } catch {
616
+ // Ignore errors
617
+ }
618
+ await new Promise((resolve) => setTimeout(resolve, 1000));
619
+ }
620
+
621
+ throw new Error("Timeout waiting for Android device");
622
+ }
623
+
624
+ /**
625
+ * Interactive device picker for iOS
626
+ */
627
+ async function pickDevice(devices: iOSDevice[], platform: string): Promise<iOSDevice> {
628
+ console.log(chalk.blue(`\n${platform} Devices:`));
629
+ console.log(chalk.gray("─".repeat(50)));
630
+
631
+ devices.forEach((device, index) => {
632
+ const status = device.state === "Booted" ? chalk.green("● Running") : chalk.gray("○ Stopped");
633
+ console.log(` ${chalk.cyan(index + 1)}. ${device.name} ${status}`);
634
+ console.log(chalk.gray(` ${device.runtime}`));
635
+ });
636
+
637
+ console.log(chalk.gray("─".repeat(50)));
638
+
639
+ const index = await promptNumber(`Select device (1-${devices.length})`, 1, devices.length);
640
+ return devices[index - 1];
641
+ }
642
+
643
+ /**
644
+ * Interactive device picker for Android
645
+ */
646
+ async function pickAndroidDevice(devices: AndroidDevice[]): Promise<AndroidDevice> {
647
+ console.log(chalk.blue("\nAndroid Devices:"));
648
+ console.log(chalk.gray("─".repeat(50)));
649
+
650
+ devices.forEach((device, index) => {
651
+ const typeIcon = device.type === "emulator" ? "📱" : "📲";
652
+ console.log(` ${chalk.cyan(index + 1)}. ${typeIcon} ${device.name}`);
653
+ console.log(chalk.gray(` ${device.type} - ${device.id}`));
654
+ });
655
+
656
+ console.log(chalk.gray("─".repeat(50)));
657
+
658
+ const index = await promptNumber(`Select device (1-${devices.length})`, 1, devices.length);
659
+ return devices[index - 1];
660
+ }
661
+
662
+ /**
663
+ * Interactive AVD picker
664
+ */
665
+ async function pickAVD(avds: string[]): Promise<string | null> {
666
+ console.log(chalk.blue("\nAvailable Emulators:"));
667
+ console.log(chalk.gray("─".repeat(50)));
668
+
669
+ avds.forEach((avd, index) => {
670
+ console.log(` ${chalk.cyan(index + 1)}. ${avd}`);
671
+ });
672
+ console.log(` ${chalk.cyan(0)}. Cancel`);
673
+
674
+ console.log(chalk.gray("─".repeat(50)));
675
+
676
+ const index = await promptNumber(`Select emulator to start (0-${avds.length})`, 0, avds.length);
677
+ return index === 0 ? null : avds[index - 1];
678
+ }
679
+
680
+ /**
681
+ * Prompt for a number
682
+ */
683
+ function promptNumber(message: string, min: number, max: number): Promise<number> {
684
+ return new Promise((resolve) => {
685
+ const rl = createInterface({
686
+ input: process.stdin,
687
+ output: process.stdout,
688
+ });
689
+
690
+ const ask = () => {
691
+ rl.question(chalk.yellow(`\n${message}: `), (answer) => {
692
+ const num = Number.parseInt(answer, 10);
693
+ if (Number.isNaN(num) || num < min || num > max) {
694
+ console.log(chalk.red(`Please enter a number between ${min} and ${max}`));
695
+ ask();
696
+ } else {
697
+ rl.close();
698
+ resolve(num);
699
+ }
700
+ });
701
+ };
702
+
703
+ ask();
704
+ });
705
+ }
706
+
707
+ /**
708
+ * Run a command with output streaming
709
+ */
710
+ function runCommand(command: string, args: string[]): Promise<void> {
711
+ return new Promise((resolve, reject) => {
712
+ const proc = spawn(command, args, {
713
+ stdio: "inherit",
714
+ shell: true,
715
+ });
716
+
717
+ proc.on("close", (code) => {
718
+ if (code === 0) {
719
+ resolve();
720
+ } else {
721
+ reject(new Error(`Command exited with code ${code}`));
722
+ }
723
+ });
724
+
725
+ proc.on("error", reject);
726
+ });
727
+ }