catalyst-core 0.3.0-canary.1 → 0.3.0-canary.3

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/changelog.md CHANGED
@@ -1,14 +1,6 @@
1
- # 1.0.0-beta.1
2
-
3
- - Replaced the Webpack build and SSR pipeline with Vite and ESM.
4
- - Added React 19 Suspense-aware routing, split components, and manifest-based asset loading.
5
- - Added opt-in request, compression, flush, and bot-aware OpenTelemetry spans.
6
- - Preserved native application builds, internal plugins, Sentry exports, and safe-area SSR hydration.
7
- - Removed the legacy `devBuild` and `devServe` commands.
8
-
9
1
  # Changelog
10
2
 
11
- ## [0.4.0] - unreleased
3
+ ## [1.0.0] - unreleased
12
4
 
13
5
  This release freezes the public API. The root entry of `catalyst-core` now exports exactly eight names and nothing else: `RouterDataProvider`, `useCurrentRouteData`, `useRouterData`, `MetaTag`, `split`, `hydrationReady`, `Head`, and `Body`. Anything previously reachable through the root entry but not in that list is no longer public. Applications that import a removed name will fail at build time rather than silently resolving to `undefined`.
14
6
 
@@ -68,7 +60,7 @@ Where a hook had two names for one value, the shorter one is canonical and the o
68
60
 
69
61
  Hooks fall into three categories, and the category decides whether a hook has `execute`. A hook that wraps a single native operation exposes it as `execute` plus a domain-specific alias — `takePhoto`, `pickFile`, `openFile`, `signIn`, `trigger`, `request`. A hook that wraps several distinct operations has no single action to name, so its named functions are the API and it has no `execute` at all: `useVideoStream`, `useDataProtection`, and `useNativeTransition` work this way. A read-only hook reports ambient device state and has no action functions at all: `useNetworkStatus`, `useDeviceInfo`, and `useSafeArea` return state plus the runtime-context keys. `useNotification` is the one exception. It carries `execute` and `schedule` as aliases of `scheduleLocal` for backward compatibility. Both are frozen, both are discouraged, and neither should be read as the pattern for multi-action hooks. `execute` never takes an operation name as a string argument; it takes the arguments of the operation itself.
70
62
 
71
- Hook errors are `CatalystError` values from the framework-wide error registry introduced in this release, carrying a `RUNTIME-NATIVE-*` code, a message, a category, a suggested action, a documentation link, and the originating platform error as `cause`. `useVideoStream` in particular used to report seventeen failures as bare `{ message }` objects with no code at all; those now carry real codes, and a camera denial reports the same code whether it comes from the permission hook or the stream.
63
+ Hook errors are `CatalystError` values from the framework-wide error registry introduced in this release, carrying a `RUNTIME-NATIVE-*` code, a message, a category, a suggested action, a documentation link, and the originating platform error as `cause`. `useVideoStream` in particular used to report seventeen failures as bare `{ message }` objects with no code at all; those now carry real codes, and a camera denial reports the same code whether it comes from the permission hook or the stream. The error object's fields changed with it: `action` is now `suggestedAction`, `nativeError` is now `cause`, `recoverable` and `timestamp` are gone, and `code` values are registry codes such as `RUNTIME-NATIVE-001` instead of names such as `PERMISSION_DENIED`, so compare against `ERROR_CODES` from `catalyst-core/errors` rather than string literals.
72
64
 
73
65
  One consequence for error logging: `CatalystError` extends `Error`, so `message` now lives on the prototype rather than as an own property. Reading `error.message` is unaffected, but `JSON.stringify(error)` no longer includes it. Log `error.message` explicitly, or use the `code` and `docUrl` fields.
74
66
 
@@ -86,13 +78,83 @@ The app contract validators in `server/utils/validator.js` used to log a bare me
86
78
 
87
79
  The route and store checks moved out of the per-request path. `validateGetRoutes` and `validateConfigureStore` now run once when the renderer module loads — at server startup in production, and on the first SSR request in development, where the handler is loaded lazily — rather than once per request inside the handler's `try`/`catch`. The shape of `addMiddlewares` from `server/server.js` is checked during server setup.
88
80
 
