catalyst-core 1.1.0 → 1.2.0-canary.2

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 (69) hide show
  1. package/bin/catalyst.js +54 -6
  2. package/changelog.md +27 -1
  3. package/dist/errors/registry.js +306 -18
  4. package/dist/errors-index.json +273 -70
  5. package/dist/native/androidProject/app/build.gradle.kts +53 -79
  6. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
  10. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
  11. package/dist/native/androidSetup.js +76 -170
  12. package/dist/native/androidToolchain.js +642 -0
  13. package/dist/native/appIcon.js +100 -0
  14. package/dist/native/buildAndroid/assets.js +29 -5
  15. package/dist/native/buildAndroid/build.js +110 -223
  16. package/dist/native/buildAndroid/config.js +57 -65
  17. package/dist/native/buildAndroid/devices.js +176 -0
  18. package/dist/native/buildAndroid/index.js +271 -46
  19. package/dist/native/buildAndroid/output.js +43 -0
  20. package/dist/native/buildAndroid/signing.js +456 -0
  21. package/dist/native/buildAppAndroid.js +27 -7
  22. package/dist/native/buildAppIos.js +139 -20
  23. package/dist/native/buildIos/assets.js +76 -4
  24. package/dist/native/buildIos/build.js +31 -181
  25. package/dist/native/buildIos/config.js +126 -11
  26. package/dist/native/buildIos/index.js +354 -34
  27. package/dist/native/buildIos/localhostCert.js +87 -0
  28. package/dist/native/buildIos/output.js +28 -0
  29. package/dist/native/buildIos/signing.js +596 -0
  30. package/dist/native/credentials.js +309 -0
  31. package/dist/native/exportCredentials.js +51 -0
  32. package/dist/native/iosToolchain.js +583 -0
  33. package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
  34. package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
  35. package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
  36. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
  37. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
  38. package/dist/native/localState.js +148 -0
  39. package/dist/native/nativeConfig.js +621 -0
  40. package/dist/native/releaseOutput.js +53 -0
  41. package/dist/native/renameAndroidProject.js +154 -369
  42. package/dist/native/setupEmulatorIos.js +58 -275
  43. package/dist/native/setupServer.js +520 -124
  44. package/dist/native/utils.js +418 -115
  45. package/dist/vite/resolveDevServerConfig.js +21 -0
  46. package/mcp_v2/knowledge-base.json +13 -11
  47. package/mcp_v2/test/config.test.ts +40 -0
  48. package/mcp_v2/tools/build.js +80 -104
  49. package/mcp_v2/tools/config.js +181 -174
  50. package/package.json +1 -1
  51. package/test/cli/nativeFlags.test.ts +97 -0
  52. package/test/native/androidDevices.test.ts +230 -0
  53. package/test/native/androidElfAlignment.test.ts +148 -0
  54. package/test/native/androidSigning.test.ts +646 -0
  55. package/test/native/androidToolchain.test.ts +766 -0
  56. package/test/native/appIcon.test.ts +124 -0
  57. package/test/native/credentials.test.ts +241 -0
  58. package/test/native/interaction.test.ts +179 -0
  59. package/test/native/iosConfigPhase.test.ts +611 -0
  60. package/test/native/iosSigning.test.ts +1060 -0
  61. package/test/native/iosToolchain.test.ts +821 -0
  62. package/test/native/localState.test.ts +80 -0
  63. package/test/native/nativeConfig.test.ts +602 -0
  64. package/test/native/setupServer.test.ts +529 -0
  65. package/test/native/utils.test.ts +102 -127
  66. package/test/vite/resolveDevServerConfig.test.js +25 -0
  67. package/dist/native/androidProject/keystore/Untitled +0 -0
  68. package/dist/native/androidProject/keystore/release-key.jks +0 -0
  69. package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
@@ -8,7 +8,9 @@ import android.view.MotionEvent
8
8
  import android.view.WindowManager
9
9
  import androidx.activity.OnBackPressedCallback
