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
@@ -154,7 +154,7 @@
154
154
  {
155
155
  "section": "build_system",
156
156
  "title": "WEBVIEW_CONFIG Location and Schema",
157
- "content": "Location: config/config.json. Schema: { port, android: { buildType, sdkPath, emulatorName, cachePattern }, ios: { buildType, appBundleId, simulatorName, cachePattern } }. cachePattern is comma-separated globs: *.css,*.js,*.png,*.svg,*.jpg",
157
+ "content": "Location: config/config.json. Schema (catalyst-core 1.3.0+): { appId, appName, url, allowPrivateHost, LOCAL_IP: 'localhost', port, android: { emulatorName, sdkPath, cachePattern, buildOptimisation }, ios: { developmentTeam, simulatorName, deviceUDID, exportMethod, cachePattern } }. appId is the Android application id and iOS bundle id and wins over android.packageName / ios.appBundleId; appName wins over the per-platform appName. url is the https origin a release build loads. Every android/ios key is optional; buildType 'release' is deprecated in favour of --release. cachePattern is comma-separated globs: *.css,*.js,*.png,*.svg,*.jpg",
158
158
  "layer": "Config",
159
159
  "source": "static",
160
160
  "tags": ["config", "build"]
@@ -170,7 +170,7 @@
170
170
  {
171
171
  "section": "build_system",
172
172
  "title": "iOS Build Pipeline",
173
- "content": "npm run buildApp:ios generates ConfigConstants.swift and plugin metadata from WEBVIEW_CONFIG, syncs selected public/ios assets into the package-owned Xcode project, detects a physical device or launches a simulator, cleans Xcode artifacts, compiles with the Debug configuration, and installs and launches the .app. It does not bundle the full Catalyst web client and does not create an App Store archive. The generated project is located under the resolved catalyst-core package at dist/native/iosnativeWebView/iosnativeWebView.xcodeproj.",
173
+ "content": "npm run ios (catalyst buildApp:ios) checks full Xcode and offers the licence and first-launch steps, installs a simulator runtime, uses an existing iPhone simulator (ios.simulatorName, else the last pick, a booted one or the newest; 'Catalyst iPhone' is created after asking only when there is none), picks the target (simulator by default; a terminal run asks when an iPhone is connected; --device uses the iPhone; the pick is remembered in .catalyst/local.json), starts catalyst start when nothing answers and waits for it, generates ConfigConstants.swift and plugin metadata from WEBVIEW_CONFIG, syncs public/ios assets into the package-owned Xcode project, compiles with the Debug configuration, and installs and launches the .app. The simulator loads the dev server on localhost; an iPhone on the Mac's <LocalHostName>.local (else the LAN IP), with NSAllowsLocalNetworking in debug, and needs ios.developmentTeam plus an Apple account signed in to Xcode once (a free account works). config.json is never written. With --release (or the deprecated ios.buildType release) it archives and exports the signed store IPA instead. The generated project is located under the resolved catalyst-core package at dist/native/iosnativeWebView/iosnativeWebView.xcodeproj.",
174
174
  "layer": "Build",
175
175
  "source": "static",
176
176
  "tags": ["ios", "build", "buildApp:ios", "debug", "xcode"],
@@ -181,17 +181,19 @@
181
181
  },
182
182
  {
183
183
  "section": "build_system",
184
- "title": "Catalyst iOS Production Release Flow (0.2.x and 0.3.x)",
185
- "content": "Catalyst 0.2.x and 0.3.x use the same iOS production-release flow: (1) deploy the production web application and verify its HTTPS URL, (2) finalize WEBVIEW_CONFIG, bundle ID, app version/build, plugins, permissions, capabilities, icons, splash assets, access-control rules, and privacy declarations, (3) run npm run buildApp:ios from the consuming app root to prepare the package-owned native project and perform its Debug build, (4) open the resolved catalyst-core package's dist/native/iosnativeWebView/iosnativeWebView.xcodeproj, (5) confirm the iosnativeWebView scheme's Archive action uses Release, configure Release signing and capabilities, choose a generic iOS device destination, and run Product -> Archive, (6) Validate App and upload with Distribute App -> TestFlight & App Store, then (7) finish TestFlight testing, App Store Connect metadata, build selection, and App Review submission. buildApp:ios does not create the final App Store artifact.",
184
+ "title": "Catalyst Native Release Flow (1.3.0+)",
185
+ "content": "From catalyst-core 1.3.0 the store builds need no Xcode or Android Studio UI. (1) Deploy the web app over HTTPS and set WEBVIEW_CONFIG.url (or pass --url); localhost, private, .local and example hosts fail with NATIVE-001 unless allowPrivateHost is true. (2) Set WEBVIEW_CONFIG.appId to your own reverse-DNS id (com.example.* and template ids fail with NATIVE-003) and the version in package.json. The build number is the minutes since 2024-01-01 UTC, or CATALYST_BUILD_NUMBER (NATIVE-002 if invalid). (3) Android: npm run android:release. The first run in a terminal creates the upload key in credentials/android/ (never without a terminal, ANDROID-005), refuses if the app is already on Google Play, and writes build-output/android/<version>-<build>/<name>.aab plus upload_certificate.pem. (4) iOS: put signing files in credentials/ios/: AuthKey_<KEYID>.p8 (Admin-role App Store Connect API key) plus signing.properties with ascIssuerId for cloud signing (needs ios.developmentTeam), or one .p12 and .mobileprovision plus p12Password for local signing. npm run ios:release archives (Release, generic/platform=iOS), exports with ios.exportMethod (default app-store-connect) and writes build-output/ios/<version>-<build>/<name>.ipa and <name>-dSYMs.zip, with ITSAppUsesNonExemptEncryption=false. (5) Back up and CI: catalyst credentials:export android|ios prints the folder as one value for the CATALYST_ANDROID_CREDENTIALS / CATALYST_IOS_CREDENTIALS secret (48 KB limit). (6) Upload is manual: Play Console internal testing; Apple's Transporter app for TestFlight. A second release with the same version and build number (two in one minute) fails with NATIVE-011; an app id that is not a valid Android package name or iOS bundle id fails with NATIVE-010. A shipped WEBVIEW_CONFIG.ios.version object still sets the iOS version and build number (deprecated; CATALYST_BUILD_NUMBER wins). Errors: IOS-010 signing not set up, IOS-011 API key incomplete, IOS-012 p12/profile inputs, IOS-013 export method. There is no --build-number, no --no-sign and no WEBVIEW_CONFIG.release block.",
186
186
  "layer": "Build",
187
187
  "source": "static",
188
188
  "tags": [
189
189
  "ios",
190
+ "android",
190
191
  "release",
191
192
  "production",
192
- "0.2.x",
193
- "0.3.x",
193
+ "1.3.0",
194
194
  "buildApp:ios",
195
+ "buildApp:android",
196
+ "credentials",
195
197
  "xcode",
196
198
  "archive",
197
199
  "app-store",
@@ -205,7 +207,7 @@
205
207
  {
206
208
  "section": "build_system",
207
209
  "title": "Generated iOS Project Signing and Persistence",
208
- "content": "The generated Xcode project is package-owned state. Its shared xcconfig is regenerated with development-oriented code-signing defaults, and the bundled push entitlement can use aps-environment=development. Before an App Store archive, review the Release team, distribution signing, provisioning, capabilities, and entitlements in Xcode. Run the final npm run buildApp:ios before release-only Xcode changes: later Catalyst generation, package upgrades, or dependency installation can replace or regenerate native config constants, shared Xcode configuration, plist and entitlement content, generated plugin code/resources, selected project-file entries, and assets. Do not treat manual edits inside the generated project as durable app configuration.",
210
+ "content": "The generated Xcode project is package-owned state, regenerated by every build and package install, so keep release settings in config/config.json and signing files in credentials/ios/. Shared.xcconfig is written per build: CODE_SIGN_STYLE Automatic for ios.developmentTeam (cloud signing with the App Store Connect API key from credentials/ios/, else the Apple account in Xcode), or Manual with the profile when credentials/ios/ holds a .p12 and .mobileprovision (imported into a temporary keychain and removed after the build) or ios.provisioningProfile is set. The project carries no team or profile of its own. The aps-environment entitlement is only written when notifications.enabled is true (development for automatic signing, which export turns into production; production for a manually signed release). The app bundles PrivacyInfo.xcprivacy and a localhost.p12 generated per build for the in-app HTTPS server.",
209
211
  "layer": "Build",
210
212
  "source": "static",
211
213
  "tags": [
@@ -226,7 +228,7 @@
226
228
  {
227
229
  "section": "build_system",
228
230
  "title": "Legacy 0.2.x Build NPM Scripts",
229
- "content": "All catalyst npm scripts to add to package.json scripts block (each maps to: catalyst <scriptName>): devBuild \u2014 dev web build. devServe \u2014 dev server. buildApp \u2014 builds native apps. buildApp:android \u2014 Android native app build. buildApp:ios \u2014 iOS native project preparation and Debug build. buildApp:android:release \u2014 Android release build. setupEmulator \u2014 sets up both emulators. setupEmulator:android \u2014 sets up Android AVD. setupEmulator:ios \u2014 sets up iOS simulator. There is no separate buildApp:ios:release flow: run buildApp:ios, then create the production archive from the generated Xcode project's Release Archive action. NOTE: build:android / build:android:release / build:ios do NOT exist in catalyst \u2014 common mistake. Missing any script = that npm run command fails.",
231
+ "content": "All catalyst npm scripts to add to package.json scripts block (each maps to: catalyst <scriptName>): devBuild \u2014 dev web build. devServe \u2014 dev server. buildApp \u2014 builds native apps. buildApp:android \u2014 Android native app build. buildApp:ios \u2014 iOS Debug build and run (add -- --release --build-number <n> for the signed store IPA). buildApp:android:release \u2014 Android release build. setupEmulator \u2014 sets up both emulators. setupEmulator:android \u2014 sets up Android AVD. setupEmulator:ios \u2014 sets up iOS simulator. There is no separate buildApp:ios:release script: pass --release to buildApp:ios. NOTE: build:android / build:android:release / build:ios do NOT exist in catalyst \u2014 common mistake. Missing any script = that npm run command fails.",
230
232
  "layer": "Build",
231
233
  "source": "static",
232
234
  "tags": ["build", "scripts", "package.json", "native"]
@@ -466,7 +468,7 @@
466
468
  {
467
469
  "section": "webview_config",
468
470
  "title": "Real WEBVIEW_CONFIG Example",
469
- "content": "Full WEBVIEW_CONFIG example (as of canary.19+): { port: '3005', LOCAL_IP: '192.168.0.11', useHttps: false, appInfo: 'android-5Feb2026-v2.1.0', cachePattern: '*.css', accessControl: { enabled: false, allowedUrls: ['*.yourdomain.com*', 'http://localhost:*'] }, android: { appName: 'MyApp', packageName: 'com.example.app', buildType: 'debug', sdkPath: '/Users/user/Android/', emulatorName: 'Pixel_5_API_30', cachePattern: '*.css,*.js,*.png', security: { mode: 'custom', allowBackup: true } }, ios: { appBundleId: 'com.example.app', appName: 'MyApp', buildType: 'debug', simulatorName: 'iPhone 17 Pro', cachePattern: '*.css,*.js', accessControl: { enabled: true, allowedUrls: ['*.yourdomain.com*', 'http://localhost:*'] } }, splashScreen: { backgroundColor: '#ffffff', duration: 2000, imageWidth: 400, imageHeight: 200, cornerRadius: 20 }, notifications: { enabled: true, color: \"#007AFF\" } }. Note: splashScreen is INSIDE WEBVIEW_CONFIG, not at top-level config.json. Note canary.19 change: iOS has its OWN accessControl block separate from top-level accessControl.",
471
+ "content": "Full WEBVIEW_CONFIG example (catalyst-core 1.3.0+): { appId: 'com.yourcompany.app', appName: 'MyApp', url: 'https://www.yourdomain.com', LOCAL_IP: 'localhost', port: '3005', appInfo: 'android-5Feb2026-v2.1.0', cachePattern: '*.css', accessControl: { enabled: false, allowedUrls: ['*.yourdomain.com*', 'http://localhost:*'] }, android: { cachePattern: '*.css,*.js,*.png', security: { mode: 'custom', allowBackup: true } }, ios: { developmentTeam: 'ABCDE12345', cachePattern: '*.css,*.js', accessControl: { enabled: true, allowedUrls: ['*.yourdomain.com*', 'http://localhost:*'] } }, splashScreen: { backgroundColor: '#ffffff', duration: 2000, imageWidth: 400, imageHeight: 200, cornerRadius: 20 }, notifications: { enabled: true, color: \"#007AFF\" } }. Note: splashScreen is INSIDE WEBVIEW_CONFIG, not at top-level config.json. Note canary.19 change: iOS has its OWN accessControl block separate from top-level accessControl. LOCAL_IP stays localhost: debug builds reach the dev server through adb reverse (Android) or the Mac's .local name (iPhone).",
470
472
  "layer": "Config",
471
473
  "source": "extracted:config/config.json",
472
474
  "tags": ["config", "example"]
@@ -482,7 +484,7 @@
482
484
  {
483
485
  "section": "config_structure",
484
486
  "title": "WEBVIEW_CONFIG Required Fields",
485
- "content": "WEBVIEW_CONFIG field reference (from buildAppAndroid.js + buildAppIos.js source):\n\nTOP-LEVEL (both platforms):\n- port (string, required) \u2014 web server port e.g. \"3005\"\n- LOCAL_IP (string, required) \u2014 machine LAN IP e.g. \"192.168.0.11\". NOT localhost \u2014 emulator cannot resolve it\n- appInfo (string, required) \u2014 build identifier e.g. \"android-5Feb2026-v2.1.0\". Missing = iOS build breaks\n- useHttps (boolean, optional) \u2014 default false\n- cachePattern (string, optional) \u2014 comma-separated globs e.g. \"*.css\"\n- accessControl.enabled (boolean, optional) \u2014 default false\n- accessControl.allowedUrls (array, required if enabled) \u2014 URL patterns\n\nANDROID (WEBVIEW_CONFIG.android):\n- sdkPath (string, REQUIRED) \u2014 abs path to Android SDK. Build exits immediately without it\n- emulatorName (string, REQUIRED for debug) \u2014 AVD name. Run: emulator -list-avds\n- appName (string, optional) \u2014 default \"Catalyst Application\"\n- packageName (string, optional) \u2014 e.g. com.company.app. Derived from appName if missing\n- buildType (string, optional) \u2014 \"debug\" or \"release\". Default \"debug\"\n- keystore or keystoreConfig (required for release buildType only)\n- buildOptimisation (boolean, optional) \u2014 default false\n- cachePattern (string, optional)\n\nIOS (WEBVIEW_CONFIG.ios):\n- appBundleId (string, optional) \u2014 default \"com.debug.webview\". Must set for distribution\n- appName (string, optional) \u2014 default \"Catalyst Application\"\n- simulatorName (string, optional) \u2014 auto-detected if missing. e.g. \"iPhone 17 Pro\"\n- buildType (string, optional) \u2014 may label copied Debug output but does not select the Xcode build configuration or create an App Store archive\n- deviceUDID (string, optional) \u2014 physical device builds only\n- developmentTeam (string, required if deviceUDID set) \u2014 Apple Developer Team ID\n- cachePattern (string, optional)\n\nFor iOS production, run buildApp:ios to prepare the generated project, then archive the iosnativeWebView scheme using its Release Archive action in Xcode.",
487
+ "content": "WEBVIEW_CONFIG field reference (catalyst-core 1.3.0+, from nativeConfig.js):\n\nTOP-LEVEL (both platforms):\n- appId (string, required for release): Android application id and iOS bundle id, e.g. com.company.app. Wins over android.packageName / ios.appBundleId / ios.bundleIdentifier. Release refuses com.example.* (NATIVE-003)\n- appName (string, optional): name under the icon; wins over android.appName / ios.appName\n- url (string, required for release): https origin the release app loads, no path. --url overrides. Debug builds ignore it\n- allowPrivateHost (boolean, optional): lets a release load a private host\n- LOCAL_IP (string, optional): keep 'localhost'. Debug builds work out the dev server address; apps without url still use LOCAL_IP/port/useHttps as the release host\n- port (string, optional): dev server port when NODE_SERVER_PORT is not set\n- appInfo (string, required): build identifier e.g. \"android-5Feb2026-v2.1.0\". Missing = iOS build breaks\n- cachePattern (string, optional): comma-separated globs e.g. \"*.css\"\n- accessControl.enabled / allowedUrls: URL allowlist; debug builds add the dev server origin\n\nANDROID (WEBVIEW_CONFIG.android, all optional):\n- sdkPath: else ANDROID_HOME, ANDROID_SDK_ROOT, default location; installed when missing\n- emulatorName: else the last used or catalyst-api36, created when missing\n- buildType: deprecated; 'release' builds a release with a warning\n- outputPath: release folder instead of build-output/android\n- buildOptimisation, cachePattern\n\nIOS (WEBVIEW_CONFIG.ios, all optional):\n- developmentTeam: Apple team ID, needed for an iPhone and for cloud or Xcode-account signing (IOS-010)\n- simulatorName: simulator to use when it exists; else the last pick, a booted iPhone simulator or the newest one. 'Catalyst iPhone' is created only when there is no iPhone simulator\n- deviceUDID: which iPhone --device uses without a terminal\n- provisioningProfile: installed profile name for manual signing\n- exportMethod: app-store-connect (default), release-testing, enterprise or debugging\n- buildType: deprecated; 'release' builds the store IPA with a warning\n\nVersion comes from package.json; signing files live in credentials/android/ and credentials/ios/, never in config.json.",
486
488
  "layer": "Config",
487
489
  "source": "static",
488
490
  "tags": ["config", "WEBVIEW_CONFIG", "android", "ios", "required", "fields"]
@@ -1281,7 +1283,7 @@
1281
1283
  {
1282
1284
  "section": "build_system",
1283
1285
  "title": "Catalyst 0.3.x CLI Commands",
1284
- "content": "Current web commands are catalyst start, catalyst build, catalyst serve, and catalyst serve:inspect. devBuild and devServe are legacy 0.2.x commands and are not accepted by the 0.3.x CLI. Native commands remain buildApp, buildApp:ios, buildApp:android, setupEmulator, setupEmulator:ios, and setupEmulator:android. catalyst-core 1.0.2 OUTPUT: every command prints a short header, one line per step with timing, and one diagnostic block on failure. Flags: --verbose shows every step and, on failure, the full tool output (bundler, Gradle); --debug also prints stacks and the environment. Child processes inherit the mode through CATALYST_OUTPUT_MODE=verbose|debug. Failures carry registry codes from the error catalog (for example BUNDLE-000, ANDROID-000, IOS-000, CCA-001); use the explain_error tool for any code. catalyst serve (1.0.2+) exits when there is no build in BUILD_OUTPUT_PATH and refuses a stale build with PREFLIGHT-022 when PUBLIC_STATIC_ASSET_URL, PUBLIC_STATIC_ASSET_PATH or a CLIENT_ENV_VARIABLES value in config/config.json changed after catalyst build (the build records them in <BUILD_OUTPUT_PATH>/.catalyst-build.json; a build from an older version has no record, so serve warns and skips the check until you rebuild). Fix: run catalyst build again, then catalyst serve. catalyst serve passes the app's own stdout/stderr through unchanged, so JSON log shippers keep working. Scripts that parsed the old CLI text output must be updated.",
1286
+ "content": "Current web commands are catalyst start, catalyst build, catalyst serve, and catalyst serve:inspect. devBuild and devServe are legacy 0.2.x commands and are not accepted by the 0.3.x CLI. Native commands are buildApp:android and buildApp:ios (template scripts android, ios, android:release, ios:release; flags --release, --device, --url, --yes, --accept-licenses; nothing is prompted without a TTY or with CI set, NATIVE-005 names the missing flag), credentials:export <android|ios>, and the install-only setupEmulator:android and setupEmulator:ios. catalyst-core 1.0.2 OUTPUT: every command prints a short header, one line per step with timing, and one diagnostic block on failure. Flags: --verbose shows every step and, on failure, the full tool output (bundler, Gradle); --debug also prints stacks and the environment. Child processes inherit the mode through CATALYST_OUTPUT_MODE=verbose|debug. Failures carry registry codes from the error catalog (for example BUNDLE-000, ANDROID-000, IOS-000, CCA-001); use the explain_error tool for any code. catalyst serve (1.0.2+) exits when there is no build in BUILD_OUTPUT_PATH and refuses a stale build with PREFLIGHT-022 when PUBLIC_STATIC_ASSET_URL, PUBLIC_STATIC_ASSET_PATH or a CLIENT_ENV_VARIABLES value in config/config.json changed after catalyst build (the build records them in <BUILD_OUTPUT_PATH>/.catalyst-build.json; a build from an older version has no record, so serve warns and skips the check until you rebuild). Fix: run catalyst build again, then catalyst serve. catalyst serve passes the app's own stdout/stderr through unchanged, so JSON log shippers keep working. Scripts that parsed the old CLI text output must be updated.",
1285
1287
  "layer": "Build",
1286
1288
  "source": "static",
1287
1289
  "tags": [
@@ -109,3 +109,43 @@ describe("check_config: splash screen", () => {
109
109
  expect(findings(r).some((f) => f.field === "WEBVIEW_CONFIG.splashScreen.minDuration")).toBe(false)
110
110
  })
111
111
  })
112
+
113
+ describe("check_config: native port and cache patterns", () => {
114
+ const fieldsOf = (list: Array<{ field: string }>) => list.map((f) => f.field)
115
+
116
+ it("accepts WEBVIEW_CONFIG.port as the template's string or a number", () => {
117
+ for (const port of ["3005", 3005]) {
118
+ const r = handle_check_config({
119
+ project_path: makeSplashProject({ NODE_SERVER_PORT: 3005, WEBVIEW_CONFIG: { port } }),
120
+ })
121
+ expect(fieldsOf(r.issues)).not.toContain("WEBVIEW_CONFIG.port")
122
+ expect(fieldsOf(r.warnings)).not.toContain("WEBVIEW_CONFIG.port")
123
+ expect(fieldsOf(r.passed)).toContain("WEBVIEW_CONFIG.port")
124
+ }
125
+ })
126
+
127
+ it("still rejects a port that is not a port", () => {
128
+ for (const port of ["abc", 0, 70000, true]) {
129
+ const r = handle_check_config({ project_path: makeSplashProject({ WEBVIEW_CONFIG: { port } }) })
130
+ expect(fieldsOf(r.issues)).toContain("WEBVIEW_CONFIG.port")
131
+ }
132
+ })
133
+
134
+ it("reads ios.cachePattern as an array, and warns about a comma-separated string", () => {
135
+ const ok = handle_check_config({
136
+ project_path: makeSplashProject({ WEBVIEW_CONFIG: { ios: { cachePattern: ["*.css", "*.js"] } } }),
137
+ })
138
+ expect(fieldsOf(ok.passed)).toContain("WEBVIEW_CONFIG.ios.cachePattern")
139
+ expect(fieldsOf(ok.warnings)).not.toContain("WEBVIEW_CONFIG.ios.cachePattern")
140
+
141
+ const commas = handle_check_config({
142
+ project_path: makeSplashProject({ WEBVIEW_CONFIG: { ios: { cachePattern: "*.css,*.js" } } }),
143
+ })
144
+ expect(fieldsOf(commas.warnings)).toContain("WEBVIEW_CONFIG.ios.cachePattern")
145
+
146
+ const wrong = handle_check_config({
147
+ project_path: makeSplashProject({ WEBVIEW_CONFIG: { ios: { cachePattern: 5 } } }),
148
+ })
149
+ expect(fieldsOf(wrong.warnings)).toContain("WEBVIEW_CONFIG.ios.cachePattern")
150
+ })
151
+ })
@@ -29,7 +29,7 @@ const MASTER_FLOWS = {
29
29
  step: 2,
30
30
  cmd: null,
31
31
  label: "Browser / WebView loads",
32
- detail: "Web: browser hits localhost. Universal: WebView loads http://<LOCAL_IP>:<port>. IP auto-detected by catalyst at startup.",
32
+ detail: "Web: browser hits localhost. Universal debug builds load the same dev server: Android over adb reverse on localhost, the iOS simulator on localhost, an iPhone on <mac>.local. Nothing is written to config.json.",
33
33
  },
34
34
  ],
35
35
 
@@ -87,27 +87,27 @@ const MASTER_FLOWS = {
87
87
  android_debug: [
88
88
  {
89
89
  step: 1,
90
- cmd: null,
91
- label: "Pre-check: config",
92
- detail: 'Reads config/config.json → WEBVIEW_CONFIG.android. Requires: sdkPath, emulatorName, buildType="debug" (or "Debug").',
90
+ cmd: "npm run android",
91
+ label: "Android debug build",
92
+ detail: "catalyst buildApp:android. Checks JDK 17 to 23 and the Android SDK (sdkPath, ANDROID_HOME, ANDROID_SDK_ROOT, default location) and installs what is missing after asking (--yes / --accept-licenses without a terminal). Since 1.0.2 Gradle runs behind one progress line; on failure --verbose or --debug add the ANDROID-000 report.",
93
93
  },
94
94
  {
95
95
  step: 2,
96
- cmd: "catalyst build",
97
- label: "Build web assets",
98
- detail: "Compiles web app to build/. Android build embeds these as the WebView content.",
96
+ cmd: null,
97
+ label: "Target",
98
+ detail: "The emulator (android.emulatorName, else the last one used, else catalyst-api36, created when missing). With a phone connected, a terminal run asks; --device uses the phone (ANDROID_SERIAL picks one without a terminal). The pick is remembered in .catalyst/local.json.",
99
99
  },
100
100
  {
101
101
  step: 3,
102
- cmd: "npm run buildApp:android",
103
- label: "Android debug build",
104
- detail: "Runs buildAppAndroid.js: validates ADB + emulator path from sdkPath, launches emulator (emulatorName), copies web assets to androidProject/, runs Gradle debug build, installs APK. Since 1.0.2 Gradle runs behind one progress line that shows the current task; on failure --verbose or --debug add the ANDROID-000 report with the full Gradle output.",
102
+ cmd: null,
103
+ label: "Dev server",
104
+ detail: "Starts `catalyst start` when nothing answers on NODE_SERVER_PORT and waits until it answers; a running server is reused. No separate npm start needed.",
105
105
  },
106
106
  {
107
107
  step: 4,
108
108
  cmd: null,
109
- label: "WebView startup",
110
- detail: "App launches → WebView loads http://<LOCAL_IP>:<port> → bridge initialises → hooks available.",
109
+ label: "Install, reverse, launch",
110
+ detail: "Installs <appId>.debug, runs adb reverse for NODE_SERVER_PORT, the HMR port and every localhost:<port> in config.json, then launches. The app loads http://localhost:<port>; WEBVIEW_CONFIG.url is ignored in debug.",
111
111
  },
112
112
  ],
113
113
 
@@ -116,52 +116,46 @@ const MASTER_FLOWS = {
116
116
  step: 1,
117
117
  cmd: null,
118
118
  label: "Pre-check: config",
119
- detail: 'Requires WEBVIEW_CONFIG.android.buildType="release". keystoreConfig must be present with real passwords (not placeholder values).',
119
+ detail: "WEBVIEW_CONFIG.url (or --url) is the production origin; a localhost/LAN/.local/example host fails with NATIVE-001 unless allowPrivateHost is true. appId must not be com.example.* or a template id (NATIVE-003). Version from package.json; build number = minutes since 2024-01-01 UTC, or CATALYST_BUILD_NUMBER.",
120
120
  },
121
121
  {
122
122
  step: 2,
123
123
  cmd: null,
124
- label: "Pre-check: keystore fields",
125
- detail: "Required keystoreConfig fields: keyAlias, storePassword, keyPassword, organizationInfo (companyName, city, state, countryCode).",
124
+ label: "Pre-check: upload key",
125
+ detail: "credentials/android/upload.jks + keystore.properties. The first release in a terminal asks to create it (never without a terminal, even with --yes: ANDROID-005) and refuses if the app is already on Google Play. Back it up with `catalyst credentials:export android`; CI reads the same value from CATALYST_ANDROID_CREDENTIALS.",
126
126
  },
127
127
  {
128
128
  step: 3,
129
- cmd: "catalyst build",
130
- label: "Build web assets",
131
- detail: "Production web build. Assets are consumed by the Android release build.",
132
- },
133
- {
134
- step: 4,
135
- cmd: "npm run buildApp:android",
129
+ cmd: "npm run android:release",
136
130
  label: "Android release build",
137
- detail: "Skips emulator validation. Runs Gradle release build. Calls buildAndroidAAB() — renames project, creates/verifies keystore, signs AAB. Output goes to ./deployment/.",
131
+ detail: "catalyst buildApp:android --release. Signs the AAB with the upload key (passwords via environment, never argv). targetSdk 36, 16 KB aligned native libraries. Run `catalyst build` first only with android.buildOptimisation.",
138
132
  },
139
133
  {
140
- step: 5,
134
+ step: 4,
141
135
  cmd: null,
142
136
  label: "AAB output",
143
- detail: "Signed .aab in ./deployment/. Upload to Play Console. Not directly installable — use bundletool to test on device.",
137
+ detail: "build-output/android/<version>-<build>/<name>.aab and upload_certificate.pem. Upload to Play Console (Testing > Internal testing) by hand; Catalyst does not publish.",
144
138
  },
145
139
  ],
146
140
 
147
141
  ios_debug: [
148
142
  {
149
143
  step: 1,
150
- cmd: null,
151
- label: "Pre-check: config",
152
- detail: "Reads WEBVIEW_CONFIG and WEBVIEW_CONFIG.ios. Confirm the development URL, appBundleId, simulatorName, plugins, and selected public/ios assets.",
144
+ cmd: "npm run ios",
145
+ label: "iOS debug build",
146
+ detail: 'catalyst buildApp:ios. Checks full Xcode, offers the Xcode licence and first-launch steps, installs a simulator runtime, and uses an existing iPhone simulator (ios.simulatorName, else the last pick, a booted one or the newest), creating "Catalyst iPhone" after asking only when there is none.',
153
147
  },
154
148
  {
155
149
  step: 2,
156
- cmd: "npm run buildApp:ios",
157
- label: "iOS simulator build",
158
- detail: "Generates native configuration and plugin code in the package-owned Xcode project, syncs selected assets, compiles with Xcode's Debug configuration, installs the .app, and launches it.",
150
+ cmd: null,
151
+ label: "Target",
152
+ detail: "Simulator by default; with an iPhone connected a terminal run asks; --device uses the iPhone (ios.deviceUDID picks one without a terminal). The pick is remembered in .catalyst/local.json. An iPhone needs ios.developmentTeam and an Apple account signed in to Xcode once (a free account works).",
159
153
  },
160
154
  {
161
155
  step: 3,
162
156
  cmd: null,
163
- label: "Remote web application",
164
- detail: "The iOS shell loads the configured remote URL. buildApp:ios does not bundle the full Catalyst web client, so the target web application must be reachable.",
157
+ label: "Dev server",
158
+ detail: "Starts `catalyst start` when nothing answers and waits. Simulator: localhost. iPhone: <LocalHostName>.local (else the LAN IP), with the server bound to the network; a server already running for localhost only fails with NATIVE-006. Debug builds add NSAllowsLocalNetworking and ignore WEBVIEW_CONFIG.url.",
165
159
  },
166
160
  {
167
161
  step: 4,
@@ -175,44 +169,26 @@ const MASTER_FLOWS = {
175
169
  {
176
170
  step: 1,
177
171
  cmd: null,
178
- label: "Deploy the production web application",
179
- detail: "Deploy the web application first and confirm its production URL is reachable over HTTPS. The iOS shell loads this remote URL; it does not embed the full web client bundle.",
172
+ label: "Pre-check: config",
173
+ detail: "WEBVIEW_CONFIG.url (or --url), appId (not com.example.*), ios.developmentTeam for cloud or Xcode-account signing (else IOS-010). Version from package.json, build number from the clock or CATALYST_BUILD_NUMBER.",
180
174
  },
181
175
  {
182
176
  step: 2,
183
177
  cmd: null,
184
- label: "Pre-check: config",
185
- detail: "Finalize WEBVIEW_CONFIG, appBundleId, app name, version, incremented build number, access-control rules, plugins, permissions, capabilities, icons, splash assets, and privacy declarations.",
178
+ label: "Pre-check: signing",
179
+ detail: "credentials/ios/ (CI: CATALYST_IOS_CREDENTIALS from `catalyst credentials:export ios`). Cloud signing: AuthKey_<KEYID>.p8 (Admin-role App Store Connect API key) + signing.properties with ascIssuerId. Local signing: one .p12 + one .mobileprovision + p12Password in signing.properties (temporary keychain, removed after the build). Bad key inputs: IOS-011; bad p12/profile: IOS-012.",
186
180
  },
187
181
  {
188
182
  step: 3,
189
- cmd: "npm run buildApp:ios",
190
- label: "Prepare the native project",
191
- detail: "Run from the consuming Catalyst app root. This generates native configuration and plugin code, syncs selected assets, and performs a Debug native build. It does not create an App Store archive.",
183
+ cmd: "npm run ios:release",
184
+ label: "iOS release build",
185
+ detail: "catalyst buildApp:ios --release. No Xcode UI: xcodebuild archive (Release, generic/platform=iOS), then -exportArchive with ios.exportMethod (default app-store-connect). Writes build-output/ios/<version>-<build>/<name>.ipa and <name>-dSYMs.zip. ITSAppUsesNonExemptEncryption=false.",
192
186
  },
193
187
  {
194
188
  step: 4,
195
189
  cmd: null,
196
- label: "Open the generated Xcode project",
197
- detail: "Open node_modules/catalyst-core/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj (or the resolved package equivalent). Confirm the iosnativeWebView scheme's Archive action uses Release.",
198
- },
199
- {
200
- step: 5,
201
- cmd: null,
202
- label: "Configure signing and archive",
203
- detail: "Select the correct Apple Developer team, distribution signing, capabilities, entitlements, bundle ID, version, and build. Choose a generic iOS device destination, then Product → Archive.",
204
- },
205
- {
206
- step: 6,
207
- cmd: null,
208
- label: "Validate and upload",
209
- detail: "In Xcode Organizer, run Validate App, then Distribute App → TestFlight & App Store and upload the build.",
210
- },
211
- {
212
- step: 7,
213
- cmd: null,
214
- label: "Complete the release",
215
- detail: "Wait for App Store Connect processing, test through TestFlight, complete store metadata and privacy disclosures, select the processed build, and submit it for App Review.",
190
+ label: "Upload and release",
191
+ detail: "Upload the IPA with Transporter or `xcrun altool --upload-app` (Catalyst does not upload), test through TestFlight, complete App Store Connect metadata and privacy details, and submit for App Review.",
216
192
  },
217
193
  ],
218
194
  }
@@ -296,16 +272,17 @@ const MASTER_DIAGRAMS = {
296
272
  "→ [web path] hook falls back to browser API (navigator.vibrate, <input type=file>, etc.)",
297
273
  ],
298
274
  configurable_today: [
299
- "port — WebView loads this port from the local server",
300
- "useHttps — switches WebView URL to https (requires cert setup)",
301
- "LOCAL_IP — override auto-detected IP for device builds",
275
+ "appId: Android application id and iOS bundle id (release refuses com.example.*)",
276
+ "appName: name under the app icon on both platforms",
277
+ "url: production origin release builds load (--url overrides); debug builds use the dev server",
278
+ "allowPrivateHost: lets a release load a private host (internal apps)",
302
279
  "accessControl.allowedUrls — whitelist for outbound requests from WebView",
303
280
  "googleSignIn.enabled / clientId / iosClientId",
304
281
  "splashScreen — duration, minDuration, backgroundColor, image size; optional animated public/splashscreen.html (1.0.2+)",
305
282
  "notifications.enabled — push notification permission prompt",
306
283
  "android.buildOptimisation — enables build caching",
307
- "android.keystoreConfig — signing for release builds",
308
- "ios.appBundleId — bundle identifier for App Store",
284
+ "version: package.json version; build number from the clock or CATALYST_BUILD_NUMBER",
285
+ "signing: credentials/android/ and credentials/ios/ (CI: CATALYST_ANDROID_CREDENTIALS / CATALYST_IOS_CREDENTIALS)",
309
286
  ],
310
287
  },
311
288
 
@@ -383,7 +360,7 @@ const MASTER_DIAGRAMS = {
383
360
  "",
384
361
  "[App build] In app project root:",
385
362
  "→ catalyst build (Vite client + SSR output)",
386
- "→ npm run buildApp:android / buildApp:ios",
363
+ "→ npm run android / npm run ios (catalyst buildApp:android / buildApp:ios)",
387
364
  "→ native build script reads WEBVIEW_CONFIG from config/config.json",
388
365
  "→ copies build/ assets into native project",
389
366
  "→ compiles native project (Gradle / xcodebuild)",
@@ -490,7 +467,7 @@ const LEGACY_BUILD_PIPELINE = {
490
467
  flow: [
491
468
  "catalyst build (webpack)",
492
469
  "→ catalyst serve for production-style validation",
493
- "→ npm run buildApp:android or npm run buildApp:ios",
470
+ "→ npm run android or npm run ios",
494
471
  ],
495
472
  }
496
473
 
@@ -510,7 +487,6 @@ function getProjectContext(root) {
510
487
  hasIos: !!wv.ios,
511
488
  androidBuildType: (wv.android?.buildType || "debug").toLowerCase(),
512
489
  iosBuildType: (wv.ios?.buildType || "debug").toLowerCase(),
513
- hasKeystore: !!wv.android?.keystoreConfig,
514
490
  hasGoogleSignIn: !!wv.googleSignIn?.enabled,
515
491
  hasNotifications: !!wv.notifications?.enabled,
516
492
  hasEcosystem: fileExists("ecosystem.config.js"),
@@ -520,6 +496,27 @@ function getProjectContext(root) {
520
496
  }
521
497
  }
522
498
 
499
+ /** Warnings shared by both platforms for appId, url and LOCAL_IP (catalyst-core 1.3.0+). */
500
+ function appIdentityWarnings(ctx, isRelease, warnings) {
501
+ const wv = ctx.wv
502
+ const appId = wv.appId || wv.android?.packageName || wv.ios?.appBundleId || wv.ios?.bundleIdentifier
503
+ if (isRelease && (!appId || /^com\.example\./i.test(appId))) {
504
+ warnings.push(
505
+ "WEBVIEW_CONFIG.appId is missing or a com.example placeholder: the release fails with NATIVE-003. Set your own reverse-DNS id."
506
+ )
507
+ }
508
+ if (isRelease && !wv.url && !wv.LOCAL_IP) {
509
+ warnings.push(
510
+ "WEBVIEW_CONFIG.url not set: set the production origin (e.g. https://shop.acme.com) or pass --url."
511
+ )
512
+ }
513
+ if (wv.LOCAL_IP && /^\d{1,3}(\.\d{1,3}){3}$/.test(wv.LOCAL_IP) && wv.LOCAL_IP !== "127.0.0.1") {
514
+ warnings.push(
515
+ `WEBVIEW_CONFIG.LOCAL_IP is ${wv.LOCAL_IP}. Keep it "localhost"; debug builds work out the dev server address, and the first 1.3.0 build resets saved LAN addresses.`
516
+ )
517
+ }
518
+ }
519
+
523
520
  function getKnownErrors(keywords) {
524
521
  if (!keywords || !keywords.length) return []
525
522
  // Score known_errors rows by keyword overlap
@@ -604,25 +601,13 @@ function handle_get_build_flow({ platform, mode, symptom } = {}) {
604
601
 
605
602
  const isRelease = m === "release" || ctx.androidBuildType === "release"
606
603
  flow_key = isRelease ? "android_release" : "android_debug"
607
-
608
- if (isRelease) {
609
- if (!ctx.hasKeystore) {
610
- warnings.push(
611
- "WEBVIEW_CONFIG.android.keystoreConfig missing — release build will fail at AAB signing step. Add keystoreConfig with keyAlias, storePassword, keyPassword, organizationInfo."
612
- )
613
- }
614
- } else {
615
- if (!ctx.wv.android?.sdkPath) {
616
- warnings.push(
617
- "WEBVIEW_CONFIG.android.sdkPath not set — ADB and emulator validation will fail."
618
- )
619
- }
620
- if (!ctx.wv.android?.emulatorName) {
621
- warnings.push("WEBVIEW_CONFIG.android.emulatorName not set — emulator launch step will fail.")
622
- }
604
+ appIdentityWarnings(ctx, isRelease, warnings)
605
+ if (ctx.androidBuildType === "release") {
606
+ warnings.push(
607
+ 'android.buildType "release" is deprecated: use `npm run android:release` and set buildType to "debug" or remove it.'
608
+ )
623
609
  }
624
610
 
625
- notes.push(`buildType detected: ${ctx.androidBuildType}`)
626
611
  if (ctx.wv.android?.sdkPath) notes.push(`sdkPath: ${ctx.wv.android.sdkPath}`)
627
612
  if (ctx.wv.android?.emulatorName) notes.push(`emulatorName: ${ctx.wv.android.emulatorName}`)
628
613
  if (ctx.wv.android?.buildOptimisation) notes.push("buildOptimisation: enabled")
@@ -636,41 +621,32 @@ function handle_get_build_flow({ platform, mode, symptom } = {}) {
636
621
  )
637
622
  }
638
623
 
639
- const isRelease = m === "release"
624
+ const isRelease = m === "release" || ctx.iosBuildType === "release"
640
625
  flow_key = isRelease ? "ios_release" : "ios_debug"
641
-
642
- if (!ctx.wv.ios?.appBundleId) {
626
+ appIdentityWarnings(ctx, isRelease, warnings)
627
+ if (ctx.iosBuildType === "release") {
643
628
  warnings.push(
644
- 'WEBVIEW_CONFIG.ios.appBundleId not set — Xcode build will use fallback "com.debug.webview".'
629
+ 'ios.buildType "release" is deprecated: use `npm run ios:release` and set buildType to "debug" or remove it.'
645
630
  )
646
631
  }
647
- if (!ctx.wv.ios?.simulatorName && !isRelease) {
648
- warnings.push("WEBVIEW_CONFIG.ios.simulatorName not set — simulator launch step will fail.")
649
- }
650
- if (ctx.hasGoogleSignIn && !ctx.wv.ios?.simulatorName) {
632
+ if (isRelease && !ctx.wv.ios?.developmentTeam && !ctx.wv.ios?.provisioningProfile) {
651
633
  warnings.push(
652
- "googleSignIn.enabled=true but iosClientId may be missing — check WEBVIEW_CONFIG.googleSignIn.iosClientId."
634
+ "ios.developmentTeam not set. A release fails with IOS-010 unless credentials/ios/ holds a .p12 and its provisioning profile."
653
635
  )
654
636
  }
655
- if (isRelease && !ctx.useHttps) {
637
+ if (ctx.hasGoogleSignIn && !ctx.wv.googleSignIn?.iosClientId) {
656
638
  warnings.push(
657
- "WEBVIEW_CONFIG.useHttps is not enabled — the App Store flow requires a reachable HTTPS production web application."
639
+ "googleSignIn.enabled=true but iosClientId may be missing. Check WEBVIEW_CONFIG.googleSignIn.iosClientId."
658
640
  )
659
641
  }
660
642
  if (isRelease && ctx.hasNotifications) {
661
643
  warnings.push(
662
- "Push notifications are enabled — review the generated aps-environment entitlement and Release provisioning before archiving."
644
+ "Push notifications are enabled: review the generated aps-environment entitlement and the release provisioning profile."
663
645
  )
664
646
  }
665
647
 
666
- notes.push(`buildType detected: ${ctx.wv.ios?.buildType || "not set (defaults to debug)"}`)
667
- if (isRelease) {
668
- notes.push(
669
- "ios.buildType does not select the App Store build configuration; buildApp:ios still performs a Debug build. Archive the generated project with the scheme's Release action."
670
- )
671
- }
672
- if (ctx.wv.ios?.appBundleId) notes.push(`appBundleId: ${ctx.wv.ios.appBundleId}`)
673
648
  if (ctx.wv.ios?.simulatorName) notes.push(`simulatorName: ${ctx.wv.ios.simulatorName}`)
649
+ if (ctx.wv.ios?.developmentTeam) notes.push(`developmentTeam: ${ctx.wv.ios.developmentTeam}`)
674
650
  if (ctx.hasGoogleSignIn) notes.push("googleSignIn: enabled")
675
651
  } else {
676
652
  return { error: `Unknown platform "${platform}". Use: web | android | ios` }