89
- Module aliases and `config/config.json` keys are not checked at load time; a missing alias or key still surfaces where it is used. An application that satisfies the contract sees no change. The full contract — the files Catalyst imports by fixed path and the required module aliases — is documented under File Conventions.
81
+ Separately, `catalyst start`, `build` and `serve` now check `config/config.json` and `package.json` before starting and exit with a coded error when either is missing, a required config key is missing, or a required module alias is missing. An application that satisfies the contract sees no change. The full contract — the files Catalyst imports by fixed path and the required module aliases — is documented under File Conventions.
82
+ The webpack-era keys `WEBPACK_DEV_SERVER_HOSTNAME`, `WEBPACK_DEV_SERVER_PORT` and `ANALYZE_BUNDLE` are no longer required or read; remove them from `config/config.json`.
83
+
84
+ ## [0.3.0-beta.5] - 2026-08-18
85
+
86
+ - Upgrade react-router-dom v6 to v7.18.2 (CVE fixes) (#399).
87
+ - Docs: clarify setMetaData must return array of JSX elements (#327).
88
+
89
+ ## [0.3.0-beta.4] - 2026-08-10
90
+
91
+ - Detect CommonJS by parsing, not by matching source text (#397).
92
+ - Sub-path base + HMR config for reverse-proxy dev setups (#379).
93
+
94
+ ## [0.3.0-beta.3] - 2026-08-04
95
+
96
+ - Resolve semgrep and dependency findings from security scan (#375).
97
+
98
+ ## [0.3.0-beta.2] - 2026-08-03
99
+
100
+ - @catalyst/cloud-ai — multi-provider AI integration with useAI hook (#288).
101
+ - Eliminate SSR streaming race between onShellReady and onAllReady (#323).
102
+ - Update iOS release docs and MCP guidance (#324).
103
+ - Patch @tailwindcss/postcss base so Vite scans the whole app (#326).
104
+
105
+ ## [0.3.0-beta.1] - 2026-07-15
106
+
107
+ - Replaced the Webpack build and SSR pipeline with Vite and ESM.
108
+ - Added React 19 Suspense-aware routing, split components, and manifest-based asset loading.
109
+ - Added opt-in request, compression, flush, and bot-aware OpenTelemetry spans.
110
+ - Preserved native application builds, internal plugins, Sentry exports, and safe-area SSR hydration.
111
+ - Removed the legacy `devBuild` and `devServe` commands.
112
+
113
+ ## [0.2.0-beta.3] - 2026-07-14
114
+
115
+ - Catalyst Profiler Runtime (#274).
116
+
117
+ ## [0.2.0-beta.2] - 2026-07-09
118
+
119
+ - Route Caching (#287).
120
+ - Android build fixes (#303).
121
+
122
+ ## [0.2.0-beta.1] - 2026-07-08
123
+
124
+ - Catalyst react + rendering update (#293).
125
+ - Add GitHub feedback integration to Catalyst MCP v2 (#272).
126
+ - Sync MCP v2: extract seeding, delegate docs sync, add FTS + initial sync (#295).
127
+
128
+ ## [0.1.0-beta.7] - 2026-06-29
129
+
130
+ - Syncing dweb branch with main (#283).
131
+
132
+ ## [0.1.0-beta.6] - 2026-06-11
133
+
134
+ - Add examples folder with local testing app for catalyst-core (#246).
135
+
136
+ ## [0.1.0-beta.5] - 2026-06-09
137
+
138
+ - Semgrep Plugin Fixes (#266).
139
+
140
+ ## [0.1.0-beta.4] - 2026-06-03
141
+
142
+ - Merge video hook, web fallback, and plugin system into monorepo (#258).
143
+
144
+ ## [0.1.0-beta.3] - 2026-06-02
145
+
146
+ - Security Fixes (semgrep) (#259).
147
+ - Catalyst Docs Mismatches Fix (#244).
90
148
 
91
149
  ## [0.1.0-beta.2] - 2026-05-06
92
150
 
93
151
  - Moved Catalyst into a monorepo structure with `catalyst-core`, `create-catalyst-app`, the Catalyst docs app, and the Catalyst core test app managed from one repository.
94
152
  - Replaced the internal-package pre-release flow with a local release sandbox that scaffolds a real app using current-branch CCA and a locally packed current-branch `catalyst-core`.
95
153
 
154
+ ## [0.1.0-canary.9] - 2026-05-06
155
+
156
+ - Changelog-only update; same code as 0.1.0-beta.2.
157
+
96
158
  ## [0.1.0-canary.8] - 2026-04-15
97
159
 
98
160
  - Introduced Catalyst’s internal plugin architecture, adding the `PluginBridge` web API, native plugin runtimes for Android and iOS, and manifest-driven build composition for modular native capabilities.
@@ -120,7 +182,7 @@ Module aliases and `config/config.json` keys are not checked at load time; a mis
120
182
  - Improved universal app runtime behavior with safe-area inset support, edge-to-edge rendering, and notification permission override fixes.
121
183
  - Expanded platform support with offline fallback handling, notification/access-control refinements, localhost HTTP allowances for local development, and file-picker/HTTPS server improvements.
122
184
 
123
- ## [0.1.0-canary.4] - 2026-02-12
185
+ ## [0.1.0-canary.4] - 2026-02-16
124
186
 
125
187
  - Added Google Sign-In support for both Android and iOS in Catalyst, enabling a unified native authentication experience for apps built on the framework.
126
188
  - Improved release/build reliability with related bridge and CI updates, making integration smoother and more consistent across platforms.
@@ -148,26 +210,26 @@ Module aliases and `config/config.json` keys are not checked at load time; a mis
148
210
  - Notification refactor: streamlined config toggle for local vs push, centralized permission/asset handling, and simpler routing into the web app
149
211
  - Access control: combined access control config for ios
150
212
 
151
- ## [0.0.3-canary.18] - 2025-11-19
213
+ ## [0.0.3-canary.18] - 2025-11-25
152
214
 
153
215
  - Configuration Files
154
216
  - Android Native WebView - MainActivity.kt
155
217
  - Android Native WebView - CustomWebview.kt
156
218
 
157
- ## [0.0.3-canary.17] - 2025-11-19
219
+ ## [0.0.3-canary.17] - 2025-11-21
158
220
 
159
- - ✨ Added complete notification system (local & push)
160
- - 🔔 New useNotification() React hook
161
- - 📱 Firebase Cloud Messaging conditional build support
162
- - 🤖 Android notification handling in MainActivity
163
- - 🔧 5 new notification commands + 5 callbacks in NativeBridge
164
- - 📦 Auto-processing of notification icons & sounds
165
- - ⚙️ Conditional dependencies based on notification config
166
- - 🧹 Updated .gitignore & .npmignore
167
- - 📋 Enhanced build process with automatic permission injection
168
- - 🔗 Deep link integration with notifications
221
+ - Added complete notification system (local & push)
222
+ - New useNotification() React hook
223
+ - Firebase Cloud Messaging conditional build support
224
+ - Android notification handling in MainActivity
225
+ - 5 new notification commands + 5 callbacks in NativeBridge
226
+ - Auto-processing of notification icons & sounds
227
+ - Conditional dependencies based on notification config
228
+ - Updated .gitignore & .npmignore
229
+ - Enhanced build process with automatic permission injection
230
+ - Deep link integration with notifications
169
231
 
170
- ## [0.0.3-canary.16] - 2025-11-19
232
+ ## [0.0.3-canary.16] - 2025-11-20
171
233
 
172
234
  - FeaturesGallery Support for File Picker: Enhanced file picker to open native gallery for selecting images and videos
173
235
  - HTTPS Framework Server: Enabled HTTPS support for the framework server with self-signed SSL certificate
@@ -177,17 +239,17 @@ Module aliases and `config/config.json` keys are not checked at load time; a mis
177
239
 
178
240
  - Adding Android release support and appInfo key
179
241
 
180
- ## [0.0.3-canary.14] - 2025-09-29
242
+ ## [0.0.3-canary.14] - 2025-10-29
181
243
 
182
244
  - Android custom splashscreen
183
245
 
184
- ## [0.0.3-canary.13] - 2025-09-28
246
+ ## [0.0.3-canary.13] - 2025-10-28
185
247
 
186
248
  - Multi file support in useFilePicker hook
187
249
  - Android multiple app icon support
188
250
  - Android whitelisting toggle support
189
251
 
190
- ## [0.0.3-canary.12] - 2025-09-26
252
+ ## [0.0.3-canary.12] - 2025-10-13
191
253
 
192
254
  - Universal App Context: Added comprehensive context support for building universal mobile applications
193
255
  - OpenTelemetry SDK Integration: Built-in support for observability and performance monitoring with Otel SDK
@@ -198,70 +260,42 @@ Module aliases and `config/config.json` keys are not checked at load time; a mis
198
260
  - Enhanced Android native support: Add support for native keyboard and webview resizing in Android
199
261
  - Device info API web support: Web support in device info API
200
262
 
201
- ## [0.0.3-canary.10] - 2025-09-11
263
+ ## [0.0.3-canary.10] - 2025-09-17
202
264
 
203
- - **Sentry import bug fix**
204
- - **Open telemetry SDK**
265
+ - Sentry import bug fix.
266
+ - Open telemetry SDK.
205
267
 
206
268
  ## [0.0.3-canary.9] - 2025-09-11
207
269
 
208
- - **App name configuration**
209
- - **Device Info API**
210
- - **Access control configuration**
211
-
212
- ## [0.0.3-canary.8] - 2025-01-06
213
-
214
- - **App icon configuration**
215
- - **Splash screen integration**
216
- - **Url Whitelisting**
270
+ - App name configuration.
271
+ - Device Info API.
272
+ - Access control configuration.
217
273
 
218
- ## [0.0.3-canary.7] - 2025-01-06
274
+ ## [0.0.3-canary.8] - 2025-08-08
219
275
 
220
- ### 🚀 Features
276
+ - App icon configuration.
277
+ - Splash screen integration.
278
+ - Url Whitelisting.
221
279
 
222
- - **Native API Integration**: Enhanced native module integration for better performance
223
- - **Automated IP Resolution**: Intelligent network detection eliminates manual IP configuration
224
- - **Universal Build System**: Streamlined one-command builds for both Android and iOS platforms
225
- - **Enhanced CLI Interface**: Improved command-line tools for better developer experience
226
-
227
- ### 🐛 Bug Fixes
280
+ ## [0.0.3-canary.7] - 2025-08-06
228
281
 
282
+ - Native API Integration: Enhanced native module integration for better performance
283
+ - Automated IP Resolution: Intelligent network detection eliminates manual IP configuration
284
+ - Universal Build System: Streamlined one-command builds for both Android and iOS platforms
285
+ - Enhanced CLI Interface: Improved command-line tools for better developer experience
229
286
  - Fixed IP address detection issues on development environments
230
287
  - Resolved build configuration conflicts between platforms
231
288
  - Improved error handling and recovery mechanisms
232
-
233
- ### 🔧 Improvements
234
-
235
- - **Performance**: Optimized native module loading and execution
236
- - **Developer Experience**: Enhanced CLI with better error messages and debugging info
237
- - **Configuration**: Dynamic config updates without requiring restarts
238
- - **Build Process**: Faster and more reliable build pipeline
239
-
240
- ### 🧪 Testing & Validation
241
-
242
- - ✅ **Android Build**: Verified APK generation, installation, and runtime behavior
243
- - ✅ **iOS Build**: Tested build process and device compatibility
244
- - ✅ **IP Detection**: Validated automatic network resolution across platforms
245
- - ✅ **Native Modules**: Confirmed API integration and performance benchmarks
246
- - ✅ **CLI Operations**: Tested enhanced command-line workflows and error handling
247
-
248
- ### 🔄 Breaking Changes
249
-
250
- **None** - All changes maintain backward compatibility while enhancing existing functionality
251
-
252
- ### 📝 Technical Details
253
-
289
+ - Performance: Optimized native module loading and execution
290
+ - Developer Experience: Enhanced CLI with better error messages and debugging info
291
+ - Configuration: Dynamic config updates without requiring restarts
292
+ - Build Process: Faster and more reliable build pipeline
293
+ - Android Build: Verified APK generation, installation, and runtime behavior
294
+ - iOS Build: Tested build process and device compatibility
295
+ - IP Detection: Validated automatic network resolution across platforms
296
+ - Native Modules: Confirmed API integration and performance benchmarks
297
+ - CLI Operations: Tested enhanced command-line workflows and error handling
254
298
  - Improved native API bridge for better cross-platform communication
255
299
  - Enhanced error reporting with detailed stack traces and suggestions
256
300
  - Optimized build configuration for faster development cycles
257
301
  - Added comprehensive logging for debugging and monitoring
258
-
259
- ### 🔗 Migration Notes
260
-
261
- - No migration steps required
262
- - Existing projects will automatically benefit from improvements
263
- - Optional: Update CLI usage to leverage new enhanced features
264
-
265
- ## Version
266
-
267
- - Target version: 0.0.3-canary.7
@@ -28,7 +28,9 @@ export const ERROR_CODES = {
28
28
 
29
29
  BUNDLE_UPSTREAM_ERROR: "BUNDLE-000",
30
30
  IOS_UPSTREAM_ERROR: "IOS-000",
31
+ IOS_SIMULATOR_APP_NOT_FOUND: "IOS-001",
31
32
  ANDROID_UPSTREAM_ERROR: "ANDROID-000",
33
+ ANDROID_EMULATOR_BOOT_TIMEOUT: "ANDROID-001",
32
34
 
33
35
  RUNTIME_NATIVE_PERMISSION_DENIED: "RUNTIME-NATIVE-001",
34
36
  RUNTIME_NATIVE_PERMISSION_REQUIRED: "RUNTIME-NATIVE-002",
@@ -231,12 +233,27 @@ export const ERROR_DEFINITIONS = {
231
233
  defaultDetails: "This wraps an error from Xcode/CocoaPods. See the printed upstream output for the cause.",
232
234
  suggestedAction: "Read the upstream Xcode/CocoaPods error printed above and fix the underlying issue.",
233
235
  },
236
+ [ERROR_CODES.IOS_SIMULATOR_APP_NOT_FOUND]: {
237
+ category: "IOS",
238
+ defaultMessage: "Could not find an installed iOS Simulator app",
239
+ defaultDetails:
240
+ "Neither DeviceHub.app (Xcode 27+, bundle id com.apple.dt.Devices) nor the legacy Simulator.app (bundle id com.apple.iphonesimulator) is installed under Xcode's Contents/Applications. The simulator itself may still boot headlessly via simctl, but catalyst-core cannot open or focus its window.",
241
+ suggestedAction: "Confirm a full Xcode.app (not just Command Line Tools) is installed and selected via xcode-select -p, then reopen it once from Spotlight so macOS registers its app bundle.",
242
+ },
234
243
  [ERROR_CODES.ANDROID_UPSTREAM_ERROR]: {
235
244
  category: "ANDROID",
236
245
  defaultMessage: "Android build failed in an upstream toolchain step",
237
246
  defaultDetails: "This wraps an error from Gradle. See the printed upstream output for the cause.",
238
247
  suggestedAction: "Read the upstream Gradle error printed above and fix the underlying issue.",
239
248
  },
249
+ [ERROR_CODES.ANDROID_EMULATOR_BOOT_TIMEOUT]: {
250
+ category: "ANDROID",
251
+ defaultMessage: "Timed out waiting for the Android emulator to boot",
252
+ defaultDetails:
253
+ "buildApp:android launched the configured AVD but it did not appear in `adb devices` or did not finish booting (sys.boot_completed=1 and init.svc.bootanim=stopped) within the timeout.",
254
+ suggestedAction:
255
+ "Try booting the AVD manually first (emulator -avd <name>) to see the actual startup error, check available disk space/RAM, or increase the emulator's cold-boot allowance if the device is unusually slow.",
256
+ },
240
257
 
241
258
  [ERROR_CODES.RUNTIME_NATIVE_PERMISSION_DENIED]: {
242
259
  category: "RUNTIME-NATIVE",
@@ -244,6 +244,13 @@
244
244
  "suggestedAction": "Read the upstream Xcode/CocoaPods error printed above and fix the underlying issue.",
245
245
  "docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/IOS/IOS-000.md"
246
246
  },
247
+ "IOS-001": {
248
+ "category": "IOS",
249
+ "message": "Could not find an installed iOS Simulator app",
250
+ "details": "Neither DeviceHub.app (Xcode 27+, bundle id com.apple.dt.Devices) nor the legacy Simulator.app (bundle id com.apple.iphonesimulator) is installed under Xcode's Contents/Applications. The simulator itself may still boot headlessly via simctl, but catalyst-core cannot open or focus its window.",
251
+ "suggestedAction": "Confirm a full Xcode.app (not just Command Line Tools) is installed and selected via xcode-select -p, then reopen it once from Spotlight so macOS registers its app bundle.",
252
+ "docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/IOS/IOS-001.md"
253
+ },
247
254
  "ANDROID-000": {
248
255
  "category": "ANDROID",
249
256
  "message": "Android build failed in an upstream toolchain step",
@@ -251,6 +258,13 @@
251
258
  "suggestedAction": "Read the upstream Gradle error printed above and fix the underlying issue.",
252
259
  "docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/ANDROID/ANDROID-000.md"
253
260
  },
261
+ "ANDROID-001": {
262
+ "category": "ANDROID",
263
+ "message": "Timed out waiting for the Android emulator to boot",
264
+ "details": "buildApp:android launched the configured AVD but it did not appear in `adb devices` or did not finish booting (sys.boot_completed=1 and init.svc.bootanim=stopped) within the timeout.",
265
+ "suggestedAction": "Try booting the AVD manually first (emulator -avd <name>) to see the actual startup error, check available disk space/RAM, or increase the emulator's cold-boot allowance if the device is unusually slow.",
266
+ "docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/ANDROID/ANDROID-001.md"
267
+ },
254
268
  "RUNTIME-NATIVE-001": {
255
269
  "category": "RUNTIME-NATIVE",
256
270
  "message": "Permission denied",
@@ -2,6 +2,10 @@
2
2
 
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
+ const {
6
+ spawn,
7
+ execFileSync
8
+ } = require("child_process");
5
9
  const {
6
10
  buildAndroidAAB
7
11
  } = require("../renameAndroidProject.js");
@@ -101,36 +105,176 @@ function createBuildPhase(ctx) {
101
105
  return false;
102
106
  }
103
107
  }
108
+
109
+ // Returns the serial of an online emulator (adb state "device"), or null if
110
+ // none is online. `devices.includes("emulator")` alone would also match a
111
+ // serial like "emulator-5554" whose state is "offline" or still enumerating —
112
+ // i.e. a device that's connected but not actually ready — so this checks the
113
+ // state column explicitly rather than doing a substring match.
104
114
  async function checkEmulator(ADB_PATH) {
105
115
  try {
106
116
  // nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
107
117
  const devices = ctx.runCommand(`${ADB_PATH} devices`);
108
- return devices.includes("emulator");
118
+ const onlineEmulator = devices.split("\n").map(line => line.trim().split(/\s+/)).find(([id, state]) => id?.startsWith("emulator-") && state === "device");
119
+ return onlineEmulator ? onlineEmulator[0] : null;
109
120
  } catch (error) {
110
121
  progress.log("Error checking emulator status: " + error.message, "error");
111
- return false;
122
+ return null;
112
123
  }
113
124
  }
125
+
126
+ // The Android emulator binary is a long-running server process — it does not
127
+ // exit after boot, it keeps running until the emulator window is closed. Using
128
+ // runInteractiveCommand (which only resolves on the child's `close` event, see
129
+ // utils.js) would block forever waiting for an exit that never happens before
130
+ // the app finishes booting. Spawn it detached instead (same approach as
131
+ // androidSetup.js's startEmulator) and let handleEmulatorSetup poll for
132
+ // readiness separately.
133
+ /**
134
+ * Launch the emulator detached and return once it has actually spawned
135
+ * (not once it exits — see the note above on why runInteractiveCommand
136
+ * can't be used here).
137
+ * @returns {Promise<number>} the spawned process's pid, so a caller whose
138
+ * boot-readiness wait times out can kill it instead of leaving a
139
+ * CPU/RAM-heavy orphaned emulator running past a failed build.
140
+ */
114
141
  async function startEmulator(EMULATOR_PATH, androidConfig) {
115
142
  progress.log(`Starting emulator: ${androidConfig.emulatorName}...`, "info");
116
- // nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - EMULATOR_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
117
- return ctx.runInteractiveCommand(EMULATOR_PATH, ["-avd", androidConfig.emulatorName, "-read-only"], {}).then(() => {
118
- progress.log("Emulator started successfully", "success");
119
- }).catch(error => {
120
- progress.log("Error starting emulator: " + error.message, "error");
121
- throw error;
143
+ return new Promise((resolve, reject) => {
144
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - spawn with an argv array (no shell), so androidConfig.emulatorName can't be interpreted as shell syntax even though it comes from a locally-editable config file.
145
+ const child = spawn(EMULATOR_PATH, ["-avd", androidConfig.emulatorName, "-read-only"], {
146
+ detached: true,
147
+ stdio: "ignore"
148
+ });
149
+ child.once("error", error => {
150
+ progress.log("Error starting emulator: " + error.message, "error");
151
+ reject(error);
152
+ });
153
+ // "error" only fires for spawn failures (e.g. bad EMULATOR_PATH); once the
154
+ // process has actually spawned we can detach and let the caller poll adb
155
+ // for boot readiness rather than waiting on this child at all.
156
+ child.once("spawn", () => {
157
+ const {
158
+ pid
159
+ } = child;
160
+ child.unref();
161
+ progress.log("Emulator process started, waiting for it to boot...", "info");
162
+ resolve(pid);
163
+ });
122
164
  });
123
165
  }
166
+
167
+ // Best-effort cleanup for an emulator process this build spawned but that
168
+ // never became ready — kills the whole detached process group (negative pid)
169
+ // so we don't leave a CPU/RAM-heavy orphaned emulator running after a failed
170
+ // build. Never throws: a failure here shouldn't mask the original timeout error.
171
+ function killOrphanedEmulator(pid, emulatorName) {
172
+ try {
173
+ process.kill(-pid, "SIGTERM");
174
+ progress.log(`Stopped orphaned emulator process (pid ${pid}) after boot failure`, "info");
175
+ } catch (error) {
176
+ progress.log(`Could not stop orphaned emulator "${emulatorName}" (pid ${pid}): ${error.message}. ` + "You may need to quit it manually.", "warning");
177
+ }
178
+ }
179
+ function listEmulatorSerials(ADB_PATH) {
180
+ try {
181
+ // nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
182
+ const devices = ctx.runCommand(`${ADB_PATH} devices`);
183
+ return devices.split("\n").map(line => line.trim().split(/\s+/)[0]).filter(id => id && id.startsWith("emulator-"));
184
+ } catch {
185
+ return [];
186
+ }
187
+ }
188
+
189
+ // Waits for a NEW emulator-XXXX serial to appear in `adb devices` (one not in
190
+ // knownSerials, captured before startEmulator was called), so boot polling can
191
+ // be scoped to the emulator this build just launched rather than whichever
192
+ // device `adb shell` (with no -s) happens to pick when multiple are connected.
193
+ async function waitForNewEmulatorSerial(ADB_PATH, knownSerials, {
194
+ timeoutMs = 30000,
195
+ pollIntervalMs = 1000
196
+ } = {}) {
197
+ const deadline = Date.now() + timeoutMs;
198
+ while (Date.now() < deadline) {
199
+ const current = listEmulatorSerials(ADB_PATH);
200
+ const newSerial = current.find(id => !knownSerials.includes(id));
201
+ if (newSerial) return newSerial;
202
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
203
+ }
204
+ return null;
205
+ }
206
+
207
+ /**
208
+ * Poll a specific emulator (by adb serial, e.g. "emulator-5554") until it
209
+ * reports a completed boot and a stopped boot animation, or the timeout
210
+ * elapses.
211
+ * @param {string} ADB_PATH Path to the adb executable.
212
+ * @param {string} serial The emulator's adb serial to target (adb -s).
213
+ * @param {Object} [options]
214
+ * @param {number} [options.timeoutMs=120000] Overall deadline in milliseconds.
215
+ * @param {number} [options.pollIntervalMs=2000] Delay between readiness checks.
216
+ * @returns {Promise<boolean>} true once both readiness signals are observed;
217
+ * false if the deadline elapses first (a transient/hung adb call counts
218
+ * against the same deadline via the per-call execFileSync timeout below).
219
+ */
220
+ async function waitForEmulatorBoot(ADB_PATH, serial, {
221
+ timeoutMs = 120000,
222
+ pollIntervalMs = 2000
223
+ } = {}) {
224
+ const deadline = Date.now() + timeoutMs;
225
+ const getprop = prop => {
226
+ const remaining = Math.max(deadline - Date.now(), 1000);
227
+ return execFileSync(ADB_PATH, ["-s", serial, "shell", "getprop", prop], {
228
+ encoding: "utf8",
229
+ timeout: remaining
230
+ }).trim();
231
+ };
232
+ while (Date.now() < deadline) {
233
+ try {
234
+ const bootCompleted = getprop("sys.boot_completed");
235
+ const bootAnimDone = getprop("init.svc.bootanim");
236
+ if (bootCompleted === "1" && bootAnimDone === "stopped") return true;
237
+ } catch {
238
+ // adb not ready yet (device still enumerating), or this call hit its
239
+ // own per-call timeout — either way, keep polling against the outer
240
+ // deadline rather than treating it as fatal.
241
+ }
242
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
243
+ }
244
+ return false;
245
+ }
246
+
247
+ // Message mirrors errors/registry.js ERROR_DEFINITIONS[ANDROID-001] (see
248
+ // errors/ANDROID/ANDROID-001.md) — this CJS subtree can't import the ESM
249
+ // error registry (see buildErrorFormat.js), so the code is prefixed onto the
250
+ // thrown message directly rather than imported.
251
+ function emulatorBootTimeoutError(detail) {
252
+ return new Error(`[ANDROID-001] Timed out waiting for the Android emulator to boot: ${detail}`);
253
+ }
124
254
  async function handleEmulatorSetup(ADB_PATH, EMULATOR_PATH, androidConfig) {
125
255
  progress.log("Setting up emulator...", "info");
126
- const emulatorRunning = await checkEmulator(ADB_PATH);
127
- if (!emulatorRunning) {
256
+ let serial = await checkEmulator(ADB_PATH);
257
+ let pid = null;
258
+ if (!serial) {
128
259
  progress.log("No emulator running, attempting to start one...", "info");
129
- await startEmulator(EMULATOR_PATH, androidConfig);
130
- await new Promise(resolve => setTimeout(resolve, 5000));
260
+ const knownSerials = listEmulatorSerials(ADB_PATH);
261
+ pid = await startEmulator(EMULATOR_PATH, androidConfig);
262
+ serial = await waitForNewEmulatorSerial(ADB_PATH, knownSerials);
263
+ if (!serial) {
264
+ killOrphanedEmulator(pid, androidConfig.emulatorName);
265
+ throw emulatorBootTimeoutError(`emulator "${androidConfig.emulatorName}" never appeared in adb devices`);
266
+ }
131
267
  } else {
132
- progress.log("Emulator already running", "success");
268
+ progress.log(`Emulator ${serial} already online, confirming it's fully booted...`, "info");
269
+ }
270
+ // Wait for boot readiness whether the emulator was already online (it may
271
+ // still be finishing its boot animation) or was just launched above.
272
+ const booted = await waitForEmulatorBoot(ADB_PATH, serial);
273
+ if (!booted) {
274
+ if (pid) killOrphanedEmulator(pid, androidConfig.emulatorName);
275
+ throw emulatorBootTimeoutError(`emulator "${androidConfig.emulatorName}" (${serial}) never finished booting`);
133
276
  }
277
+ progress.log("Emulator booted successfully", "success");
134
278
  return {
135
279
  type: "emulator",
136
280
  name: androidConfig.emulatorName
@@ -286,6 +430,7 @@ function createBuildPhase(ctx) {
286
430
  checkEmulator,
287
431
  handleEmulatorSetup,
288
432
  startEmulator,
433
+ waitForEmulatorBoot,
289
434
  buildApp,
290
435
  launchApp,
291
436
  createAABConfig,