10
10
  import androidx.appcompat.app.AppCompatActivity
11
+ import androidx.core.graphics.Insets
11
12
  import androidx.core.view.ViewCompat
13
+ import androidx.core.view.WindowInsetsCompat
12
14
 
13
15
  import org.json.JSONObject
14
16
  import java.util.Properties
@@ -51,6 +53,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
51
53
  private var isHardwareAccelerationEnabled = false
52
54
  private var currentUrl: String = ""
53
55
  private var edgeToEdgeEnabled = false
56
+ private var backCallback: OnBackPressedCallback? = null
54
57
  private var latestSafeAreaInsets: SafeAreaInsets = SafeAreaInsets.ZERO
55
58
  private var splashOverlay: SplashOverlay? = null
56
59
 
@@ -77,6 +80,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
77
80
  }
78
81
  }
79
82
 
83
+ // targetSdk 35+ draws every window edge-to-edge on Android 15+, and Android 16 removed
84
+ // the opt-out. With edgeToEdge off in the config, the shell pads its root view by the
85
+ // system-bar insets instead, so content still sits between the bars.
86
+ private val systemDrawsEdgeToEdge: Boolean
87
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
88
+
80
89
  private fun configureEdgeToEdge() {
81
90
  edgeToEdgeEnabled = EdgeToEdgeUtils.isEnabled(properties)
82
91
 
@@ -119,6 +128,16 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
119
128
  // Track inset changes (first layout, rotation, cutout, bar visibility); insets stay unconsumed
120
129
  var lastCachedInsets = cachedInsets
121
130
  ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets ->
131
+ // edgeToEdge off on Android 15+: pad by the bars, and hand the children insets
132
+ // without them so the WebView doesn't inset its content a second time.
133
+ var childInsets = insets
134
+ if (!edgeToEdgeEnabled && systemDrawsEdgeToEdge) {
135
+ val barTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
136
+ val bars = insets.getInsets(barTypes)
137
+ view.setPadding(bars.left, bars.top, bars.right, bars.bottom)
138
+ childInsets = WindowInsetsCompat.Builder(insets).setInsets(barTypes, Insets.NONE).build()
139
+ }
140
+
122
141
  val updatedInsets = SafeAreaUtils.fromWindowInsets(
123
142
  insets,
124
143
  edgeToEdgeEnabled,
@@ -141,7 +160,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
141
160
  lastCachedInsets = updatedInsets
142
161
  }
143
162
 
144
- insets
163
+ childInsets
145
164
  }
146
165
  ViewCompat.requestApplyInsets(rootView)
147
166
  }
@@ -327,7 +346,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
327
346
  }
328
347
 
329
348
  // Initialize keyboard utility (pass webView so keyboard events reach WebPerfCollector)
330
- keyboardUtil = KeyboardUtil(this, binding.webviewContainer, binding.webview, edgeToEdgeEnabled)
349
+ keyboardUtil = KeyboardUtil(
350
+ this,
351
+ binding.webviewContainer,
352
+ binding.webview,
353
+ edgeToEdgeEnabled || systemDrawsEdgeToEdge
354
+ )
331
355
  keyboardUtil.initialize()
332
356
 
333
357
  // Enable hardware acceleration for the window
@@ -381,7 +405,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
381
405
  setupSafeAreaHandling()
382
406
 
383
407
  // Setup back press handler (modern API)
384
- onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
408
+ // Android 16 turns predictive back on by default; OnBackPressedDispatcher is the
409
+ // supported path for it. The callback steps aside to let the system leave the app
410
+ // and is re-armed in onResume, since a task moved to the back keeps this activity.
411
+ backCallback = object : OnBackPressedCallback(true) {
385
412
  override fun handleOnBackPressed() {
386
413
  if (customWebView.canGoBack()) {
387
414
  if (BuildConfig.DEBUG) {
@@ -397,7 +424,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
397
424
  onBackPressedDispatcher.onBackPressed()
398
425
  }
399
426
  }
400
- })
427
+ }.also { onBackPressedDispatcher.addCallback(this, it) }
401
428
 
