@teardown/cli 1.2.39 → 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.
- package/bin/teardown.js +11 -1
- package/package.json +77 -57
- package/src/cli/commands/init.ts +254 -0
- package/src/cli/commands/plugins.ts +93 -0
- package/src/cli/commands/prebuild.ts +168 -0
- package/src/cli/commands/run.ts +727 -0
- package/src/cli/commands/start.ts +87 -0
- package/src/cli/commands/validate.ts +62 -0
- package/src/cli/index.ts +59 -0
- package/src/config/index.ts +45 -0
- package/src/config/loader.ts +366 -0
- package/src/config/schema.ts +235 -0
- package/src/config/types.ts +322 -0
- package/src/index.ts +177 -0
- package/src/pipeline/cache.ts +179 -0
- package/src/pipeline/index.ts +10 -0
- package/src/pipeline/stages.ts +692 -0
- package/src/plugins/base.ts +370 -0
- package/src/plugins/capabilities/biometrics.ts +64 -0
- package/src/plugins/capabilities/bluetooth.ts +86 -0
- package/src/plugins/capabilities/calendar.ts +57 -0
- package/src/plugins/capabilities/camera.ts +77 -0
- package/src/plugins/capabilities/contacts.ts +57 -0
- package/src/plugins/capabilities/deep-linking.ts +124 -0
- package/src/plugins/capabilities/firebase.ts +138 -0
- package/src/plugins/capabilities/index.ts +96 -0
- package/src/plugins/capabilities/location.ts +87 -0
- package/src/plugins/capabilities/photo-library.ts +80 -0
- package/src/plugins/capabilities/push-notifications.ts +98 -0
- package/src/plugins/capabilities/sign-in-with-apple.ts +53 -0
- package/src/plugins/context.ts +220 -0
- package/src/plugins/index.ts +26 -0
- package/src/plugins/resolver.ts +321 -0
- package/src/templates/generator.ts +507 -0
- package/src/templates/index.ts +9 -0
- package/src/templates/paths.ts +25 -0
- package/src/transformers/android/gradle.ts +400 -0
- package/src/transformers/android/index.ts +19 -0
- package/src/transformers/android/manifest.ts +506 -0
- package/src/transformers/index.ts +39 -0
- package/src/transformers/ios/entitlements.ts +283 -0
- package/src/transformers/ios/index.ts +10 -0
- package/src/transformers/ios/pbxproj.ts +267 -0
- package/src/transformers/ios/plist.ts +198 -0
- package/src/utils/fs.ts +429 -0
- package/src/utils/index.ts +21 -0
- package/src/utils/logger.ts +203 -0
- package/templates/.gitignore +63 -0
- package/templates/Gemfile +3 -0
- package/templates/android/app/build.gradle.kts +97 -0
- package/templates/android/app/proguard-rules.pro +10 -0
- package/templates/android/app/src/main/AndroidManifest.xml +26 -0
- package/templates/android/app/src/main/java/com/appname/MainActivity.kt +22 -0
- package/templates/android/app/src/main/java/com/appname/MainApplication.kt +44 -0
- package/templates/android/app/src/main/res/values/strings.xml +3 -0
- package/templates/android/app/src/main/res/values/styles.xml +7 -0
- package/templates/android/build.gradle.kts +44 -0
- package/templates/android/gradle.properties +39 -0
- package/templates/android/settings.gradle.kts +12 -0
- package/templates/babel.config.js +15 -0
- package/templates/index.js +7 -0
- package/templates/ios/.xcode.env +11 -0
- package/templates/ios/AppName/AppDelegate.swift +25 -0
- package/templates/ios/AppName/AppName-Bridging-Header.h +4 -0
- package/templates/ios/AppName/AppName.entitlements +6 -0
- package/templates/ios/AppName/Images.xcassets/AppIcon.appiconset/Contents.json +35 -0
- package/templates/ios/AppName/Images.xcassets/Contents.json +6 -0
- package/templates/ios/AppName/Info.plist +49 -0
- package/templates/ios/AppName/LaunchScreen.storyboard +38 -0
- package/templates/ios/AppName.xcodeproj/project.pbxproj +402 -0
- package/templates/ios/AppName.xcodeproj/xcshareddata/xcschemes/AppName.xcscheme +78 -0
- package/templates/ios/Podfile +35 -0
- package/templates/metro.config.js +41 -0
- package/templates/package.json +57 -0
- package/templates/react-native.config.js +8 -0
- package/templates/src/app/index.tsx +34 -0
- package/templates/src/assets/fonts/.gitkeep +1 -0
- package/templates/src/assets/images/.gitkeep +1 -0
- package/templates/src/components/ui/accordion.tsx +114 -0
- package/templates/src/components/ui/avatar.tsx +75 -0
- package/templates/src/components/ui/button.tsx +93 -0
- package/templates/src/components/ui/card.tsx +120 -0
- package/templates/src/components/ui/checkbox.tsx +133 -0
- package/templates/src/components/ui/chip.tsx +95 -0
- package/templates/src/components/ui/dialog.tsx +134 -0
- package/templates/src/components/ui/divider.tsx +67 -0
- package/templates/src/components/ui/error-view.tsx +82 -0
- package/templates/src/components/ui/form-field.tsx +101 -0
- package/templates/src/components/ui/index.ts +100 -0
- package/templates/src/components/ui/popover.tsx +92 -0
- package/templates/src/components/ui/pressable-feedback.tsx +88 -0
- package/templates/src/components/ui/radio-group.tsx +153 -0
- package/templates/src/components/ui/scroll-shadow.tsx +108 -0
- package/templates/src/components/ui/select.tsx +165 -0
- package/templates/src/components/ui/skeleton-group.tsx +97 -0
- package/templates/src/components/ui/skeleton.tsx +87 -0
- package/templates/src/components/ui/spinner.tsx +87 -0
- package/templates/src/components/ui/surface.tsx +95 -0
- package/templates/src/components/ui/switch.tsx +124 -0
- package/templates/src/components/ui/tabs.tsx +154 -0
- package/templates/src/components/ui/text-field.tsx +106 -0
- package/templates/src/components/ui/toast.tsx +129 -0
- package/templates/src/contexts/.gitkeep +2 -0
- package/templates/src/core/clients/api/api.client.ts +113 -0
- package/templates/src/core/clients/api/index.ts +1 -0
- package/templates/src/core/clients/storage/index.ts +1 -0
- package/templates/src/core/clients/storage/storage.client.ts +121 -0
- package/templates/src/core/constants/index.ts +19 -0
- package/templates/src/core/core.ts +40 -0
- package/templates/src/core/index.ts +10 -0
- package/templates/src/global.css +87 -0
- package/templates/src/hooks/index.ts +6 -0
- package/templates/src/hooks/use-debounce.ts +23 -0
- package/templates/src/hooks/use-mounted.ts +21 -0
- package/templates/src/index.ts +28 -0
- package/templates/src/lib/index.ts +5 -0
- package/templates/src/lib/utils.ts +115 -0
- package/templates/src/modules/.gitkeep +6 -0
- package/templates/src/navigation/index.ts +8 -0
- package/templates/src/navigation/navigation-provider.tsx +36 -0
- package/templates/src/navigation/router.tsx +137 -0
- package/templates/src/providers/app.provider.tsx +29 -0
- package/templates/src/providers/index.ts +5 -0
- package/templates/src/routes/(tabs)/_layout.tsx +42 -0
- package/templates/src/routes/(tabs)/explore.tsx +161 -0
- package/templates/src/routes/(tabs)/home.tsx +138 -0
- package/templates/src/routes/(tabs)/profile.tsx +151 -0
- package/templates/src/routes/_layout.tsx +18 -0
- package/templates/src/routes/settings.tsx +194 -0
- package/templates/src/screens/auth/index.ts +6 -0
- package/templates/src/screens/auth/login.tsx +165 -0
- package/templates/src/screens/auth/register.tsx +203 -0
- package/templates/src/screens/home.tsx +204 -0
- package/templates/src/screens/index.ts +17 -0
- package/templates/src/screens/profile.tsx +210 -0
- package/templates/src/screens/settings.tsx +216 -0
- package/templates/src/screens/welcome.tsx +101 -0
- package/templates/src/styles/index.ts +103 -0
- package/templates/src/types/common.ts +71 -0
- package/templates/src/types/index.ts +5 -0
- package/templates/tsconfig.json +14 -0
- package/README.md +0 -15
- package/assets/favicon.ico +0 -0
- package/dist/commands/dev/dev.d.ts +0 -22
- package/dist/commands/dev/dev.js +0 -56
- package/dist/commands/dev/dev.js.map +0 -1
- package/dist/commands/init/init-teardown.d.ts +0 -9
- package/dist/commands/init/init-teardown.js +0 -27
- package/dist/commands/init/init-teardown.js.map +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -21
- package/dist/index.js.map +0 -1
- package/dist/modules/dev/dev-menu/keyboard-handler.d.ts +0 -21
- package/dist/modules/dev/dev-menu/keyboard-handler.js +0 -139
- package/dist/modules/dev/dev-menu/keyboard-handler.js.map +0 -1
- package/dist/modules/dev/dev-menu/open-debugger-keyboard-handler.d.ts +0 -18
- package/dist/modules/dev/dev-menu/open-debugger-keyboard-handler.js +0 -106
- package/dist/modules/dev/dev-menu/open-debugger-keyboard-handler.js.map +0 -1
- package/dist/modules/dev/dev-server/cdp/cdp.adapter.d.ts +0 -6
- package/dist/modules/dev/dev-server/cdp/cdp.adapter.js +0 -13
- package/dist/modules/dev/dev-server/cdp/cdp.adapter.js.map +0 -1
- package/dist/modules/dev/dev-server/cdp/index.d.ts +0 -2
- package/dist/modules/dev/dev-server/cdp/index.js +0 -19
- package/dist/modules/dev/dev-server/cdp/index.js.map +0 -1
- package/dist/modules/dev/dev-server/cdp/types.d.ts +0 -107
- package/dist/modules/dev/dev-server/cdp/types.js +0 -3
- package/dist/modules/dev/dev-server/cdp/types.js.map +0 -1
- package/dist/modules/dev/dev-server/dev-server-checker.d.ts +0 -22
- package/dist/modules/dev/dev-server/dev-server-checker.js +0 -73
- package/dist/modules/dev/dev-server/dev-server-checker.js.map +0 -1
- package/dist/modules/dev/dev-server/dev-server.d.ts +0 -74
- package/dist/modules/dev/dev-server/dev-server.js +0 -272
- package/dist/modules/dev/dev-server/dev-server.js.map +0 -1
- package/dist/modules/dev/dev-server/inspector/device.d.ts +0 -46
- package/dist/modules/dev/dev-server/inspector/device.event-reporter.d.ts +0 -37
- package/dist/modules/dev/dev-server/inspector/device.event-reporter.js +0 -166
- package/dist/modules/dev/dev-server/inspector/device.event-reporter.js.map +0 -1
- package/dist/modules/dev/dev-server/inspector/device.js +0 -578
- package/dist/modules/dev/dev-server/inspector/device.js.map +0 -1
- package/dist/modules/dev/dev-server/inspector/inspector.d.ts +0 -27
- package/dist/modules/dev/dev-server/inspector/inspector.js +0 -225
- package/dist/modules/dev/dev-server/inspector/inspector.js.map +0 -1
- package/dist/modules/dev/dev-server/inspector/types.d.ts +0 -156
- package/dist/modules/dev/dev-server/inspector/types.js +0 -3
- package/dist/modules/dev/dev-server/inspector/types.js.map +0 -1
- package/dist/modules/dev/dev-server/inspector/wss/servers/debugger-connection.server.d.ts +0 -14
- package/dist/modules/dev/dev-server/inspector/wss/servers/debugger-connection.server.js +0 -63
- package/dist/modules/dev/dev-server/inspector/wss/servers/debugger-connection.server.js.map +0 -1
- package/dist/modules/dev/dev-server/inspector/wss/servers/device-connection.server.d.ts +0 -19
- package/dist/modules/dev/dev-server/inspector/wss/servers/device-connection.server.js +0 -66
- package/dist/modules/dev/dev-server/inspector/wss/servers/device-connection.server.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/devtools.plugin.d.ts +0 -1
- package/dist/modules/dev/dev-server/plugins/devtools.plugin.js +0 -51
- package/dist/modules/dev/dev-server/plugins/devtools.plugin.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/favicon.plugin.d.ts +0 -1
- package/dist/modules/dev/dev-server/plugins/favicon.plugin.js +0 -19
- package/dist/modules/dev/dev-server/plugins/favicon.plugin.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/multipart.plugin.d.ts +0 -1
- package/dist/modules/dev/dev-server/plugins/multipart.plugin.js +0 -63
- package/dist/modules/dev/dev-server/plugins/multipart.plugin.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/systrace.plugin.d.ts +0 -1
- package/dist/modules/dev/dev-server/plugins/systrace.plugin.js +0 -29
- package/dist/modules/dev/dev-server/plugins/systrace.plugin.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/types.d.ts +0 -11
- package/dist/modules/dev/dev-server/plugins/types.js +0 -3
- package/dist/modules/dev/dev-server/plugins/types.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/index.d.ts +0 -3
- package/dist/modules/dev/dev-server/plugins/wss/index.js +0 -20
- package/dist/modules/dev/dev-server/plugins/wss/index.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-api.server.d.ts +0 -37
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-api.server.js +0 -67
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-api.server.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-debugger.server.d.ts +0 -63
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-debugger.server.js +0 -129
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-debugger.server.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-dev-client.server.d.ts +0 -32
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-dev-client.server.js +0 -76
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-dev-client.server.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-events.server.d.ts +0 -75
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-events.server.js +0 -199
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-events.server.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-hmr.server.d.ts +0 -44
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-hmr.server.js +0 -121
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-hmr.server.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-message.server.d.ts +0 -135
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-message.server.js +0 -364
- package/dist/modules/dev/dev-server/plugins/wss/servers/web-socket-message.server.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/types.d.ts +0 -6
- package/dist/modules/dev/dev-server/plugins/wss/types.js +0 -3
- package/dist/modules/dev/dev-server/plugins/wss/types.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/web-socket-router.d.ts +0 -32
- package/dist/modules/dev/dev-server/plugins/wss/web-socket-router.js +0 -58
- package/dist/modules/dev/dev-server/plugins/wss/web-socket-router.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/web-socket-server-adapter.d.ts +0 -13
- package/dist/modules/dev/dev-server/plugins/wss/web-socket-server-adapter.js +0 -27
- package/dist/modules/dev/dev-server/plugins/wss/web-socket-server-adapter.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/web-socket-server.d.ts +0 -39
- package/dist/modules/dev/dev-server/plugins/wss/web-socket-server.js +0 -47
- package/dist/modules/dev/dev-server/plugins/wss/web-socket-server.js.map +0 -1
- package/dist/modules/dev/dev-server/plugins/wss/wss.plugin.d.ts +0 -24
- package/dist/modules/dev/dev-server/plugins/wss/wss.plugin.js +0 -56
- package/dist/modules/dev/dev-server/plugins/wss/wss.plugin.js.map +0 -1
- package/dist/modules/dev/dev-server/sybmolicate/sybmolicate.plugin.d.ts +0 -7
- package/dist/modules/dev/dev-server/sybmolicate/sybmolicate.plugin.js +0 -41
- package/dist/modules/dev/dev-server/sybmolicate/sybmolicate.plugin.js.map +0 -1
- package/dist/modules/dev/dev-server/sybmolicate/types.d.ts +0 -64
- package/dist/modules/dev/dev-server/sybmolicate/types.js +0 -3
- package/dist/modules/dev/dev-server/sybmolicate/types.js.map +0 -1
- package/dist/modules/dev/terminal/base.terminal.reporter.d.ts +0 -25
- package/dist/modules/dev/terminal/base.terminal.reporter.js +0 -79
- package/dist/modules/dev/terminal/base.terminal.reporter.js.map +0 -1
- package/dist/modules/dev/terminal/terminal.reporter.d.ts +0 -13
- package/dist/modules/dev/terminal/terminal.reporter.js +0 -83
- package/dist/modules/dev/terminal/terminal.reporter.js.map +0 -1
- package/dist/modules/dev/types.d.ts +0 -20
- package/dist/modules/dev/types.js +0 -3
- package/dist/modules/dev/types.js.map +0 -1
- package/dist/modules/dev/utils/log.d.ts +0 -23
- package/dist/modules/dev/utils/log.js +0 -74
- package/dist/modules/dev/utils/log.js.map +0 -1
|
@@ -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
|
+
}
|