402
429
  // Load URL based on environment and deep link data
403
430
  try {
@@ -471,6 +498,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
471
498
 
472
499
  override fun onResume() {
473
500
  super.onResume()
501
+ backCallback?.isEnabled = true
474
502
  customWebView.onResume()
475
503
  }
476
504
 
@@ -18,6 +18,7 @@ class KeyboardUtil(
18
18
  private val activity: Activity,
19
19
  private val webViewContainer: View,
20
20
  private val webView: WebView? = null,
21
+ // True whenever the window draws behind the system bars: edgeToEdge on, or Android 15+.
21
22
  private val edgeToEdgeEnabled: Boolean = false,
22
23
  ) {
23
24
  private var originalHeight: Int = 0
@@ -81,9 +82,12 @@ class KeyboardUtil(
81
82
  val keyboardHeight = if (edgeToEdgeEnabled) {
82
83
  // Edge-to-edge: rootView spans behind the system bars, so measure how far the
83
84
  // visible frame's bottom sits above rootView's bottom (screen coords; works in split-screen)
85
+ // The parent's bottom padding is the navigation bar when MainActivity pads for
86
+ // system bars (edgeToEdge off on Android 15+); the keyboard covers that strip.
84
87
  val location = IntArray(2)
85
88
  rootView.getLocationOnScreen(location)
86
- maxOf(0, (location[1] + rootView.height) - rect.bottom)
89
+ val parentBottomPadding = (webViewContainer.parent as? View)?.paddingBottom ?: 0
90
+ maxOf(0, (location[1] + rootView.height) - rect.bottom - parentBottomPadding)
87
91
  } else {
88
92
  val visibleHeight = rect.height()
89
93
  screenHeight - visibleHeight
@@ -26,6 +26,8 @@ class ConfigMappingTest {
26
26
 
27
27
  while (keys.hasNext()) {
28
28
  val key = keys.next()
29
+ // The release override block is merged by the CLI, never shipped as-is.
30
+ if (prefix.isEmpty() && key == "release") continue
29
31
  val value = jsonObj.opt(key)
30
32
  val fullKey = if (prefix.isEmpty()) key else "$prefix.$key"
31
33
 
@@ -232,6 +234,29 @@ class ConfigMappingTest {
232
234
  assertEquals("3005", properties.getProperty("port"))
233
235
  }
234
236
 
237
+ @Test
238
+ fun `test top-level release override block is not flattened`() {
239
+ val json = JSONObject().apply {
240
+ put("LOCAL_IP", "192.168.1.100")
241
+ put("release", JSONObject().apply {
242
+ put("LOCAL_IP", "www.example.com")
243
+ put("allowPrivateHost", false)
244
+ })
245
+ put("android", JSONObject().apply {
246
+ put("release", "kept")
247
+ })
248
+ }
249
+
250
+ val properties = Properties()
251
+ extractProperties(json, properties)
252
+
253
+ assertEquals("192.168.1.100", properties.getProperty("LOCAL_IP"))
254
+ assertNull(properties.getProperty("release.LOCAL_IP"))
255
+ assertNull(properties.getProperty("release.allowPrivateHost"))
256
+ // Only the top-level key is reserved
257
+ assertEquals("kept", properties.getProperty("android.release"))
258
+ }
259
+
235
260
  // ============================================================
236
261
  // Test Category 2: Build Type Handling (4 tests)
237
262
  // ============================================================
@@ -4,154 +4,63 @@ var _fs = _interopRequireDefault(require("fs"));
4
4
  var _path = _interopRequireDefault(require("path"));
5
5
  var _child_process = require("child_process");
6
6
  var _utils = require("./utils.js");
7
+ var _androidToolchain = require("./androidToolchain.js");
8
+ var _localState = require("./localState.js");
7
9
  var _terminalProgress = _interopRequireDefault(require("./terminalProgress.js"));
8
- var _setupServer = require("./setupServer.js");
10
+ var _diagnostic = require("../cli/diagnostic.js");
9
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ // setupEmulator:android installs what an Android build needs (JDK, SDK
13
+ // packages, an emulator) and starts the emulator. It changes nothing in the
14
+ // app: config/config.json is read for sdkPath and emulatorName when they are
15
+ // set, never written. buildApp:android does the same checks itself, so this
16
+ // command is optional.
17
+
10
18
  const catalystCorePath = _path.default.dirname(require.resolve("catalyst-core/package.json"));
11
19
  const pwd = _path.default.join(catalystCorePath, "dist/native");
12
- const configPath = `${process.env.PWD}/config/config.json`;
20
+ const configPath = _path.default.join(process.cwd(), "config", "config.json");
21
+ const flags = (0, _utils.parseNativeArgs)(process.argv.slice(2));
13
22
 
14
23
  // Sentence case, verb first: these read as a sequence of actions, not as
15
24
  // headings.
16
25
  const steps = {
17
- java: "Check Java environment",
18
- config: "Initialize configuration",
26
+ java: "Check Java (JDK 17 to 23)",
27
+ sdk: "Set up the Android SDK",
19
28
  androidTools: "Validate Android tools",
20
- emulator: "Configure Android emulator",
21
- properties: "Update local.properties",
22
- saveConfig: "Save configuration",
23
- setupServer: "Set up server"
29
+ emulator: "Start the Android emulator",
30
+ properties: "Update local.properties"
24
31
  };
25
32
  const progress = new _terminalProgress.default(steps, "catalyst setupEmulator", {
26
33
  subject: "android"
27
34
  });
28
- async function checkJavaInstallation() {
29
- try {
30
- (0, _utils.runCommand)("java -version");
31
- (0, _utils.runCommand)("javac -version");
32
- return true;
33
- } catch (error) {
34
- progress.log("Java installation not found or not properly configured", "error");
35
- return false;
36
- }
37
- }
38
- async function installJava() {
39
- progress.log("Installing Java (Zulu JDK 17)...");
40
- try {
41
- try {
42
- (0, _utils.runCommand)("brew --version");
43
- } catch (error) {
44
- throw new Error("Homebrew is required but not installed. Please install Homebrew first.");
45
- }
46
- const javaHome = "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home";
47
-
48
- // Try installing Java
49
- try {
50
- await (0, _utils.runCommand)("brew install --cask zulu@17");
51
- } catch (error) {
52
- if (error.message.includes("already installed")) {
53
- progress.log("Attempting to reinstall Java...", "info");
54
- try {
55
- await (0, _utils.runCommand)("brew reinstall --cask zulu@17");
56
- } catch (reinstallError) {
57
- throw new Error(`Failed to reinstall Java: ${reinstallError.message}. Please run 'brew reinstall --cask zulu@17' manually.`);
58
- }
59
- } else {
60
- throw error;
61
- }
62
- }
63
-
64
- // Verify Java installation
65
- if (!_fs.default.existsSync(javaHome)) {
66
- throw new Error(`Java installation not found at ${javaHome}. Please verify the installation manually.`);
67
- }
68
-
69
- // Set up environment variables
70
- const homeDir = process.env.HOME;
71
- const shellProfile = `${homeDir}/.zshrc`;
72
- const exportCommand = `\n# Java Configuration\nexport JAVA_HOME=${javaHome}\nexport PATH=$JAVA_HOME/bin:$PATH\n`;
73
- try {
74
- _fs.default.appendFileSync(shellProfile, exportCommand);
75
- } catch (error) {
76
- throw new Error(`Failed to update ${shellProfile}: ${error.message}. Please add the following lines manually:\n${exportCommand}`);
77
- }
35
+ const log = (message, type = "info") => progress.log(message, type);
78
36
 
79
- // Set for current process
80
- process.env.JAVA_HOME = javaHome;
81
- process.env.PATH = `${javaHome}/bin:${process.env.PATH}`;
82
-
83
- // Verify Java configuration
84
- try {
85
- const javaVersion = (0, _utils.runCommand)("java -version");
86
- progress.log("Installed Java 17 (Zulu)", "info");
87
- progress.log(`Java Version: ${javaVersion}`, "info");
88
- progress.log(`IMPORTANT: Please run the following commands to complete setup:`, "warning");
89
- progress.log(`1. source ${shellProfile}`, "note");
90
- progress.log(`2. echo $JAVA_HOME`, "note");
91
- return true;
92
- } catch (error) {
93
- throw new Error(`Java installation verified but environment not configured. Please run:\n1. source ${shellProfile}\n2. echo $JAVA_HOME`);
94
- }
95
- } catch (error) {
96
- progress.log(error.message, "error");
97
- progress.log("Manual Setup Instructions:", "note");
98
- progress.log("1. Run: brew reinstall --cask zulu@17", "note");
99
- progress.log("2. Add to ~/.zshrc:", "note");
100
- progress.log(" export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home", "note");
101
- progress.log(" export PATH=$JAVA_HOME/bin:$PATH", "note");
102
- progress.log("3. Run: source ~/.zshrc", "note");
103
- throw error;
104
- }
105
- }
37
+ /**
38
+ * A usable JDK for this process and every tool it starts (Gradle, sdkmanager,
39
+ * avdmanager). JAVA_HOME is set here only; no shell profile is edited.
40
+ */
106
41
  async function validateJavaEnvironment() {
107
42
  progress.start("java");
108
- const hasJava = await checkJavaInstallation();
109
- if (!hasJava) {
110
- progress.log("Java installation required. Starting installation process...");
111
- const javaInstalled = await installJava();
112
- if (!javaInstalled) {
113
- progress.fail("java", "Failed to set up Java environment");
114
- throw new Error("Failed to set up Java environment");
115
- }
116
- }
43
+ const java = await (0, _androidToolchain.ensureJava)({
44
+ flags,
45
+ log
46
+ });
47
+ (0, _androidToolchain.applyJavaHome)(java);
48
+ progress.log(`Java ${java.version} from ${java.source} (${java.home})`, "info");
117
49
  progress.complete("java");
118
50
  }
119
- async function initializeConfig() {
120
- const configFile = _fs.default.readFileSync(configPath, "utf8");
121
- const config = JSON.parse(configFile);
122
- const {
123
- WEBVIEW_CONFIG
124
- } = config;
125
- if (!WEBVIEW_CONFIG || Object.keys(WEBVIEW_CONFIG).length === 0) {
126
- progress.log("WebView Config missing in " + configPath, "error");
127
- process.exit(1);
128
- }
129
- if (!WEBVIEW_CONFIG.android) {
130
- WEBVIEW_CONFIG.android = {};
131
- }
132
- return {
133
- WEBVIEW_CONFIG
134
- };
135
- }
136
- async function saveConfig(newConfig) {
51
+
52
+ /** WEBVIEW_CONFIG.android from config.json, or {} when there is none. Read only. */
53
+ function readAndroidConfig() {
137
54
  try {
138
- const existingConfigFile = _fs.default.readFileSync(configPath, "utf8");
139
- const existingConfig = JSON.parse(existingConfigFile);
140
- const updatedConfig = {
141
- ...existingConfig,
142
- WEBVIEW_CONFIG: {
143
- ...existingConfig.WEBVIEW_CONFIG,
144
- android: newConfig.WEBVIEW_CONFIG.android
145
- }
146
- };
147
- _fs.default.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2));
148
- } catch (error) {
149
- progress.log("Failed to save configuration: " + error.message, "error");
150
- process.exit(1);
55
+ const config = JSON.parse(_fs.default.readFileSync(configPath, "utf8"));
56
+ const android = config?.WEBVIEW_CONFIG?.android;
57
+ return android && typeof android === "object" ? android : {};
58
+ } catch {
59
+ return {};
151
60
  }
152
61
  }
153
62
  function validateAndroidTools(androidConfig) {
154
- const ANDROID_SDK = (0, _utils.expandHome)(androidConfig.sdkPath);
63
+ const ANDROID_SDK = androidConfig.sdkPath;
155
64
  const EMULATOR_PATH = `${ANDROID_SDK}/emulator/emulator`;
156
65
  const ADB_PATH = `${ANDROID_SDK}/platform-tools/adb`;
157
66
  if (!ANDROID_SDK) {
@@ -166,7 +75,7 @@ function validateAndroidTools(androidConfig) {
166
75
  throw new Error(`ADB not found at: ${ADB_PATH}`);
167
76
  }
168
77
  try {
169
- (0, _utils.runCommand)(`${ADB_PATH} version`);
78
+ (0, _utils.runFile)(ADB_PATH, ["version"]);
170
79
  } catch (error) {
171
80
  throw new Error(`ADB is not working properly: ${error.message}`);
172
81
  }
@@ -174,12 +83,12 @@ function validateAndroidTools(androidConfig) {
174
83
  throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);
175
84
  }
176
85
  try {
177
- (0, _utils.runCommand)(`${EMULATOR_PATH} -version`);
86
+ (0, _utils.runFile)(EMULATOR_PATH, ["-version"]);
178
87
  } catch (error) {
179
88
  throw new Error(`Emulator is not working properly: ${error.message}`);
180
89
  }
181
90
  try {
182
- const avdOutput = (0, _utils.runCommand)(`${EMULATOR_PATH} -list-avds`);
91
+ const avdOutput = (0, _utils.runFile)(EMULATOR_PATH, ["-list-avds"]);
183
92
  if (!avdOutput.includes(androidConfig.emulatorName)) {
184
93
  throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);
185
94
  }
@@ -194,7 +103,7 @@ function validateAndroidTools(androidConfig) {
194
103
  }
195
104
  async function checkEmulator(ADB_PATH) {
196
105
  try {
197
- const devices = (0, _utils.runCommand)(`${ADB_PATH} devices`, {
106
+ const devices = (0, _utils.runFile)(ADB_PATH, ["devices"], {
198
107
  timeout: 15000
199
108
  });
200
109
  return devices.includes("emulator");
@@ -215,75 +124,72 @@ async function startEmulator(EMULATOR_PATH, androidConfig) {
215
124
  });
216
125
  child.unref();
217
126
  }
218
- async function updateLocalProperties(sdkPath) {
127
+ function updateLocalProperties(sdkPath) {
219
128
  progress.start("properties");
220
- try {
221
- (0, _utils.runCommand)(`cd ${pwd}/androidProject && ./gradlew updateSdkPath -PsdkPath="${sdkPath}"`);
222
- progress.complete("properties");
223
- } catch (error) {
224
- progress.fail("properties", `Failed to update local.properties: ${error.message}`);
225
- throw error;
226
- }
129
+ (0, _utils.ensureLocalProperties)(`${pwd}/androidProject`, sdkPath);
130
+ progress.complete("properties");
227
131
  }
228
132
  async function setupAndroidEnvironment() {
229
133
  try {
230
134
  await validateJavaEnvironment();
231
- progress.start("config");
232
- await initializeConfig();
233
- const config = await (0, _utils.validateAndCompleteConfig)("android", configPath);
234
- progress.complete("config");
135
+ progress.start("sdk");
136
+ const fileAndroid = readAndroidConfig();
137
+ const sdkPath = (0, _androidToolchain.resolveSdkPath)(fileAndroid);
138
+ const last = (0, _localState.getLastTarget)("android");
139
+ const sdk = await (0, _androidToolchain.ensureAndroidSdk)({
140
+ sdkPath,
141
+ emulatorName: fileAndroid.emulatorName,
142
+ preferredAvd: last?.target === "emulator" ? last.avd : undefined,
143
+ flags,
144
+ log
145
+ });
146
+ if (sdk.installed.length) progress.log(`Installed ${sdk.installed.join(", ")}`, "success");
147
+ if (sdk.createdAvd) progress.log(`Created the "${sdk.emulatorName}" emulator`, "success");
148
+ progress.complete("sdk");
149
+ const androidConfig = {
150
+ ...fileAndroid,
151
+ sdkPath,
152
+ emulatorName: sdk.emulatorName
153
+ };
235
154
  progress.start("androidTools");
236
155
  const {
237
156
  EMULATOR_PATH,
238
157
  ADB_PATH
239
- } = validateAndroidTools(config.android);
158
+ } = validateAndroidTools(androidConfig);
240
159
  progress.complete("androidTools");
241
160
  progress.start("emulator");
242
161
  const emulatorRunning = await checkEmulator(ADB_PATH);
243
162
  if (!emulatorRunning) {
244
163
  progress.log("No emulator running, attempting to start one...");
245
- await startEmulator(EMULATOR_PATH, config.android);
164
+ await startEmulator(EMULATOR_PATH, androidConfig);
246
165
  } else {
247
166
  progress.log("Emulator already running", "info");
248
167
  }
249
168
  progress.complete("emulator");
250
- if (config.android?.sdkPath) {
251
- await updateLocalProperties((0, _utils.expandHome)(config.android.sdkPath));
252
- }
253
- progress.start("saveConfig");
254
- await saveConfig({
255
- WEBVIEW_CONFIG: config
256
- });
257
- progress.complete("saveConfig");
258
- progress.start("setupServer");
259
- await (0, _setupServer.setupServer)(configPath, message => progress.log(message, "info"));
260
- progress.complete("setupServer");
169
+ updateLocalProperties(sdkPath);
261
170
 
262
- // What was configured, not what the keys mean -- the explanation
171
+ // What this machine uses, not what the keys mean: the explanation
263
172
  // belongs in the docs, not in every successful run's transcript.
264
- // validateAndCompleteConfig returns { port, android: {...} } -- flat,
265
- // not nested under WEBVIEW_CONFIG.
266
- const androidCfg = config.android || {};
267
- progress.printTreeContent("Config config/config.json", [{
268
- text: `port ${config.port ?? ""}`,
269
- color: "gray"
270
- }, {
271
- text: `buildType ${androidCfg.buildType ?? ""}`,
272
- color: "gray"
273
- }, {
274
- text: `sdkPath ${androidCfg.sdkPath ?? ""}`,
173
+ progress.printTreeContent("Android", [{
174
+ text: `sdk ${sdkPath}`,
275
175
  color: "gray"
276
176
  }, {
277
- text: `emulatorName ${androidCfg.emulatorName ?? ""}`,
177
+ text: `emulator ${sdk.emulatorName}`,
278
178
  color: "gray"
279
179
  }]);
280
180
  progress.summary("Ready", "Run catalyst buildApp:android to install the app");
281
181
  process.exit(0);
282
182
  } catch (error) {
183
+ // A coded error (ANDROID-007, NATIVE-005, ...) gets the full diagnostic:
184
+ // code, hint and doc link. Anything else keeps the one-line failure.
185
+ const coded = /^[A-Z]+-\d{3}$/.test(String(error?.code || ""));
283
186
  if (progress.currentStep) {
284
- progress.fail(progress.currentStep.id, error.message);
187
+ progress.fail(progress.currentStep.id, coded ? undefined : error.message);
285
188
  }
286
- progress.hint(error.hint);
189
+ if (coded) process.stderr.write((0, _diagnostic.buildFailure)({
190
+ error,
191
+ stage: "Android setup"
192
+ }));else progress.hint(error.hint);
287
193
  process.exit(1);
288
194
  }
289
195
  }