catalyst-core 0.1.0-beta.5 → 0.1.0-beta.7

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.
@@ -465,6 +465,18 @@ class CustomWebView(
465
465
  return "build/public/$fileName"
466
466
  }
467
467
 
468
+ private fun withCorsHeaders(response: WebResourceResponse): WebResourceResponse {
469
+ val responseHeaders = response.responseHeaders
470
+ if (responseHeaders.isNullOrEmpty()) {
471
+ response.responseHeaders = mutableMapOf(
472
+ "Access-Control-Allow-Origin" to "*",
473
+ "Access-Control-Allow-Methods" to "GET, OPTIONS",
474
+ "Access-Control-Allow-Headers" to "*"
475
+ )
476
+ }
477
+ return response
478
+ }
479
+
468
480
  private fun getMimeType(path: String): String {
469
481
  return when {
470
482
  path.endsWith(".html") -> "text/html"
@@ -533,7 +545,7 @@ class CustomWebView(
533
545
  if (BuildConfig.DEBUG) {
534
546
  Log.d(TAG, "✅ SERVICE_WORKER: Served from cache: $url")
535
547
  }
536
- response
548
+ withCorsHeaders(response)
537
549
  } else {
538
550
  if (BuildConfig.DEBUG) {
539
551
  Log.d(TAG, "❌ SERVICE_WORKER: Cache miss: $url")
@@ -827,7 +839,7 @@ class CustomWebView(
827
839
  Log.d(TAG, "✅ WEBVIEW_CLIENT: Served from cache in ${duration}ms: $url")
828
840
  }
829
841
  metricsMonitor.recordCacheHit(url)
830
- return response
842
+ return withCorsHeaders(response)
831
843
  } else {
832
844
  if (BuildConfig.DEBUG) {
833
845
  Log.d(TAG, "❌ WEBVIEW_CLIENT: Cache miss, triggering async fetch: $url")
@@ -330,25 +330,27 @@ class WebCacheManager(private val context: Context, private val properties: java
330
330
  val eTag = connection.getHeaderField("ETag")
331
331
  val lastModified = connection.getHeaderField("Last-Modified")
332
332
 
333
+ // Capture all response headers for replay on cache hits
334
+ val savedHeaders = mutableMapOf<String, String>()
335
+ var i = 0
336
+ while (true) {
337
+ val key = connection.getHeaderFieldKey(i) ?: break
338
+ val value = connection.getHeaderField(i)
339
+ if (key.isNotEmpty()) savedHeaders[key] = value
340
+ i++
341
+ }
342
+
333
343
  val responseBytes = connection.inputStream.use { it.readBytes() }
334
344
  if (!isValidResponse(mimeType, responseBytes)) {
335
345
  return@withContext null
336
346
  }
337
347
 
338
348
  // Create response for immediate use
339
- val response = WebResourceResponse(
340
- mimeType,
341
- encoding,
342
- ByteArrayInputStream(responseBytes)
343
- )
349
+ val response = WebResourceResponse(mimeType, encoding, 200, "OK", savedHeaders, ByteArrayInputStream(responseBytes))
344
350
 
345
351
  // Cache the response
346
352
  val cacheEntry = CacheEntry(
347
- response = WebResourceResponse(
348
- mimeType,
349
- encoding,
350
- ByteArrayInputStream(responseBytes.copyOf())
351
- ),
353
+ response = WebResourceResponse(mimeType, encoding, 200, "OK", savedHeaders, ByteArrayInputStream(responseBytes.copyOf())),
352
354
  eTag = eTag,
353
355
  lastModified = lastModified
354
356
  )
@@ -363,7 +365,8 @@ class WebCacheManager(private val context: Context, private val properties: java
363
365
  mimeType = mimeType,
364
366
  encoding = encoding,
365
367
  eTag = eTag,
366
- lastModified = lastModified
368
+ lastModified = lastModified,
369
+ responseHeaders = savedHeaders
367
370
  ))
368
371
  if (BuildConfig.DEBUG) {
369
372
  Log.d(TAG, "✅ Successfully cached response for: $url")
@@ -391,13 +394,17 @@ class WebCacheManager(private val context: Context, private val properties: java
391
394
  val mimeType: String,
392
395
  val encoding: String,
393
396
  val eTag: String? = null,
394
- val lastModified: String? = null
397
+ val lastModified: String? = null,
398
+ val responseHeaders: Map<String, String> = emptyMap()
395
399
  ) : Serializable
396
400
 
397
401
  private fun createResponseFromCache(cacheFile: File, metadata: CacheMetadata): WebResourceResponse {
398
402
  return WebResourceResponse(
399
403
  metadata.mimeType,
400
404
  metadata.encoding,
405
+ 200,
406
+ "OK",
407
+ metadata.responseHeaders.ifEmpty { null },
401
408
  BufferedInputStream(FileInputStream(cacheFile), BUFFER_SIZE)
402
409
  )
403
410
  }
@@ -3,6 +3,7 @@ try{await(0,_utils.runCommand)("brew install --cask zulu@17");}catch(error){if(e
3
3
  if(!_fs.default.existsSync(javaHome)){throw new Error(`Java installation not found at ${javaHome}. Please verify the installation manually.`);}// Set up environment variables
4
4
  const homeDir=process.env.HOME;const shellProfile=`${homeDir}/.zshrc`;const exportCommand=`\n# Java Configuration\nexport JAVA_HOME=${javaHome}\nexport PATH=$JAVA_HOME/bin:$PATH\n`;try{_fs.default.appendFileSync(shellProfile,exportCommand);}catch(error){throw new Error(`Failed to update ${shellProfile}: ${error.message}. Please add the following lines manually:\n${exportCommand}`);}// Set for current process
5
5
  process.env.JAVA_HOME=javaHome;process.env.PATH=`${javaHome}/bin:${process.env.PATH}`;// Verify Java configuration
6
- try{const javaVersion=(0,_utils.runCommand)("java -version");progress.log("Java installed and configured successfully","success");progress.log(`Java Version: ${javaVersion}`,"info");progress.log(`IMPORTANT: Please run the following commands to complete setup:`,"warning");progress.log(`1. source ${shellProfile}`,"info");progress.log(`2. echo $JAVA_HOME`,"info");return true;}catch(error){throw new Error(`Java installation verified but environment not configured. Please run:\n1. source ${shellProfile}\n2. echo $JAVA_HOME`);}}catch(error){progress.log(error.message,"error");progress.log("Manual Setup Instructions:","info");progress.log("1. Run: brew reinstall --cask zulu@17","info");progress.log("2. Add to ~/.zshrc:","info");progress.log(" export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home","info");progress.log(" export PATH=$JAVA_HOME/bin:$PATH","info");progress.log("3. Run: source ~/.zshrc","info");throw error;}}async function validateJavaEnvironment(){progress.start("java");const hasJava=await checkJavaInstallation();if(!hasJava){progress.log("Java installation required. Starting installation process...");const javaInstalled=await installJava();if(!javaInstalled){progress.fail("java","Failed to set up Java environment");throw new Error("Failed to set up Java environment");}}progress.complete("java");}async function initializeConfig(){const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG}=config;if(!WEBVIEW_CONFIG||Object.keys(WEBVIEW_CONFIG).length===0){progress.log("WebView Config missing in "+configPath,"error");process.exit(1);}if(!WEBVIEW_CONFIG.android){WEBVIEW_CONFIG.android={};}return{WEBVIEW_CONFIG};}async function saveConfig(newConfig){try{const existingConfigFile=_fs.default.readFileSync(configPath,"utf8");const existingConfig=JSON.parse(existingConfigFile);const updatedConfig={...existingConfig,WEBVIEW_CONFIG:{...existingConfig.WEBVIEW_CONFIG,android:newConfig.WEBVIEW_CONFIG.android}};_fs.default.writeFileSync(configPath,JSON.stringify(updatedConfig,null,2));progress.log("Configuration saved successfully","success");}catch(error){progress.log("Failed to save configuration: "+error.message,"error");process.exit(1);}}function validateAndroidTools(androidConfig){const ANDROID_SDK=androidConfig.sdkPath;const EMULATOR_PATH=`${ANDROID_SDK}/emulator/emulator`;const ADB_PATH=`${ANDROID_SDK}/platform-tools/adb`;if(!ANDROID_SDK){throw new Error("Android SDK path is not configured");}if(!_fs.default.existsSync(ANDROID_SDK)){throw new Error(`Android SDK path does not exist: ${ANDROID_SDK}`);}if(!_fs.default.existsSync(ADB_PATH)){throw new Error(`ADB not found at: ${ADB_PATH}`);}try{(0,_utils.runCommand)(`${ADB_PATH} version`);progress.log("ADB is valid","success");}catch(error){throw new Error(`ADB is not working properly: ${error.message}`);}if(!_fs.default.existsSync(EMULATOR_PATH)){throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);}try{(0,_utils.runCommand)(`${EMULATOR_PATH} -version`);progress.log("Emulator is valid","success");}catch(error){throw new Error(`Emulator is not working properly: ${error.message}`);}try{const avdOutput=(0,_utils.runCommand)(`${EMULATOR_PATH} -list-avds`);if(!avdOutput.includes(androidConfig.emulatorName)){throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);}progress.log(`Emulator "${androidConfig.emulatorName}" exists`,"success");}catch(error){throw new Error(`Error checking emulator AVD: ${error.message}`);}progress.log("Android tools validation completed successfully!","success");return{EMULATOR_PATH,ADB_PATH};}async function checkEmulator(ADB_PATH){try{const devices=(0,_utils.runCommand)(`${ADB_PATH} devices`);return devices.includes("emulator");}catch(error){progress.log(`Error checking emulator: ${error}`,"error");return false;}}async function startEmulator(EMULATOR_PATH,androidConfig){progress.log(`Starting emulator: ${androidConfig.emulatorName}...`);// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - EMULATOR_PATH is validated and emulatorName comes from local Android config.
7
- (0,_child_process.exec)(`${EMULATOR_PATH} -avd ${androidConfig.emulatorName} -read-only > /dev/null &`,error=>{if(error){progress.log(`Error starting emulator: ${error}`,"error");}});}async function updateLocalProperties(sdkPath){progress.start("properties");try{(0,_utils.runCommand)(`cd ${pwd}/androidProject && ./gradlew updateSdkPath -PsdkPath="${sdkPath}"`);progress.log("Updated local.properties successfully","success");progress.complete("properties");}catch(error){progress.fail("properties",`Failed to update local.properties: ${error.message}`);throw error;}}async function setupAndroidEnvironment(){try{await validateJavaEnvironment();progress.start("config");await initializeConfig();const config=await(0,_utils.validateAndCompleteConfig)("android",configPath);progress.complete("config");progress.start("androidTools");const{EMULATOR_PATH,ADB_PATH}=validateAndroidTools(config.android);progress.complete("androidTools");progress.start("emulator");const emulatorRunning=await checkEmulator(ADB_PATH);if(!emulatorRunning){progress.log("No emulator running, attempting to start one...");await startEmulator(EMULATOR_PATH,config.android);}else{progress.log("Emulator already running, proceeding with installation...","success");}progress.complete("emulator");if(config.android?.sdkPath){await updateLocalProperties(config.android.sdkPath);}progress.start("saveConfig");await saveConfig({WEBVIEW_CONFIG:config});progress.complete("saveConfig");progress.start("setupServer");await(0,_setupServer.setupServer)(configPath);progress.complete("setupServer");progress.printTreeContent("Configuration Explanation",["WEBVIEW_CONFIG: Main configuration object for the WebView setup",{text:"port: Port number for the WebView server",indent:1,prefix:"├─ ",color:"gray"},{text:"android: Android-specific configuration",indent:1,prefix:"└─ ",color:"gray"},{text:"buildType: Build type (debug/release)",indent:2,prefix:"├─ ",color:"gray"},{text:"sdkPath: Android SDK path",indent:2,prefix:"├─ ",color:"gray"},{text:"emulatorName: Selected Android emulator name",indent:2,prefix:"└─ ",color:"gray"}]);progress.printTreeContent("Final Configuration",[JSON.stringify(config,null,2)]);process.exit(0);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);}process.exit(1);}}// Execute the main setup
6
+ try{const javaVersion=(0,_utils.runCommand)("java -version");progress.log("Java installed and configured successfully","success");progress.log(`Java Version: ${javaVersion}`,"info");progress.log(`IMPORTANT: Please run the following commands to complete setup:`,"warning");progress.log(`1. source ${shellProfile}`,"info");progress.log(`2. echo $JAVA_HOME`,"info");return true;}catch(error){throw new Error(`Java installation verified but environment not configured. Please run:\n1. source ${shellProfile}\n2. echo $JAVA_HOME`);}}catch(error){progress.log(error.message,"error");progress.log("Manual Setup Instructions:","info");progress.log("1. Run: brew reinstall --cask zulu@17","info");progress.log("2. Add to ~/.zshrc:","info");progress.log(" export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home","info");progress.log(" export PATH=$JAVA_HOME/bin:$PATH","info");progress.log("3. Run: source ~/.zshrc","info");throw error;}}async function validateJavaEnvironment(){progress.start("java");const hasJava=await checkJavaInstallation();if(!hasJava){progress.log("Java installation required. Starting installation process...");const javaInstalled=await installJava();if(!javaInstalled){progress.fail("java","Failed to set up Java environment");throw new Error("Failed to set up Java environment");}}progress.complete("java");}async function initializeConfig(){const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG}=config;if(!WEBVIEW_CONFIG||Object.keys(WEBVIEW_CONFIG).length===0){progress.log("WebView Config missing in "+configPath,"error");process.exit(1);}if(!WEBVIEW_CONFIG.android){WEBVIEW_CONFIG.android={};}return{WEBVIEW_CONFIG};}async function saveConfig(newConfig){try{const existingConfigFile=_fs.default.readFileSync(configPath,"utf8");const existingConfig=JSON.parse(existingConfigFile);const updatedConfig={...existingConfig,WEBVIEW_CONFIG:{...existingConfig.WEBVIEW_CONFIG,android:newConfig.WEBVIEW_CONFIG.android}};_fs.default.writeFileSync(configPath,JSON.stringify(updatedConfig,null,2));progress.log("Configuration saved successfully","success");}catch(error){progress.log("Failed to save configuration: "+error.message,"error");process.exit(1);}}function validateAndroidTools(androidConfig){const ANDROID_SDK=androidConfig.sdkPath;const EMULATOR_PATH=`${ANDROID_SDK}/emulator/emulator`;const ADB_PATH=`${ANDROID_SDK}/platform-tools/adb`;if(!ANDROID_SDK){throw new Error("Android SDK path is not configured");}if(!_fs.default.existsSync(ANDROID_SDK)){throw new Error(`Android SDK path does not exist: ${ANDROID_SDK}`);}if(!_fs.default.existsSync(ADB_PATH)){throw new Error(`ADB not found at: ${ADB_PATH}`);}try{(0,_utils.runCommand)(`${ADB_PATH} version`);progress.log("ADB is valid","success");}catch(error){throw new Error(`ADB is not working properly: ${error.message}`);}if(!_fs.default.existsSync(EMULATOR_PATH)){throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);}try{(0,_utils.runCommand)(`${EMULATOR_PATH} -version`);progress.log("Emulator is valid","success");}catch(error){throw new Error(`Emulator is not working properly: ${error.message}`);}try{const avdOutput=(0,_utils.runCommand)(`${EMULATOR_PATH} -list-avds`);if(!avdOutput.includes(androidConfig.emulatorName)){throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);}progress.log(`Emulator "${androidConfig.emulatorName}" exists`,"success");}catch(error){throw new Error(`Error checking emulator AVD: ${error.message}`);}progress.log("Android tools validation completed successfully!","success");return{EMULATOR_PATH,ADB_PATH};}async function checkEmulator(ADB_PATH){try{const devices=(0,_utils.runCommand)(`${ADB_PATH} devices`);return devices.includes("emulator");}catch(error){progress.log(`Error checking emulator: ${error}`,"error");return false;}}async function startEmulator(EMULATOR_PATH,androidConfig){progress.log(`Starting emulator: ${androidConfig.emulatorName}...`);// eslint-disable-next-line security/detect-child-process
7
+ (0,_child_process.exec)(`${EMULATOR_PATH} -avd ${androidConfig.emulatorName} -read-only > /dev/null &`,error=>{// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
8
+ if(error){progress.log(`Error starting emulator: ${error}`,"error");}});}async function updateLocalProperties(sdkPath){progress.start("properties");try{(0,_utils.runCommand)(`cd ${pwd}/androidProject && ./gradlew updateSdkPath -PsdkPath="${sdkPath}"`);progress.log("Updated local.properties successfully","success");progress.complete("properties");}catch(error){progress.fail("properties",`Failed to update local.properties: ${error.message}`);throw error;}}async function setupAndroidEnvironment(){try{await validateJavaEnvironment();progress.start("config");await initializeConfig();const config=await(0,_utils.validateAndCompleteConfig)("android",configPath);progress.complete("config");progress.start("androidTools");const{EMULATOR_PATH,ADB_PATH}=validateAndroidTools(config.android);progress.complete("androidTools");progress.start("emulator");const emulatorRunning=await checkEmulator(ADB_PATH);if(!emulatorRunning){progress.log("No emulator running, attempting to start one...");await startEmulator(EMULATOR_PATH,config.android);}else{progress.log("Emulator already running, proceeding with installation...","success");}progress.complete("emulator");if(config.android?.sdkPath){await updateLocalProperties(config.android.sdkPath);}progress.start("saveConfig");await saveConfig({WEBVIEW_CONFIG:config});progress.complete("saveConfig");progress.start("setupServer");await(0,_setupServer.setupServer)(configPath);progress.complete("setupServer");progress.printTreeContent("Configuration Explanation",["WEBVIEW_CONFIG: Main configuration object for the WebView setup",{text:"port: Port number for the WebView server",indent:1,prefix:"├─ ",color:"gray"},{text:"android: Android-specific configuration",indent:1,prefix:"└─ ",color:"gray"},{text:"buildType: Build type (debug/release)",indent:2,prefix:"├─ ",color:"gray"},{text:"sdkPath: Android SDK path",indent:2,prefix:"├─ ",color:"gray"},{text:"emulatorName: Selected Android emulator name",indent:2,prefix:"└─ ",color:"gray"}]);progress.printTreeContent("Final Configuration",[JSON.stringify(config,null,2)]);process.exit(0);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);}process.exit(1);}}// Execute the main setup
8
9
  setupAndroidEnvironment();
@@ -0,0 +1,52 @@
1
+ "use strict";const fs=require("fs");const path=require("path");function createAssetsPhase(ctx){const{configPath,publicPath,pwd,progress}=ctx;async function copyBuildAssets(androidConfig,buildOptimisation=false){if(!buildOptimisation)return;progress.log("Copying build assets to Android project...","info");try{const sourcePath=`${process.cwd()}/build/`;const destPath=`${pwd}/androidProject/app/src/main/assets/build/`;ctx.runCommand(`mkdir -p ${destPath}`);ctx.runCommand(`rm -rf ${destPath}/*`);const excludePatterns=["route-manifest.json.gz","route-manifest.json.br"];if(buildOptimisation){progress.log("Running with build optimization...","info");const excludeParams=excludePatterns.map(pattern=>`--exclude="${pattern}"`).join(" ");const rsyncCommand=`rsync -av ${excludeParams} ${sourcePath} ${destPath}`;progress.log("Executing rsync command with exclusions...","info");ctx.runCommand(rsyncCommand);for(const pattern of excludePatterns){const checkCommand=`find ${destPath} -name "${pattern}" | wc -l`;const count=parseInt(ctx.runCommand(checkCommand).trim(),10);if(count>0){progress.log(`Warning: Found ${count} instances of excluded file ${pattern}`,"warning");ctx.runCommand(`find ${destPath} -name "${pattern}" -delete`);}}progress.log("Build assets copied with optimization (excluded route-manifest JSON files)","success");}else{progress.log("Running without build optimization...","info");const exclusions=excludePatterns.map(pattern=>`-not -name "${pattern}"`).join(" ");const copyCommand=`find ${sourcePath} -type f ${exclusions} -exec cp --parents {} ${destPath} \\;`;progress.log(`Executing copy command with exclusions...`,"info");ctx.runCommand(copyCommand);progress.log("Build assets copied successfully!","success");}}catch(error){throw new Error("Error copying build assets: "+error.message);}}async function copySplashscreenAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const configFile=fs.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG={}}=config;if(!WEBVIEW_CONFIG.splashScreen)return;const androidPublicPath=`${process.cwd()}/public/android`;const imageFormats=["png","jpg","jpeg","gif","bmp","webp"];if(fs.existsSync(androidPublicPath)){const drawableDir=`${destPath}/drawable`;if(!fs.existsSync(drawableDir)){fs.mkdirSync(drawableDir,{recursive:true});}for(const format of imageFormats){const existingPath=`${destPath}/drawable/splashscreen.${format}`;if(fs.existsSync(existingPath)){fs.unlinkSync(existingPath);}}for(const format of imageFormats){const sourcePath=`${androidPublicPath}/splashscreen.${format}`;if(fs.existsSync(sourcePath)){fs.copyFileSync(sourcePath,`${destPath}/drawable/splashscreen.${format}`);break;}}}const backgroundColor=WEBVIEW_CONFIG.splashScreen.backgroundColor||"#ffffff";const themeFiles=[`${destPath}/values/themes.xml`,`${destPath}/values-night/themes.xml`];for(const themesFile of themeFiles){try{let content=fs.readFileSync(themesFile,"utf8");content=content.replace(/<item name="android:windowBackground">.*?<\/item>/,`<item name="android:windowBackground">${backgroundColor}</item>`);content=content.replace(/<item name="android:windowSplashScreenBackground" tools:targetApi="31">.*?<\/item>/,`<item name="android:windowSplashScreenBackground" tools:targetApi="31">${backgroundColor}</item>`);fs.writeFileSync(themesFile,content);}catch(e){if(e.code!=="ENOENT")throw e;}}}catch(error){progress.log(`Error processing splash screen: ${error.message}`,"warning");}}async function copyOfflinePage(){try{const sourcePath=`${process.cwd()}/public/offline.html`;const destDir=`${pwd}/androidProject/app/src/main/assets/offline`;const destPath=`${destDir}/offline.html`;if(!fs.existsSync(sourcePath)){progress.log("offline.html not found in public/; skipping offline asset copy","warning");return;}ctx.runCommand(`mkdir -p ${destDir}`);fs.copyFileSync(sourcePath,destPath);progress.log("offline.html copied to Android assets","success");}catch(error){progress.log(`Warning: Error copying offline.html: ${error.message}`,"warning");}}async function copyIconAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;const androidIconDir=path.join(publicPath,"android","appIcons");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - publicPath is process.cwd()/public, a trusted internal path.
2
+ const fallbackIconPath=path.join(__dirname,"../assets","catalyst.png");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - __dirname is a fixed module path, not user input.
3
+ const fallbackExists=fs.existsSync(fallbackIconPath);const extensions=["png","jpg","jpeg"];if(!fs.existsSync(publicPath)){progress.log(`Warning: Public directory not found at ${publicPath}`,"warning");return;}const densities=[{key:"mdpi",dir:"mipmap-mdpi"},{key:"hdpi",dir:"mipmap-hdpi"},{key:"xhdpi",dir:"mipmap-xhdpi"},{key:"xxhdpi",dir:"mipmap-xxhdpi"},{key:"xxxhdpi",dir:"mipmap-xxxhdpi"}];const hasIconDirectory=fs.existsSync(androidIconDir)&&fs.lstatSync(androidIconDir).isDirectory();const cleanDensityDir=dir=>{if(!fs.existsSync(dir))return;for(const file of fs.readdirSync(dir)){if(file.startsWith("icon.")){fs.unlinkSync(path.join(dir,file));// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - dir is derived from pwd/destPath (internal), file comes from fs.readdirSync on that same dir.
4
+ }}};const findIconForDensity=densityKey=>{if(!hasIconDirectory)return null;for(const ext of extensions){const candidate=path.join(androidIconDir,`icon-${densityKey}.${ext}`);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - androidIconDir is derived from publicPath (internal); densityKey and ext are fixed internal strings.
5
+ if(fs.existsSync(candidate))return{path:candidate,ext};}return null;};let hasCustomIcons=false;let usedFallback=false;for(const density of densities){const targetDir=path.join(destPath,density.dir);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destPath is derived from pwd (internal); density.dir is a fixed string from the densities array.
6
+ const source=findIconForDensity(density.key);cleanDensityDir(targetDir);if(source){fs.mkdirSync(targetDir,{recursive:true});const destination=path.join(targetDir,`icon.${source.ext}`);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - targetDir is derived from pwd (internal); source.ext is from a fixed extensions array.
7
+ fs.copyFileSync(source.path,destination);hasCustomIcons=true;}}if(!hasCustomIcons&&fallbackExists){const targetDir=path.join(destPath,"mipmap-xxxhdpi");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destPath is derived from pwd (internal); "mipmap-xxxhdpi" is a fixed literal.
8
+ fs.mkdirSync(targetDir,{recursive:true});cleanDensityDir(targetDir);const destination=path.join(targetDir,"icon.png");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - targetDir is derived from pwd (internal); "icon.png" is a fixed literal.
9
+ fs.copyFileSync(fallbackIconPath,destination);usedFallback=true;}const setManifestIcons=(iconValue,roundIconValue)=>{let current;try{current=fs.readFileSync(manifestPath,"utf8");}catch(e){if(e.code==="ENOENT")return;throw e;}const updated=current.replace(/android:icon="[^"]*"/g,`android:icon="${iconValue}"`).replace(/android:roundIcon="[^"]*"/g,`android:roundIcon="${roundIconValue}"`);if(updated!==current)fs.writeFileSync(manifestPath,updated);};if(!hasCustomIcons&&!usedFallback){progress.log("No custom Android icons found; using default template icons.","info");setManifestIcons("@mipmap/ic_launcher","@mipmap/ic_launcher_round");return;}setManifestIcons("@mipmap/icon","@mipmap/icon");if(hasCustomIcons){progress.log("Applied Android launcher icons from public/android/appIcons.","success");}if(usedFallback){progress.log("Used bundled Catalyst fallback icon for launcher.","info");}}catch(error){progress.log(`Warning: Error copying app icon assets: ${error.message}`,"warning");}}async function configureAppName(androidConfig){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const stringsFile=`${destPath}/values/strings.xml`;let stringsContent=fs.readFileSync(stringsFile,"utf8");if(androidConfig.appName){stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">${androidConfig.appName}</string>`);fs.writeFileSync(stringsFile,stringsContent);progress.log(`App display name configured: ${androidConfig.appName}`,"success");}else{stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">Catalyst Application</string>`);fs.writeFileSync(stringsFile,stringsContent);progress.log("App display name reverted to default: Catalyst Application","info");}}catch(error){progress.log(`Warning: Error configuring app name: ${error.message}`,"warning");}}// ── Notification helpers ──────────────────────────────────────────────────
10
+ async function handleGoogleServicesJson(){try{const rootGoogleServicesPath=`${process.cwd()}/google-services.json`;const androidGoogleServicesPath=`${pwd}/androidProject/app/google-services.json`;if(fs.existsSync(rootGoogleServicesPath)){progress.log("Found google-services.json in root directory","info");const appDir=`${pwd}/androidProject/app`;if(!fs.existsSync(appDir)){fs.mkdirSync(appDir,{recursive:true});}fs.copyFileSync(rootGoogleServicesPath,androidGoogleServicesPath);progress.log("Copied google-services.json to androidProject/app/","success");return true;}else if(fs.existsSync(androidGoogleServicesPath)){progress.log("google-services.json already exists in androidProject/app/","info");return true;}else{progress.log("google-services.json not found - Firebase push notifications will not work","warning");progress.log("Place google-services.json in project root or src/native/androidProject/app/","info");return false;}}catch(error){progress.log(`Warning: Error handling google-services.json: ${error.message}`,"warning");return false;}}async function processNotifications(WEBVIEW_CONFIG){const hasNotificationConfig=!!WEBVIEW_CONFIG.notifications?.enabled;try{await cleanupNotificationPermissions();await cleanupNotificationResources();await cleanupNotificationMetadata();await cleanupNotificationAssets();if(!hasNotificationConfig){progress.log("Notifications disabled - cleaned up notification configurations","info");return;}const hasGoogleServices=await handleGoogleServicesJson();if(!hasGoogleServices){progress.log("Continuing without Firebase - only local notifications will work","warning");}await addNotificationPermissions();await generateNotificationResources(WEBVIEW_CONFIG.notifications);await addNotificationMetadata();await processNotificationAssets();progress.log("Notification configuration completed successfully!","success");}catch(error){progress.log(`Warning: Error processing notifications: ${error.message}`,"warning");}}async function addNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const permissionsXml=` <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
11
+ <uses-permission android:name="android.permission.VIBRATE" />
12
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
13
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />`;manifestContent=manifestContent.replace(/(<uses-permission[^>]*>[\s\S]*?)(\s*<uses-feature)/,`$1\n${permissionsXml}$2`);fs.writeFileSync(manifestPath,manifestContent);progress.log("Added notification permissions to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification permissions: ${error.message}`);}}async function generateNotificationResources(notificationConfig){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=fs.readFileSync(colorsPath,"utf8");const notificationColorXml=` <color name="notification_color">${notificationConfig.color||"#007AFF"}</color>`;if(!colorsContent.includes('name="notification_color"')){colorsContent=colorsContent.replace(/(<\/resources>)/,`${notificationColorXml}\n$1`);fs.writeFileSync(colorsPath,colorsContent);progress.log("Added notification color to colors.xml","success");}}catch(error){throw new Error(`Failed to generate notification resources: ${error.message}`);}}async function addNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const metadataXml=`
14
+ <!-- Default notification configuration -->
15
+ <meta-data
16
+ android:name="default_notification_channel_id"
17
+ android:value="default_notifications" />
18
+ <meta-data
19
+ android:name="default_notification_icon"
20
+ android:resource="@drawable/ic_notification" />
21
+ <meta-data
22
+ android:name="default_notification_color"
23
+ android:resource="@color/notification_color" />
24
+
25
+ <!-- Firebase default notification configuration -->
26
+ <meta-data
27
+ android:name="com.google.firebase.messaging.default_notification_channel_id"
28
+ android:value="default" />
29
+ <meta-data
30
+ android:name="com.google.firebase.messaging.default_notification_icon"
31
+ android:resource="@drawable/ic_notification" />
32
+ <meta-data
33
+ android:name="com.google.firebase.messaging.default_notification_sound"
34
+ android:resource="@raw/notification_sound_default" />
35
+
36
+ <!-- Push Notification Service -->
37
+ <service
38
+ android:name="io.yourname.androidproject.utils.PushNotificationUtils"
39
+ android:exported="false">
40
+ <intent-filter>
41
+ <action android:name="com.google.firebase.MESSAGING_EVENT" />
42
+ </intent-filter>
43
+ </service>`;manifestContent=manifestContent.replace(/(\s*<\/application>)/,`${metadataXml}\n$1`);fs.writeFileSync(manifestPath,manifestContent);progress.log("Added notification metadata and push notification service to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification metadata: ${error.message}`);}}async function processNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp"];const audioFormats=["mp3","wav","ogg"];let assetsProcessed=0;const notificationIcons=[{sourceName:"notification-icon",resourceName:"ic_notification"},{sourceName:"notification-large",resourceName:"ic_notification_large"}];const notificationSounds=[{sourceName:"notification-sound-default",resourceName:"notification_sound_default"},{sourceName:"notification-sound-urgent",resourceName:"notification_sound_urgent"}];for(const icon of notificationIcons){for(const format of imageFormats){const existingIconPath=`${destPath}/drawable/${icon.resourceName}.${format}`;if(fs.existsSync(existingIconPath)){fs.unlinkSync(existingIconPath);progress.log(`Removed existing ${icon.resourceName}.${format}`,"info");}}}for(const sound of notificationSounds){for(const format of audioFormats){const existingSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;if(fs.existsSync(existingSoundPath)){fs.unlinkSync(existingSoundPath);progress.log(`Removed existing ${sound.resourceName}.${format}`,"info");}}}let iconFound=false;for(const icon of notificationIcons){for(const format of imageFormats){const iconImagePath=`${publicPath}/${icon.sourceName}.${format}`;if(fs.existsSync(iconImagePath)){const destImagePath=`${destPath}/drawable/${icon.resourceName}.${format}`;fs.copyFileSync(iconImagePath,destImagePath);progress.log(`Notification icon copied: ${icon.sourceName}.${format} -> ${icon.resourceName}.${format}`,"success");assetsProcessed++;if(icon.sourceName==="notification-icon"){iconFound=true;}break;}}}if(!iconFound){generateNotificationIconDrawable("ic_notification",destPath);progress.log("Generated default notification icon","info");}const rawDir=`${destPath}/raw`;if(!fs.existsSync(rawDir)){fs.mkdirSync(rawDir,{recursive:true});}for(const sound of notificationSounds){for(const format of audioFormats){const soundImagePath=`${publicPath}/${sound.sourceName}.${format}`;if(fs.existsSync(soundImagePath)){const destSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;fs.copyFileSync(soundImagePath,destSoundPath);progress.log(`Notification sound copied: ${sound.sourceName}.${format} -> ${sound.resourceName}.${format}`,"success");assetsProcessed++;break;}}}if(assetsProcessed>0){progress.log(`Processed ${assetsProcessed} notification assets from public/`,"success");}else{progress.log("No notification assets found in public/ - using defaults","info");}}catch(error){throw new Error(`Failed to process notification assets: ${error.message}`);}}function generateNotificationIconDrawable(iconName,destPath){const iconXml=`<vector xmlns:android="http://schemas.android.com/apk/res/android"
44
+ android:width="24dp"
45
+ android:height="24dp"
46
+ android:viewportWidth="24"
47
+ android:viewportHeight="24"
48
+ android:tint="?attr/colorOnPrimary">
49
+ <path
50
+ android:fillColor="@android:color/white"
51
+ android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
52
+ </vector>`;fs.writeFileSync(`${destPath}/drawable/${iconName}.xml`,iconXml);}async function cleanupNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const notificationPermissions=["android.permission.POST_NOTIFICATIONS","android.permission.VIBRATE","android.permission.WAKE_LOCK","android.permission.RECEIVE_BOOT_COMPLETED"];manifestContent=manifestContent.split("\n").filter(line=>{return!notificationPermissions.some(permission=>line.includes(`<uses-permission android:name="${permission}"`));}).join("\n");fs.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification permissions: ${error.message}`,"warning");}}async function cleanupNotificationResources(){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=fs.readFileSync(colorsPath,"utf8");const existingColorLine=colorsContent.split("\n").find(line=>line.includes('name="notification_color"'));if(existingColorLine){colorsContent=colorsContent.replace(existingColorLine,"");fs.writeFileSync(colorsPath,colorsContent);}}catch(error){progress.log(`Warning: Error cleaning notification resources: ${error.message}`,"warning");}}async function cleanupNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const metadataNames=["default_notification_channel_id","default_notification_icon","default_notification_color","com.google.firebase.messaging.default_notification_channel_id","com.google.firebase.messaging.default_notification_icon","com.google.firebase.messaging.default_notification_sound"];manifestContent=manifestContent.split("\n").filter(line=>{return!metadataNames.some(metadataName=>line.includes(`android:name="${metadataName}"`));}).join("\n");const serviceRegex=/\s*<!--\s*Push Notification Service\s*-->\s*<service[^>]*android:name="[^"]*PushNotificationUtils"[\s\S]*?<\/service>/gi;manifestContent=manifestContent.replace(serviceRegex,"");manifestContent=manifestContent.replace(/\s*<!--\s*Default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Firebase default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Push Notification Service\s*-->/gi,"");fs.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification metadata: ${error.message}`,"warning");}}async function cleanupNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp","xml"];const audioFormats=["mp3","wav","ogg"];const notificationIcons=["ic_notification","ic_notification_large"];const notificationSounds=["notification_sound_default","notification_sound_urgent"];for(const icon of notificationIcons){for(const format of imageFormats){const iconPath=`${destPath}/drawable/${icon}.${format}`;if(fs.existsSync(iconPath)){fs.unlinkSync(iconPath);}}}const rawDir=`${destPath}/raw`;if(fs.existsSync(rawDir)){for(const sound of notificationSounds){for(const format of audioFormats){const soundPath=`${rawDir}/${sound}.${format}`;if(fs.existsSync(soundPath)){fs.unlinkSync(soundPath);}}}}}catch(error){progress.log(`Warning: Error cleaning notification assets: ${error.message}`,"warning");}}return{copyBuildAssets,copySplashscreenAssets,copyOfflinePage,copyIconAssets,configureAppName,processNotifications};}module.exports=createAssetsPhase;
@@ -0,0 +1,17 @@
1
+ "use strict";const fs=require("fs");const path=require("path");const{buildAndroidAAB}=require("../renameAndroidProject.js");const DEFAULT_DEPLOYMENT_PATH="./deployment";const DEFAULT_OLD_PROJECT_NAME="androidProject";const DEFAULT_OVERWRITE_EXISTING=true;function createBuildPhase(ctx){const{configPath,pwd,progress,ANDROID_PACKAGE}=ctx;async function detectPhysicalDevice(ADB_PATH){try{progress.log("Detecting physical devices...","info");// 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.
2
+ const devices=ctx.runCommand(`${ADB_PATH} devices -l`);const lines=devices.split("\n").filter(line=>line.trim()&&!line.includes("List of devices"));for(const line of lines){const parts=line.trim().split(/\s+/);if(parts.length>=2&&parts[1]==="device"){const deviceId=parts[0];if(!deviceId.startsWith("emulator-")){try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
3
+ const qemuCheck=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell getprop ro.kernel.qemu`);if(!qemuCheck.trim()){progress.log(`Physical device detected: ${deviceId}`,"success");let deviceModel="Unknown Device";try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
4
+ const model=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell getprop ro.product.model`);deviceModel=model.trim()||deviceModel;}catch(e){// Ignore model detection errors
5
+ }return{id:deviceId,model:deviceModel};}}catch(error){continue;}}}}progress.log("No physical devices detected","info");return null;}catch(error){progress.log("Error detecting physical devices: "+error.message,"error");return null;}}async function testPhysicalDeviceInstallation(ADB_PATH,deviceId){try{progress.log(`Testing installation capability on device ${deviceId}...`,"info");// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
6
+ const connectionTest=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell echo "test"`);if(!connectionTest.includes("test")){progress.log("Device connection test failed","error");return false;}try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
7
+ const developerOptions=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell settings get global development_settings_enabled`);if(developerOptions.trim()!=="1"){progress.log("Developer options not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify developer options status","warning");}try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
8
+ const usbDebugging=ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell settings get global adb_enabled`);if(usbDebugging.trim()!=="1"){progress.log("USB debugging not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify USB debugging status","warning");}try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
9
+ ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell pm list packages -3 | head -1`);}catch(error){progress.log("Cannot access package manager on device","error");return false;}progress.log(`Device ${deviceId} is ready for installation`,"success");return true;}catch(error){progress.log(`Installation capability test failed: ${error.message}`,"error");return false;}}async function checkEmulator(ADB_PATH){try{// 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.
10
+ const devices=ctx.runCommand(`${ADB_PATH} devices`);return devices.includes("emulator");}catch(error){progress.log("Error checking emulator status: "+error.message,"error");return false;}}async function startEmulator(EMULATOR_PATH,androidConfig){progress.log(`Starting emulator: ${androidConfig.emulatorName}...`,"info");// 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.
11
+ return ctx.runInteractiveCommand(EMULATOR_PATH,["-avd",androidConfig.emulatorName,"-read-only"],{}).then(()=>{progress.log("Emulator started successfully","success");}).catch(error=>{progress.log("Error starting emulator: "+error.message,"error");throw error;});}async function handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig){progress.log("Setting up emulator...","info");const emulatorRunning=await checkEmulator(ADB_PATH);if(!emulatorRunning){progress.log("No emulator running, attempting to start one...","info");await startEmulator(EMULATOR_PATH,androidConfig);await new Promise(resolve=>setTimeout(resolve,5000));}else{progress.log("Emulator already running","success");}return{type:"emulator",name:androidConfig.emulatorName};}async function buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice=null){progress.log("Building and installing app...","info");try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - pwd and configPath are internal resolved paths, not user input.
12
+ let buildCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath} -PbuildOptimisation=${buildOptimisation} && ./gradlew clean installDebug -PconfigPath=${configPath} --quiet --console=rich`;if(targetDevice&&targetDevice.type==="physical"){buildCommand=buildCommand.replace("installDebug",`installDebug -Pandroid.injected.target.device=${targetDevice.id}`);}await ctx.runInteractiveCommand("sh",["-c",buildCommand],{"BUILD SUCCESSFUL":""});progress.log("App build and installation completed successfully!","success");}catch(error){throw new Error("Error building/installing app: "+error.message);}}async function launchApp(ADB_PATH,buildType="debug",targetDevice=null){if(!targetDevice){progress.log("No target device specified, skipping launch","warning");return;}if(targetDevice.type==="physical"){progress.log(`App installed on physical device: ${targetDevice.model}`,"success");progress.log("Manual launch required - check your device to open the app","info");return;}try{progress.log("Launching app on emulator...","info");const packageName=`${ANDROID_PACKAGE}${buildType==="debug"?".debug":""}`;// 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.
13
+ const launchCommand=`${ADB_PATH} shell monkey -p ${packageName} 1`;await ctx.runInteractiveCommand("sh",["-c",launchCommand],{});progress.log("App launched successfully on emulator!","success");}catch(error){progress.log(`Warning: Could not auto-launch app: ${error.message}`,"warning");progress.log("App was installed successfully, but auto-launch failed","info");}}async function createAABConfig(androidConfig){const DEFAULT_PROJECT_PATH=`${pwd}/androidProject`;const aabConfig={projectPath:androidConfig.projectPath||DEFAULT_PROJECT_PATH,deploymentPath:DEFAULT_DEPLOYMENT_PATH,oldProjectName:DEFAULT_OLD_PROJECT_NAME,overwriteExisting:androidConfig.overwriteExisting!==undefined?androidConfig.overwriteExisting:DEFAULT_OVERWRITE_EXISTING,newProjectName:androidConfig.newProjectName||androidConfig.appName||androidConfig.packageName?.split(".").pop()||"catalystapp",packageName:androidConfig.packageName||null,createSignedAAB:true,outputPath:androidConfig.outputPath||`${process.cwd()}/build-output`};if(androidConfig.keystoreConfig){aabConfig.keystoreConfig=androidConfig.keystoreConfig;}else if(androidConfig.keystore){aabConfig.keystoreConfig={keyAlias:androidConfig.keystore.alias||"release",storePassword:androidConfig.keystore.storePassword,keyPassword:androidConfig.keystore.keyPassword,validityYears:25,organizationInfo:{companyName:androidConfig.keystore.organizationName||"YourCompany",city:androidConfig.keystore.city||"YourCity",state:androidConfig.keystore.state||"YourState",countryCode:androidConfig.keystore.countryCode||"US"}};}progress.log(`AAB Configuration:`,"info");progress.log(` Project Path: ${aabConfig.projectPath} ${aabConfig.projectPath===DEFAULT_PROJECT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Deployment Path: ${aabConfig.deploymentPath} ${aabConfig.deploymentPath===DEFAULT_DEPLOYMENT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Old Project Name: ${aabConfig.oldProjectName} ${aabConfig.oldProjectName===DEFAULT_OLD_PROJECT_NAME?"(default)":"(configured)"}`,"info");progress.log(` New Project Name: ${aabConfig.newProjectName}`,"info");progress.log(` Overwrite Existing: ${aabConfig.overwriteExisting} ${aabConfig.overwriteExisting===DEFAULT_OVERWRITE_EXISTING?"(default)":"(configured)"}`,"info");progress.log(` Output Path: ${aabConfig.outputPath}`,"info");return aabConfig;}async function moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,appName){try{if(!BUILD_OUTPUT_PATH){progress.log("BUILD_OUTPUT_PATH not set, skipping APK move","warning");return null;}const currentDate=new Date().toLocaleDateString("en-GB").replace(/\//g,"-");const currentTime=new Date().toLocaleTimeString("en-US",{hour12:true,hour:"2-digit",minute:"2-digit",second:"2-digit"}).replace(/:/g,"-");let destinationApkFileName="";if(appName){destinationApkFileName=buildType==="release"?`${appName}-${currentTime}.release.apk`:`${appName}-${currentTime}.debug.apk`;}else{destinationApkFileName=buildType==="release"?`app-${currentTime}.release.apk`:`app-${currentTime}.debug.apk`;}const sourceApkFileName=buildType==="release"?`app.apk`:`app-debug.apk`;const sourceApkPath=path.join(pwd,"androidProject","app","build","outputs","apk",buildType,sourceApkFileName);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - pwd is an internal resolved path; buildType is "debug"/"release" from config; sourceApkFileName is derived from buildType.
14
+ const destinationDir=path.join(process.cwd(),BUILD_OUTPUT_PATH,"native","android",currentDate,buildType);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - BUILD_OUTPUT_PATH is from trusted config; buildType and currentDate are internal values.
15
+ const destinationApkPath=path.join(destinationDir,destinationApkFileName);// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destinationDir and destinationApkFileName are derived from internal config values, not user input.
16
+ if(!fs.existsSync(sourceApkPath)){progress.log(`APK not found at source path: ${sourceApkPath}`,"warning");return null;}if(!fs.existsSync(destinationDir)){fs.mkdirSync(destinationDir,{recursive:true});}fs.copyFileSync(sourceApkPath,destinationApkPath);return destinationApkPath;}catch(error){progress.log(`Error moving APK: ${error.message}`,"error");return null;}}async function buildSignedAAB(androidConfig){progress.log("Building signed AAB for release...","info");try{progress.log("Generating webview configuration for release...","info");try{// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - pwd and configPath are internal resolved paths, not user input.
17
+ const generateConfigCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath}`;await ctx.runInteractiveCommand("sh",["-c",generateConfigCommand],{"BUILD SUCCESSFUL":""});progress.log("Webview config generated successfully","success");}catch(configError){progress.log(`Warning: Webview config generation failed: ${configError.message}`,"warning");throw new Error("Webview config generation is required for release builds");}const aabConfig=await createAABConfig(androidConfig);await buildAndroidAAB(aabConfig);progress.log("Signed AAB build completed successfully!","success");}catch(error){throw new Error("Error building signed AAB: "+error.message);}}return{detectPhysicalDevice,testPhysicalDeviceInstallation,checkEmulator,handleEmulatorSetup,startEmulator,buildApp,launchApp,createAABConfig,moveApkToOutputPath,buildSignedAAB};}module.exports=createBuildPhase;
@@ -0,0 +1 @@
1
+ "use strict";const fs=require("fs");function createConfigPhase(ctx){const{configPath,progress}=ctx;async function initializeConfig(){const configFile=fs.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=config;if(!WEBVIEW_CONFIG||Object.keys(WEBVIEW_CONFIG).length===0){throw new Error("WebView Config missing in "+configPath);}if(!WEBVIEW_CONFIG.android){throw new Error("Android config missing in WebView Config");}const buildType=WEBVIEW_CONFIG.android.buildType||"debug";progress.log(`Build Type: ${buildType}`,"info");if(buildType==="release"){progress.log("Release build detected - AAB will be generated","info");}return{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH};}function validateAndroidTools(androidConfig){const ANDROID_SDK=androidConfig.sdkPath;const ADB_PATH=`${ANDROID_SDK}/platform-tools/adb`;const EMULATOR_PATH=`${ANDROID_SDK}/emulator/emulator`;progress.log("Validating Android tools...","info");if(!ANDROID_SDK){throw new Error("Android SDK path is not configured");}if(!fs.existsSync(ANDROID_SDK)){throw new Error(`Android SDK path does not exist: ${ANDROID_SDK}`);}if(!fs.existsSync(ADB_PATH)){throw new Error(`ADB not found at: ${ADB_PATH}`);}try{ctx.runCommand(`${ADB_PATH} version`);progress.log("ADB validation successful","success");}catch(error){throw new Error(`ADB is not working properly: ${error.message}`);}const buildType=androidConfig.buildType||"debug";if(buildType!=="release"){if(!fs.existsSync(EMULATOR_PATH)){throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);}try{ctx.runCommand(`${EMULATOR_PATH} -version`);progress.log("Emulator validation successful","success");}catch(error){throw new Error(`Emulator is not working properly: ${error.message}`);}try{const avdOutput=ctx.runCommand(`${EMULATOR_PATH} -list-avds`);if(!avdOutput.includes(androidConfig.emulatorName)){throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);}progress.log(`Emulator "${androidConfig.emulatorName}" exists`,"success");}catch(error){throw new Error(`Error checking emulator AVD: ${error.message}`);}}else{progress.log("Skipping emulator validation for release build","info");}progress.log("Android tools validation completed successfully!","success");return{ANDROID_SDK,ADB_PATH,EMULATOR_PATH};}return{initializeConfig,validateAndroidTools};}module.exports=createConfigPhase;
@@ -0,0 +1 @@
1
+ "use strict";const path=require("path");const{runCommand,runInteractiveCommand}=require("../utils.js");const TerminalProgress=require("../TerminalProgress.js").default;const{composeAndroidPlugins}=require("../pluginComposerAndroid.js");const{resolvePluginConfig,resolveInternalPluginsRoot}=require("../internalPluginUtils.js");const createConfigPhase=require("./config.js");const createAssetsPhase=require("./assets.js");const createBuildPhase=require("./build.js");const catalystCorePath=path.dirname(require.resolve("catalyst-core/package.json"));const pwd=path.join(catalystCorePath,"dist/native");const ANDROID_PACKAGE="io.yourname.androidproject";const steps={config:"Initialize Configuration",tools:"Validate Android Tools",emulator:"Check and Start Emulator",copyAssets:"Copy Build Assets",build:"Build and Install Application",aab:"Build Signed AAB"};const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};function createAndroidBuild(config){const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=config;const configPath=`${process.cwd()}/config/config.json`;const publicPath=`${process.cwd()}/public`;const progress=new TerminalProgress(steps,"Catalyst Android Build",progressConfig);const ctx={WEBVIEW_CONFIG,BUILD_OUTPUT_PATH,configPath,publicPath,pwd,progress,ANDROID_PACKAGE,runCommand,runInteractiveCommand};const{initializeConfig,validateAndroidTools}=createConfigPhase(ctx);const{copyBuildAssets,copySplashscreenAssets,copyOfflinePage,copyIconAssets,configureAppName,processNotifications}=createAssetsPhase(ctx);const{detectPhysicalDevice,testPhysicalDeviceInstallation,handleEmulatorSetup,buildApp,launchApp,moveApkToOutputPath,buildSignedAAB}=createBuildPhase(ctx);async function buildAndroidApp(){let androidConfig=null;let targetDevice=null;try{progress.start("config");const{WEBVIEW_CONFIG:wvConfig,BUILD_OUTPUT_PATH:bop}=await initializeConfig();androidConfig=wvConfig.android;const buildType=androidConfig.buildType||"debug";const buildOptimisation=!!androidConfig.buildOptimisation||false;progress.complete("config");progress.start("tools");const{ADB_PATH,EMULATOR_PATH}=validateAndroidTools(androidConfig);progress.complete("tools");if(buildType!=="release"){progress.start("emulator");const physicalDevice=await detectPhysicalDevice(ADB_PATH);if(physicalDevice){progress.log(`Found physical device: ${physicalDevice.model} (${physicalDevice.id})`,"success");const canInstallOnPhysical=await testPhysicalDeviceInstallation(ADB_PATH,physicalDevice.id);if(canInstallOnPhysical){targetDevice={type:"physical",id:physicalDevice.id,model:physicalDevice.model};progress.log(`Using physical device: ${physicalDevice.model}`,"success");}else{progress.log("Physical device installation test failed, falling back to emulator","warning");targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}}else{targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}progress.complete("emulator");}else{progress.log("Skipping device setup for release build","info");}progress.start("copyAssets");await copyBuildAssets(androidConfig,buildOptimisation);await copySplashscreenAssets();await copyOfflinePage();await copyIconAssets();await configureAppName(androidConfig);const pluginConfig=resolvePluginConfig(wvConfig);composeAndroidPlugins({corePluginsRoot:resolveInternalPluginsRoot(catalystCorePath),androidProjectPath:`${pwd}/androidProject`,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await processNotifications(wvConfig);progress.log(`Build optimization: ${buildOptimisation?"Enabled":"Disabled"}`,"info");progress.complete("copyAssets");let movedApkPath=null;if(buildType==="release"){progress.start("aab");await buildSignedAAB(androidConfig);progress.complete("aab");movedApkPath=await moveApkToOutputPath(buildType,bop,androidConfig.appName);}else{progress.start("build");await buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice);await launchApp(ADB_PATH,buildType,targetDevice);progress.complete("build");movedApkPath=await moveApkToOutputPath(buildType,bop,androidConfig.appName);}const summaryItems=["Build completed successfully:",{text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`SDK Path: ${androidConfig.sdkPath}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Build Optimization: ${buildOptimisation?"Enabled":"Disabled"}`,indent:1,prefix:"├─ ",color:"gray"}];if(buildType==="release"){if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}summaryItems.push({text:`Output: Signed AAB generated in build-output/`,indent:1,prefix:"└─ ",color:"green"});}else{if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}if(targetDevice&&targetDevice.type==="physical"){summaryItems.push({text:`Target Device: ${targetDevice.model} (Physical)`,indent:1,prefix:"└─ ",color:"green"});}else{summaryItems.push({text:`Target Device: ${androidConfig.emulatorName} (Emulator)`,indent:1,prefix:"└─ ",color:"gray"});}}progress.printTreeContent("Build Summary",summaryItems);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);const troubleshootingItems=["Build failed. Please try the following steps:",{text:"Check if Android SDK is properly configured",indent:1,prefix:"├─ ",color:"yellow"},{text:"Verify build assets exist in the source directory",indent:1,prefix:"├─ ",color:"yellow"}];const buildType=androidConfig?.buildType||"debug";if(buildType!=="release"){if(error.message.includes("physical device")||error.message.includes("installation test")){troubleshootingItems.push("\nPhysical Device Issues:",{text:"Enable Developer Options on your device",indent:1,prefix:"├─ ",color:"yellow"},{text:"Enable USB Debugging in Developer Options",indent:1,prefix:"├─ ",color:"yellow"},{text:'Accept the "Allow USB Debugging" prompt on your device',indent:1,prefix:"├─ ",color:"yellow"},{text:"Try disconnecting and reconnecting your device",indent:1,prefix:"├─ ",color:"yellow"},{text:'Check if "adb devices" shows your device as authorized',indent:1,prefix:"├─ ",color:"yellow"});}troubleshootingItems.push({text:"Verify that the emulator exists and is working",indent:1,prefix:"├─ ",color:"yellow"},{text:"If using physical device, ensure it stays connected",indent:1,prefix:"├─ ",color:"yellow"});}else{troubleshootingItems.push({text:"Verify keystore configuration for release builds",indent:1,prefix:"├─ ",color:"yellow"},{text:"Check that keystore passwords are properly set",indent:1,prefix:"├─ ",color:"yellow"});}troubleshootingItems.push({text:'Run "npm run setupEmulator:android" to reconfigure Android settings',indent:1,prefix:"└─ ",color:"yellow"},"\nVerify Configuration:");if(androidConfig){troubleshootingItems.push({text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Android SDK Path: ${androidConfig.sdkPath||"Not configured"}`,indent:1,prefix:"├─ ",color:"gray"});if(buildType!=="release"){if(targetDevice){troubleshootingItems.push({text:`Target Device: ${targetDevice.type==="physical"?`${targetDevice.model} (Physical)`:`${androidConfig.emulatorName} (Emulator)`}`,indent:1,prefix:"└─ ",color:"gray"});}else{troubleshootingItems.push({text:`Selected Emulator: ${androidConfig.emulatorName||"Not configured"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:`Output Path: ${androidConfig.outputPath||"build-output/"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:"Configuration could not be loaded",indent:1,prefix:"├─ ",color:"red"},{text:"Check if config/config.json exists and has valid Android configuration",indent:1,prefix:"└─ ",color:"red"});}progress.printTreeContent("Troubleshooting Guide",troubleshootingItems);}throw error;}}async function buildAndroidForTesting(){try{const{WEBVIEW_CONFIG:wvConfig}=await initializeConfig();const androidConfig=wvConfig.android;const buildOptimisation=!!androidConfig.buildOptimisation||false;await copyBuildAssets(androidConfig,buildOptimisation);await copySplashscreenAssets();await copyOfflinePage();await copyIconAssets();await configureAppName(androidConfig);const pluginConfig=resolvePluginConfig(wvConfig);composeAndroidPlugins({corePluginsRoot:resolveInternalPluginsRoot(catalystCorePath),androidProjectPath:`${pwd}/androidProject`,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await processNotifications(wvConfig);progress.log("✅ buildAndroidForTesting complete — project ready for gradlew test","success");return{success:true};}catch(error){progress.log("buildAndroidForTesting failed: "+error.message,"error");throw error;}}return{buildAndroidApp,buildAndroidForTesting,progress,WEBVIEW_CONFIG};}module.exports={createAndroidBuild,pwd};
@@ -1,148 +1,2 @@
1
- /* eslint-disable no-extra-semi */"use strict";var _renameAndroidProject=require("./renameAndroidProject.js");var _child_process=require("child_process");var _fs=_interopRequireDefault(require("fs"));var _path=_interopRequireDefault(require("path"));var _utils=require("./utils.js");var _TerminalProgress=_interopRequireDefault(require("./TerminalProgress.js"));var _pluginComposerAndroid=require("./pluginComposerAndroid.js");var _internalPluginUtils=require("./internalPluginUtils.js");// Import the AAB builder
2
- function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e};}const configPath=`${process.cwd()}/config/config.json`;const publicPath=`${process.cwd()}/public`;const catalystCorePath=_path.default.dirname(require.resolve("catalyst-core/package.json"));const pwd=_path.default.join(catalystCorePath,"dist/native");const ANDROID_PACKAGE="io.yourname.androidproject";// Default values for AAB building
3
- const DEFAULT_PROJECT_PATH=`${pwd}/androidProject`;const DEFAULT_DEPLOYMENT_PATH="./deployment";const DEFAULT_OLD_PROJECT_NAME="androidProject";// Use actual project name in catalyst
4
- const DEFAULT_OVERWRITE_EXISTING=true;const steps={config:"Initialize Configuration",tools:"Validate Android Tools",emulator:"Check and Start Emulator",copyAssets:"Copy Build Assets",build:"Build and Install Application",aab:"Build Signed AAB"};const progressConfig={titlePaddingTop:2,titlePaddingBottom:1,stepPaddingLeft:4,stepSpacing:1,errorPaddingLeft:6,bottomMargin:2};const progress=new _TerminalProgress.default(steps,"Catalyst Android Build",progressConfig);async function initializeConfig(){const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=config;if(!WEBVIEW_CONFIG||Object.keys(WEBVIEW_CONFIG).length===0){throw new Error("WebView Config missing in "+configPath);}if(!WEBVIEW_CONFIG.android){throw new Error("Android config missing in WebView Config");}// Log build type information
5
- const buildType=WEBVIEW_CONFIG.android.buildType||"debug";progress.log(`Build Type: ${buildType}`,"info");if(buildType==="release"){progress.log("Release build detected - AAB will be generated","info");}return{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH};}function validateAndroidTools(androidConfig){const ANDROID_SDK=androidConfig.sdkPath;const ADB_PATH=`${ANDROID_SDK}/platform-tools/adb`;const EMULATOR_PATH=`${ANDROID_SDK}/emulator/emulator`;progress.log("Validating Android tools...","info");if(!ANDROID_SDK){throw new Error("Android SDK path is not configured");}if(!_fs.default.existsSync(ANDROID_SDK)){throw new Error(`Android SDK path does not exist: ${ANDROID_SDK}`);}if(!_fs.default.existsSync(ADB_PATH)){throw new Error(`ADB not found at: ${ADB_PATH}`);}try{;(0,_utils.runCommand)(`${ADB_PATH} version`);progress.log("ADB validation successful","success");}catch(error){throw new Error(`ADB is not working properly: ${error.message}`);}// Skip emulator validation for release builds
6
- const buildType=androidConfig.buildType||"debug";if(buildType!=="release"){if(!_fs.default.existsSync(EMULATOR_PATH)){throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);}try{;(0,_utils.runCommand)(`${EMULATOR_PATH} -version`);progress.log("Emulator validation successful","success");}catch(error){throw new Error(`Emulator is not working properly: ${error.message}`);}try{const avdOutput=(0,_utils.runCommand)(`${EMULATOR_PATH} -list-avds`);if(!avdOutput.includes(androidConfig.emulatorName)){throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);}progress.log(`Emulator "${androidConfig.emulatorName}" exists`,"success");}catch(error){throw new Error(`Error checking emulator AVD: ${error.message}`);}}else{progress.log("Skipping emulator validation for release build","info");}progress.log("Android tools validation completed successfully!","success");return{ANDROID_SDK,ADB_PATH,EMULATOR_PATH};}async function detectPhysicalDevice(ADB_PATH){try{progress.log("Detecting physical devices...","info");const devices=(0,_utils.runCommand)(`${ADB_PATH} devices -l`);const lines=devices.split("\n").filter(line=>line.trim()&&!line.includes("List of devices"));for(const line of lines){const parts=line.trim().split(/\s+/);if(parts.length>=2&&parts[1]==="device"){const deviceId=parts[0];// Check if it's a physical device (not emulator)
7
- if(!deviceId.startsWith("emulator-")){try{// Double-check it's not an emulator using device properties
8
- const qemuCheck=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell getprop ro.kernel.qemu`);if(!qemuCheck.trim()){// Empty means physical device
9
- progress.log(`Physical device detected: ${deviceId}`,"success");// Get device model/name for better logging
10
- let deviceModel="Unknown Device";try{const model=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell getprop ro.product.model`);deviceModel=model.trim()||deviceModel;}catch(e){// Ignore model detection errors
11
- }return{id:deviceId,model:deviceModel};}}catch(error){// If we can't check properties, skip this device
12
- continue;}}}}progress.log("No physical devices detected","info");return null;}catch(error){progress.log("Error detecting physical devices: "+error.message,"error");return null;}}async function testPhysicalDeviceInstallation(ADB_PATH,deviceId){try{progress.log(`Testing installation capability on device ${deviceId}...`,"info");// Test 1: Basic ADB connection
13
- const connectionTest=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell echo "test"`);if(!connectionTest.includes("test")){progress.log("Device connection test failed","error");return false;}// Test 2: Check if developer options are enabled
14
- try{const developerOptions=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell settings get global development_settings_enabled`);if(developerOptions.trim()!=="1"){progress.log("Developer options not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify developer options status","warning");}// Test 3: Check USB debugging status
15
- try{const usbDebugging=(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell settings get global adb_enabled`);if(usbDebugging.trim()!=="1"){progress.log("USB debugging not enabled on device","warning");return false;}}catch(error){progress.log("Cannot verify USB debugging status","warning");}// Test 4: Check if we can access package manager
16
- try{;(0,_utils.runCommand)(`${ADB_PATH} -s ${deviceId} shell pm list packages -3 | head -1`);}catch(error){progress.log("Cannot access package manager on device","error");return false;}progress.log(`Device ${deviceId} is ready for installation`,"success");return true;}catch(error){progress.log(`Installation capability test failed: ${error.message}`,"error");return false;}}async function checkEmulator(ADB_PATH){try{const devices=(0,_utils.runCommand)(`${ADB_PATH} devices`);return devices.includes("emulator");}catch(error){progress.log("Error checking emulator status: "+error.message,"error");return false;}}async function handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig){progress.log("Setting up emulator...","info");const emulatorRunning=await checkEmulator(ADB_PATH);if(!emulatorRunning){progress.log("No emulator running, attempting to start one...","info");await startEmulator(EMULATOR_PATH,androidConfig);await new Promise(resolve=>setTimeout(resolve,5000));}else{progress.log("Emulator already running","success");}return{type:"emulator",name:androidConfig.emulatorName};}async function startEmulator(EMULATOR_PATH,androidConfig){progress.log(`Starting emulator: ${androidConfig.emulatorName}...`,"info");return(0,_utils.runInteractiveCommand)(EMULATOR_PATH,["-avd",androidConfig.emulatorName,"-read-only"],{}).then(()=>{progress.log("Emulator started successfully","success");}).catch(error=>{progress.log("Error starting emulator: "+error.message,"error");throw error;});}async function copyBuildAssets(androidConfig,buildOptimisation=false){if(!buildOptimisation)return;progress.log("Copying build assets to Android project...","info");try{// Define source and destination paths
17
- const sourcePath=`${process.env.PWD}/build/`;const destPath=`${pwd}/androidProject/app/src/main/assets/build/`// Create destination directory if it doesn't exist
18
- ;(0,_utils.runCommand)(`mkdir -p ${destPath}`)// Clear existing destination to avoid conflicts
19
- ;(0,_utils.runCommand)(`rm -rf ${destPath}/*`);// Files to exclude from copying
20
- const excludePatterns=["route-manifest.json.gz","route-manifest.json.br"];if(buildOptimisation){progress.log("Running with build optimization...","info");const excludeParams=excludePatterns.map(pattern=>`--exclude="${pattern}"`).join(" ");const rsyncCommand=`rsync -av ${excludeParams} ${sourcePath} ${destPath}`;progress.log("Executing rsync command with exclusions...","info");(0,_utils.runCommand)(rsyncCommand);// Verify excluded files don't exist in destination
21
- for(const pattern of excludePatterns){const checkCommand=`find ${destPath} -name "${pattern}" | wc -l`;const count=parseInt((0,_utils.runCommand)(checkCommand).trim(),10);if(count>0){progress.log(`Warning: Found ${count} instances of excluded file ${pattern}`,"warning");(0,_utils.runCommand)(`find ${destPath} -name "${pattern}" -delete`);}}progress.log("Build assets copied with optimization (excluded route-manifest JSON files)","success");}else{progress.log("Running without build optimization...","info");const exclusions=excludePatterns.map(pattern=>`-not -name "${pattern}"`).join(" ");const copyCommand=`find ${sourcePath} -type f ${exclusions} -exec cp --parents {} ${destPath} \\;`;progress.log(`Executing copy command with exclusions...`,"info");(0,_utils.runCommand)(copyCommand);progress.log("Build assets copied successfully!","success");}}catch(error){throw new Error("Error copying build assets: "+error.message);}}async function buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice=null){progress.log("Building and installing app...","info");try{// Build command without monkey launch
22
- let buildCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath} -PbuildOptimisation=${buildOptimisation} && ./gradlew clean installDebug -PconfigPath=${configPath} --quiet --console=rich`;// Add device-specific install target if physical device
23
- if(targetDevice&&targetDevice.type==="physical"){buildCommand=buildCommand.replace("installDebug",`installDebug -Pandroid.injected.target.device=${targetDevice.id}`);}await(0,_utils.runInteractiveCommand)("sh",["-c",buildCommand],{"BUILD SUCCESSFUL":""});progress.log("App build and installation completed successfully!","success");}catch(error){throw new Error("Error building/installing app: "+error.message);}}async function launchApp(ADB_PATH,buildType="debug",targetDevice=null){if(!targetDevice){progress.log("No target device specified, skipping launch","warning");return;}if(targetDevice.type==="physical"){progress.log(`App installed on physical device: ${targetDevice.model}`,"success");progress.log("Manual launch required - check your device to open the app","info");return;}// Launch on emulator
24
- try{progress.log("Launching app on emulator...","info");const packageName=`${ANDROID_PACKAGE}${buildType==="debug"?".debug":""}`;const launchCommand=`${ADB_PATH} shell monkey -p ${packageName} 1`;await(0,_utils.runInteractiveCommand)("sh",["-c",launchCommand],{});progress.log("App launched successfully on emulator!","success");}catch(error){progress.log(`Warning: Could not auto-launch app: ${error.message}`,"warning");progress.log("App was installed successfully, but auto-launch failed","info");}}async function copySplashscreenAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const configFile=_fs.default.readFileSync(configPath,"utf8");const config=JSON.parse(configFile);const{WEBVIEW_CONFIG={}}=config;if(!WEBVIEW_CONFIG.splashScreen)return;const androidPublicPath=`${process.env.PWD}/public/android`;const imageFormats=["png","jpg","jpeg","gif","bmp","webp"];// Copy splash screen image if exists
25
- if(_fs.default.existsSync(androidPublicPath)){const drawableDir=`${destPath}/drawable`;if(!_fs.default.existsSync(drawableDir)){_fs.default.mkdirSync(drawableDir,{recursive:true});}for(const format of imageFormats){const existingPath=`${destPath}/drawable/splashscreen.${format}`;if(_fs.default.existsSync(existingPath)){_fs.default.unlinkSync(existingPath);}}for(const format of imageFormats){const sourcePath=`${androidPublicPath}/splashscreen.${format}`;if(_fs.default.existsSync(sourcePath)){_fs.default.copyFileSync(sourcePath,`${destPath}/drawable/splashscreen.${format}`);break;}}}// Update theme background color
26
- const backgroundColor=WEBVIEW_CONFIG.splashScreen.backgroundColor||"#ffffff";const themeFiles=[`${destPath}/values/themes.xml`,`${destPath}/values-night/themes.xml`];for(const themesFile of themeFiles){if(_fs.default.existsSync(themesFile)){let content=_fs.default.readFileSync(themesFile,"utf8");// Replace windowBackground - matches any color value
27
- content=content.replace(/<item name="android:windowBackground">.*?<\/item>/,`<item name="android:windowBackground">${backgroundColor}</item>`);// Replace splash screen background - matches any color value
28
- content=content.replace(/<item name="android:windowSplashScreenBackground" tools:targetApi="31">.*?<\/item>/,`<item name="android:windowSplashScreenBackground" tools:targetApi="31">${backgroundColor}</item>`);_fs.default.writeFileSync(themesFile,content);}}}catch(error){progress.log(`Error processing splash screen: ${error.message}`,"warning");}}async function copyOfflinePage(){try{const sourcePath=`${process.env.PWD}/public/offline.html`;const destDir=`${pwd}/androidProject/app/src/main/assets/offline`;const destPath=`${destDir}/offline.html`;if(!_fs.default.existsSync(sourcePath)){progress.log("offline.html not found in public/; skipping offline asset copy","warning");return;}// Ensure destination directory exists
29
- ;(0,_utils.runCommand)(`mkdir -p ${destDir}`);_fs.default.copyFileSync(sourcePath,destPath);progress.log("offline.html copied to Android assets","success");}catch(error){progress.log(`Warning: Error copying offline.html: ${error.message}`,"warning");}}async function copyIconAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;const androidIconDir=_path.default.join(publicPath,"android","appIcons");const fallbackIconPath=_path.default.join(__dirname,"assets","catalyst.png");const fallbackExists=_fs.default.existsSync(fallbackIconPath);const extensions=["png","jpg","jpeg"];if(!_fs.default.existsSync(publicPath)){progress.log(`Warning: Public directory not found at ${publicPath}`,"warning");return;}const densities=[{key:"mdpi",dir:"mipmap-mdpi"},{key:"hdpi",dir:"mipmap-hdpi"},{key:"xhdpi",dir:"mipmap-xhdpi"},{key:"xxhdpi",dir:"mipmap-xxhdpi"},{key:"xxxhdpi",dir:"mipmap-xxxhdpi"}];const hasIconDirectory=_fs.default.existsSync(androidIconDir)&&_fs.default.lstatSync(androidIconDir).isDirectory();const cleanDensityDir=dir=>{if(!_fs.default.existsSync(dir)){return;}for(const file of _fs.default.readdirSync(dir)){if(file.startsWith("icon.")){_fs.default.unlinkSync(_path.default.join(dir,file));}}};const findIconForDensity=densityKey=>{if(!hasIconDirectory){return null;}for(const ext of extensions){const candidate=_path.default.join(androidIconDir,`icon-${densityKey}.${ext}`);if(_fs.default.existsSync(candidate)){return{path:candidate,ext};}}return null;};let hasCustomIcons=false;let usedFallback=false;for(const density of densities){const targetDir=_path.default.join(destPath,density.dir);const source=findIconForDensity(density.key);cleanDensityDir(targetDir);if(source){_fs.default.mkdirSync(targetDir,{recursive:true});const destination=_path.default.join(targetDir,`icon.${source.ext}`);_fs.default.copyFileSync(source.path,destination);hasCustomIcons=true;}}if(!hasCustomIcons&&fallbackExists){const targetDir=_path.default.join(destPath,"mipmap-xxxhdpi");_fs.default.mkdirSync(targetDir,{recursive:true});cleanDensityDir(targetDir);const destination=_path.default.join(targetDir,"icon.png");_fs.default.copyFileSync(fallbackIconPath,destination);usedFallback=true;}const setManifestIcons=(iconValue,roundIconValue)=>{if(!_fs.default.existsSync(manifestPath))return;const current=_fs.default.readFileSync(manifestPath,"utf8");const updated=current.replace(/android:icon="[^"]*"/g,`android:icon="${iconValue}"`).replace(/android:roundIcon="[^"]*"/g,`android:roundIcon="${roundIconValue}"`);if(updated!==current){_fs.default.writeFileSync(manifestPath,updated);}};if(!hasCustomIcons&&!usedFallback){progress.log("No custom Android icons found; using default template icons.","info");setManifestIcons("@mipmap/ic_launcher","@mipmap/ic_launcher_round");return;}setManifestIcons("@mipmap/icon","@mipmap/icon");if(hasCustomIcons){progress.log("Applied Android launcher icons from public/android/appIcons.","success");}if(usedFallback){progress.log("Used bundled Catalyst fallback icon for launcher.","info");}}catch(error){progress.log(`Warning: Error copying app icon assets: ${error.message}`,"warning");}}async function configureAppName(androidConfig){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const stringsFile=`${destPath}/values/strings.xml`;let stringsContent=_fs.default.readFileSync(stringsFile,"utf8");// Check if androidConfig.appName exists
30
- if(androidConfig.appName){stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">${androidConfig.appName}</string>`);_fs.default.writeFileSync(stringsFile,stringsContent);progress.log(`App display name configured: ${androidConfig.appName}`,"success");}else{// No appName configured, revert to default
31
- stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">Catalyst Application</string>`);_fs.default.writeFileSync(stringsFile,stringsContent);progress.log("App display name reverted to default: Catalyst Application","info");}}catch(error){progress.log(`Warning: Error configuring app name: ${error.message}`,"warning");}}async function handleGoogleServicesJson(){try{const rootGoogleServicesPath=`${process.env.PWD}/google-services.json`;const androidGoogleServicesPath=`${pwd}/androidProject/app/google-services.json`;// Check if google-services.json exists in the root directory
32
- if(_fs.default.existsSync(rootGoogleServicesPath)){progress.log("Found google-services.json in root directory","info");// Create the app directory if it doesn't exist
33
- const appDir=`${pwd}/androidProject/app`;if(!_fs.default.existsSync(appDir)){_fs.default.mkdirSync(appDir,{recursive:true});}// Copy the file to the Android project
34
- _fs.default.copyFileSync(rootGoogleServicesPath,androidGoogleServicesPath);progress.log("Copied google-services.json to androidProject/app/","success");return true;}else if(_fs.default.existsSync(androidGoogleServicesPath)){progress.log("google-services.json already exists in androidProject/app/","info");return true;}else{progress.log("google-services.json not found - Firebase push notifications will not work","warning");progress.log("Place google-services.json in project root or src/native/androidProject/app/","info");return false;}}catch(error){progress.log(`Warning: Error handling google-services.json: ${error.message}`,"warning");return false;}}async function processNotifications(WEBVIEW_CONFIG){const hasNotificationConfig=!!WEBVIEW_CONFIG.notifications?.enabled;try{// Always clean up notification configurations first (regardless of config)
35
- await cleanupNotificationPermissions();await cleanupNotificationResources();await cleanupNotificationMetadata();await cleanupNotificationAssets();if(!hasNotificationConfig){progress.log("Notifications disabled - cleaned up notification configurations","info");return;}// Handle google-services.json file for Firebase
36
- const hasGoogleServices=await handleGoogleServicesJson();if(!hasGoogleServices){progress.log("Continuing without Firebase - only local notifications will work","warning");}// Only add configurations if notifications are enabled
37
- await addNotificationPermissions();await generateNotificationResources(WEBVIEW_CONFIG.notifications);await addNotificationMetadata();await processNotificationAssets();progress.log("Notification configuration completed successfully!","success");}catch(error){progress.log(`Warning: Error processing notifications: ${error.message}`,"warning");}}async function addNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");const permissionsXml=` <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
38
- <uses-permission android:name="android.permission.VIBRATE" />
39
- <uses-permission android:name="android.permission.WAKE_LOCK" />
40
- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />`;manifestContent=manifestContent.replace(/(<uses-permission[^>]*>[\s\S]*?)(\s*<uses-feature)/,`$1\n${permissionsXml}$2`);_fs.default.writeFileSync(manifestPath,manifestContent);progress.log("Added notification permissions to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification permissions: ${error.message}`);}}async function generateNotificationResources(notificationConfig){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=_fs.default.readFileSync(colorsPath,"utf8");const notificationColorXml=` <color name="notification_color">${notificationConfig.color||"#007AFF"}</color>`;if(!colorsContent.includes('name="notification_color"')){colorsContent=colorsContent.replace(/(<\/resources>)/,`${notificationColorXml}\n$1`);_fs.default.writeFileSync(colorsPath,colorsContent);progress.log("Added notification color to colors.xml","success");}}catch(error){throw new Error(`Failed to generate notification resources: ${error.message}`);}}async function addNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");const metadataXml=`
41
- <!-- Default notification configuration -->
42
- <meta-data
43
- android:name="default_notification_channel_id"
44
- android:value="default_notifications" />
45
- <meta-data
46
- android:name="default_notification_icon"
47
- android:resource="@drawable/ic_notification" />
48
- <meta-data
49
- android:name="default_notification_color"
50
- android:resource="@color/notification_color" />
51
-
52
- <!-- Firebase default notification configuration -->
53
- <meta-data
54
- android:name="com.google.firebase.messaging.default_notification_channel_id"
55
- android:value="default" />
56
- <meta-data
57
- android:name="com.google.firebase.messaging.default_notification_icon"
58
- android:resource="@drawable/ic_notification" />
59
- <meta-data
60
- android:name="com.google.firebase.messaging.default_notification_sound"
61
- android:resource="@raw/notification_sound_default" />
62
-
63
- <!-- Push Notification Service -->
64
- <service
65
- android:name="io.yourname.androidproject.utils.PushNotificationUtils"
66
- android:exported="false">
67
- <intent-filter>
68
- <action android:name="com.google.firebase.MESSAGING_EVENT" />
69
- </intent-filter>
70
- </service>`;manifestContent=manifestContent.replace(/(\s*<\/application>)/,`${metadataXml}\n$1`);_fs.default.writeFileSync(manifestPath,manifestContent);progress.log("Added notification metadata and push notification service to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification metadata: ${error.message}`);}}async function processNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp"];const audioFormats=["mp3","wav","ogg"];let assetsProcessed=0;// Define notification icons to process
71
- const notificationIcons=[{sourceName:"notification-icon",resourceName:"ic_notification"},{sourceName:"notification-large",resourceName:"ic_notification_large"}];// Define notification sounds to process
72
- const notificationSounds=[{sourceName:"notification-sound-default",resourceName:"notification_sound_default"},{sourceName:"notification-sound-urgent",resourceName:"notification_sound_urgent"}];// Remove existing notification icons to avoid conflicts
73
- for(const icon of notificationIcons){for(const format of imageFormats){const existingIconPath=`${destPath}/drawable/${icon.resourceName}.${format}`;if(_fs.default.existsSync(existingIconPath)){_fs.default.unlinkSync(existingIconPath);progress.log(`Removed existing ${icon.resourceName}.${format}`,"info");}}}// Remove existing notification sounds to avoid conflicts
74
- for(const sound of notificationSounds){for(const format of audioFormats){const existingSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;if(_fs.default.existsSync(existingSoundPath)){_fs.default.unlinkSync(existingSoundPath);progress.log(`Removed existing ${sound.resourceName}.${format}`,"info");}}}// Process notification icons
75
- let iconFound=false;for(const icon of notificationIcons){for(const format of imageFormats){const iconImagePath=`${publicPath}/${icon.sourceName}.${format}`;if(_fs.default.existsSync(iconImagePath)){const destImagePath=`${destPath}/drawable/${icon.resourceName}.${format}`;_fs.default.copyFileSync(iconImagePath,destImagePath);progress.log(`Notification icon copied: ${icon.sourceName}.${format} -> ${icon.resourceName}.${format}`,"success");assetsProcessed++;// Track if we found the main notification icon
76
- if(icon.sourceName==="notification-icon"){iconFound=true;}break;}}}// Generate default notification icon if not found
77
- if(!iconFound){generateNotificationIconDrawable("ic_notification",destPath);progress.log("Generated default notification icon","info");}// Create raw directory if it doesn't exist
78
- const rawDir=`${destPath}/raw`;if(!_fs.default.existsSync(rawDir)){_fs.default.mkdirSync(rawDir,{recursive:true});}// Process notification sounds
79
- for(const sound of notificationSounds){for(const format of audioFormats){const soundImagePath=`${publicPath}/${sound.sourceName}.${format}`;if(_fs.default.existsSync(soundImagePath)){const destSoundPath=`${destPath}/raw/${sound.resourceName}.${format}`;_fs.default.copyFileSync(soundImagePath,destSoundPath);progress.log(`Notification sound copied: ${sound.sourceName}.${format} -> ${sound.resourceName}.${format}`,"success");assetsProcessed++;break;}}}if(assetsProcessed>0){progress.log(`Processed ${assetsProcessed} notification assets from public/`,"success");}else{progress.log("No notification assets found in public/ - using defaults","info");}}catch(error){throw new Error(`Failed to process notification assets: ${error.message}`);}}function generateNotificationIconDrawable(iconName,destPath){// Create vector drawable for notification icon as fallback
80
- const iconXml=`<vector xmlns:android="http://schemas.android.com/apk/res/android"
81
- android:width="24dp"
82
- android:height="24dp"
83
- android:viewportWidth="24"
84
- android:viewportHeight="24"
85
- android:tint="?attr/colorOnPrimary">
86
- <path
87
- android:fillColor="@android:color/white"
88
- android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
89
- </vector>`;_fs.default.writeFileSync(`${destPath}/drawable/${iconName}.xml`,iconXml);}async function cleanupNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");// Remove notification permissions
90
- const notificationPermissions=["android.permission.POST_NOTIFICATIONS","android.permission.VIBRATE","android.permission.WAKE_LOCK","android.permission.RECEIVE_BOOT_COMPLETED"];manifestContent=manifestContent.split("\n").filter(line=>{return!notificationPermissions.some(permission=>line.includes(`<uses-permission android:name="${permission}"`));}).join("\n");_fs.default.writeFileSync(manifestPath,manifestContent);}catch(error){// Don't throw error for cleanup operations, just log
91
- progress.log(`Warning: Error cleaning notification permissions: ${error.message}`,"warning");}}async function cleanupNotificationResources(){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=_fs.default.readFileSync(colorsPath,"utf8");// Remove notification color
92
- const existingColorLine=colorsContent.split("\n").find(line=>line.includes('name="notification_color"'));if(existingColorLine){colorsContent=colorsContent.replace(existingColorLine,"");_fs.default.writeFileSync(colorsPath,colorsContent);}}catch(error){progress.log(`Warning: Error cleaning notification resources: ${error.message}`,"warning");}}async function cleanupNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=_fs.default.readFileSync(manifestPath,"utf8");// Remove notification metadata entries
93
- const metadataNames=["default_notification_channel_id","default_notification_icon","default_notification_color","com.google.firebase.messaging.default_notification_channel_id","com.google.firebase.messaging.default_notification_icon","com.google.firebase.messaging.default_notification_sound"];manifestContent=manifestContent.split("\n").filter(line=>{return!metadataNames.some(metadataName=>line.includes(`android:name="${metadataName}"`));}).join("\n");// Remove Push Notification Service
94
- const serviceRegex=/\s*<!--\s*Push Notification Service\s*-->\s*<service[^>]*android:name="[^"]*PushNotificationUtils"[\s\S]*?<\/service>/gi;manifestContent=manifestContent.replace(serviceRegex,"");// Clean up any standalone notification comments
95
- manifestContent=manifestContent.replace(/\s*<!--\s*Default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Firebase default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Push Notification Service\s*-->/gi,"");_fs.default.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification metadata: ${error.message}`,"warning");}}async function cleanupNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp","xml"];const audioFormats=["mp3","wav","ogg"];// Define notification icons to clean up
96
- const notificationIcons=["ic_notification","ic_notification_large"];// Define notification sounds to clean up
97
- const notificationSounds=["notification_sound_default","notification_sound_urgent"];// Remove existing notification icons
98
- for(const icon of notificationIcons){for(const format of imageFormats){const iconPath=`${destPath}/drawable/${icon}.${format}`;if(_fs.default.existsSync(iconPath)){_fs.default.unlinkSync(iconPath);}}}// Remove existing notification sounds
99
- const rawDir=`${destPath}/raw`;if(_fs.default.existsSync(rawDir)){for(const sound of notificationSounds){for(const format of audioFormats){const soundPath=`${rawDir}/${sound}.${format}`;if(_fs.default.existsSync(soundPath)){_fs.default.unlinkSync(soundPath);}}}}}catch(error){progress.log(`Warning: Error cleaning notification assets: ${error.message}`,"warning");}}// Legacy function for backward compatibility
100
- // async function installApp(ADB_PATH, androidConfig, buildOptimisation, buildType = "debug") {
101
- // await buildApp(ADB_PATH, androidConfig, buildOptimisation, buildType)
102
- // await launchApp(ADB_PATH, buildType, { type: "emulator" })
103
- // }
104
- async function createAABConfig(androidConfig){// Create AAB configuration based on WebView config with defaults applied
105
- const aabConfig={// Apply defaults here in buildAppAndroid.js
106
- projectPath:androidConfig.projectPath||DEFAULT_PROJECT_PATH,deploymentPath:DEFAULT_DEPLOYMENT_PATH,oldProjectName:DEFAULT_OLD_PROJECT_NAME,overwriteExisting:androidConfig.overwriteExisting!==undefined?androidConfig.overwriteExisting:DEFAULT_OVERWRITE_EXISTING,// Required field - use configured value or derive from other config
107
- newProjectName:androidConfig.newProjectName||androidConfig.appName||androidConfig.packageName?.split(".").pop()||"catalystapp",// IMPORTANT: Pass packageName for renaming
108
- packageName:androidConfig.packageName||null,// AAB specific settings
109
- createSignedAAB:true,outputPath:androidConfig.outputPath||`${process.env.PWD}/build-output`};// Add keystore configuration if available
110
- if(androidConfig.keystoreConfig){aabConfig.keystoreConfig=androidConfig.keystoreConfig;}else if(androidConfig.keystore){// Map old keystore format to new format
111
- aabConfig.keystoreConfig={keyAlias:androidConfig.keystore.alias||"release",storePassword:androidConfig.keystore.storePassword,keyPassword:androidConfig.keystore.keyPassword,validityYears:25,organizationInfo:{companyName:androidConfig.keystore.organizationName||"YourCompany",city:androidConfig.keystore.city||"YourCity",state:androidConfig.keystore.state||"YourState",countryCode:androidConfig.keystore.countryCode||"US"}};}// Log the configuration being used
112
- progress.log(`AAB Configuration:`,"info");progress.log(` Project Path: ${aabConfig.projectPath} ${aabConfig.projectPath===DEFAULT_PROJECT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Deployment Path: ${aabConfig.deploymentPath} ${aabConfig.deploymentPath===DEFAULT_DEPLOYMENT_PATH?"(default)":"(configured)"}`,"info");progress.log(` Old Project Name: ${aabConfig.oldProjectName} ${aabConfig.oldProjectName===DEFAULT_OLD_PROJECT_NAME?"(default)":"(configured)"}`,"info");progress.log(` New Project Name: ${aabConfig.newProjectName}`,"info");progress.log(` Overwrite Existing: ${aabConfig.overwriteExisting} ${aabConfig.overwriteExisting===DEFAULT_OVERWRITE_EXISTING?"(default)":"(configured)"}`,"info");progress.log(` Output Path: ${aabConfig.outputPath}`,"info");return aabConfig;}async function moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,appName){try{// Check if required environment variables are set
113
- if(!process.env.PWD||!BUILD_OUTPUT_PATH){progress.log("Environment variables PWD or BUILD_OUTPUT_PATH not set, skipping APK move","warning");return null;}const currentDate=new Date().toLocaleDateString("en-GB").replace(/\//g,"-");// DD-MM-YYYY format
114
- const currentTime=new Date().toLocaleTimeString("en-US",{hour12:true,hour:"2-digit",minute:"2-digit",second:"2-digit"}).replace(/:/g,":");// HH-MM-SS AM/PM format
115
- let destinationApkFileName="";// Construct source and destination paths
116
- if(appName){destinationApkFileName=buildType==="release"?`${appName}-${currentTime}.release.apk`:`${appName}-${currentTime}.debug.apk`;}else{destinationApkFileName=buildType==="release"?`app-${currentTime}.release.apk`:`app-${currentTime}.debug.apk`;}const sourceApkFileName=buildType==="release"?`app.apk`:`app-debug.apk`;const sourceApkPath=_path.default.join(pwd,"androidProject","app","build","outputs","apk",buildType,sourceApkFileName);const destinationDir=_path.default.join(process.env.PWD,BUILD_OUTPUT_PATH,"native","android",currentDate,buildType);const destinationApkPath=_path.default.join(destinationDir,destinationApkFileName);// Check if source APK exists
117
- if(!_fs.default.existsSync(sourceApkPath)){progress.log(`APK not found at source path: ${sourceApkPath}`,"warning");return null;}// Create destination directory if it doesn't exist
118
- if(!_fs.default.existsSync(destinationDir)){_fs.default.mkdirSync(destinationDir,{recursive:true});}// Copy APK to destination
119
- _fs.default.copyFileSync(sourceApkPath,destinationApkPath);return destinationApkPath;}catch(error){progress.log(`Error moving APK: ${error.message}`,"error");return null;}}async function buildSignedAAB(androidConfig){progress.log("Building signed AAB for release...","info");try{// Generate webview config BEFORE creating AAB
120
- progress.log("Generating webview configuration for release...","info");try{const generateConfigCommand=`cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath}`;await(0,_utils.runInteractiveCommand)("sh",["-c",generateConfigCommand],{"BUILD SUCCESSFUL":""});progress.log("Webview config generated successfully","success");}catch(configError){progress.log(`Warning: Webview config generation failed: ${configError.message}`,"warning");throw new Error("Webview config generation is required for release builds");}// Create AAB configuration directly without temporary file
121
- const aabConfig=await createAABConfig(androidConfig);// Call the AAB builder directly with config object
122
- await(0,_renameAndroidProject.buildAndroidAAB)(aabConfig);progress.log("Signed AAB build completed successfully!","success");}catch(error){throw new Error("Error building signed AAB: "+error.message);}}async function buildAndroidApp(){// Initialize androidConfig outside try block to ensure it's available in catch
123
- let androidConfig=null;let targetDevice=null;try{// Initialize configuration
124
- progress.start("config");const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=await initializeConfig();androidConfig=WEBVIEW_CONFIG.android;const buildType=androidConfig.buildType||"debug";const buildOptimisation=!!androidConfig.buildOptimisation||false;progress.complete("config");// Validate tools and get paths
125
- progress.start("tools");const{ADB_PATH,EMULATOR_PATH}=await validateAndroidTools(androidConfig);progress.complete("tools");// Device detection and setup (skip for release builds)
126
- if(buildType!=="release"){progress.start("emulator");// Step 1: Check for physical device first
127
- const physicalDevice=await detectPhysicalDevice(ADB_PATH);if(physicalDevice){progress.log(`Found physical device: ${physicalDevice.model} (${physicalDevice.id})`,"success");// Test if we can install on physical device
128
- const canInstallOnPhysical=await testPhysicalDeviceInstallation(ADB_PATH,physicalDevice.id);if(canInstallOnPhysical){// Use physical device - skip emulator entirely
129
- targetDevice={type:"physical",id:physicalDevice.id,model:physicalDevice.model};progress.log(`Using physical device: ${physicalDevice.model}`,"success");}else{// Physical device failed, fallback to emulator
130
- progress.log("Physical device installation test failed, falling back to emulator","warning");targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}}else{// No physical device, use emulator (current behavior)
131
- targetDevice=await handleEmulatorSetup(ADB_PATH,EMULATOR_PATH,androidConfig);}progress.complete("emulator");}else{progress.log("Skipping device setup for release build","info");}// Copy build assets
132
- progress.start("copyAssets");await copyBuildAssets(androidConfig,buildOptimisation);await copySplashscreenAssets();await copyOfflinePage();await copyIconAssets();await configureAppName(androidConfig);const pluginConfig=(0,_internalPluginUtils.resolvePluginConfig)(WEBVIEW_CONFIG);(0,_pluginComposerAndroid.composeAndroidPlugins)({corePluginsRoot:(0,_internalPluginUtils.resolveInternalPluginsRoot)(catalystCorePath),androidProjectPath:`${pwd}/androidProject`,pluginConfig,log:(message,status="info")=>progress.log(message,status)});await processNotifications(WEBVIEW_CONFIG);progress.log(`Build optimization: ${buildOptimisation?"Enabled":"Disabled"}`,"info");progress.complete("copyAssets");// Build based on type
133
- let movedApkPath=null;if(buildType==="release"){// Build signed AAB for release
134
- progress.start("aab");await buildSignedAAB(androidConfig);progress.complete("aab");// Move APK to output directory
135
- movedApkPath=await moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,androidConfig.appName);}else{// Install debug app for development
136
- progress.start("build");await buildApp(ADB_PATH,androidConfig,buildOptimisation,targetDevice);await launchApp(ADB_PATH,buildType,targetDevice);progress.complete("build");// Move APK to output directory
137
- movedApkPath=await moveApkToOutputPath(buildType,BUILD_OUTPUT_PATH,androidConfig.appName);}// Print build summary
138
- const summaryItems=["Build completed successfully:",{text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`SDK Path: ${androidConfig.sdkPath}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Build Optimization: ${buildOptimisation?"Enabled":"Disabled"}`,indent:1,prefix:"├─ ",color:"gray"}];if(buildType==="release"){// Add APK path for release builds
139
- // Show moved APK location if move was successful
140
- if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}summaryItems.push({text:`Output: Signed AAB generated in build-output/`,indent:1,prefix:"└─ ",color:"green"});}else{// Add APK path for debug builds
141
- // Show moved APK location if move was successful
142
- if(movedApkPath){summaryItems.push({text:`APK Build Location: ${movedApkPath}`,indent:1,prefix:"├─ ",color:"green"});}if(targetDevice&&targetDevice.type==="physical"){summaryItems.push({text:`Target Device: ${targetDevice.model} (Physical)`,indent:1,prefix:"└─ ",color:"green"});}else{summaryItems.push({text:`Target Device: ${androidConfig.emulatorName} (Emulator)`,indent:1,prefix:"└─ ",color:"gray"});}}progress.printTreeContent("Build Summary",summaryItems);process.exit(0);}catch(error){if(progress.currentStep){progress.fail(progress.currentStep.id,error.message);const troubleshootingItems=["Build failed. Please try the following steps:",{text:"Check if Android SDK is properly configured",indent:1,prefix:"├─ ",color:"yellow"},{text:"Verify build assets exist in the source directory",indent:1,prefix:"├─ ",color:"yellow"}];// Use androidConfig safely with null checks
143
- const buildType=androidConfig?.buildType||"debug";// Device-specific troubleshooting
144
- if(buildType!=="release"){// Check if error is related to device issues
145
- if(error.message.includes("physical device")||error.message.includes("installation test")){troubleshootingItems.push("\nPhysical Device Issues:",{text:"Enable Developer Options on your device",indent:1,prefix:"├─ ",color:"yellow"},{text:"Enable USB Debugging in Developer Options",indent:1,prefix:"├─ ",color:"yellow"},{text:'Accept the "Allow USB Debugging" prompt on your device',indent:1,prefix:"├─ ",color:"yellow"},{text:"Try disconnecting and reconnecting your device",indent:1,prefix:"├─ ",color:"yellow"},{text:'Check if "adb devices" shows your device as authorized',indent:1,prefix:"├─ ",color:"yellow"});}if(buildType==="release"){troubleshootingItems.push({text:"Verify keystore configuration for release builds",indent:1,prefix:"├─ ",color:"yellow"},{text:"Check that keystore passwords are properly set",indent:1,prefix:"├─ ",color:"yellow"});}else{troubleshootingItems.push({text:"Verify that the emulator exists and is working",indent:1,prefix:"├─ ",color:"yellow"},{text:"If using physical device, ensure it stays connected",indent:1,prefix:"├─ ",color:"yellow"});}}else{troubleshootingItems.push({text:"Verify keystore configuration for release builds",indent:1,prefix:"├─ ",color:"yellow"},{text:"Check that keystore passwords are properly set",indent:1,prefix:"├─ ",color:"yellow"});}troubleshootingItems.push({text:'Run "npm run setupEmulator:android" to reconfigure Android settings',indent:1,prefix:"└─ ",color:"yellow"},"\nVerify Configuration:");// Add configuration details only if androidConfig is available
146
- if(androidConfig){troubleshootingItems.push({text:`Build Type: ${buildType}`,indent:1,prefix:"├─ ",color:"gray"},{text:`Android SDK Path: ${androidConfig.sdkPath||"Not configured"}`,indent:1,prefix:"├─ ",color:"gray"});if(buildType!=="release"){// Show device information if available
147
- if(targetDevice){troubleshootingItems.push({text:`Target Device: ${targetDevice.type==="physical"?`${targetDevice.model} (Physical)`:`${androidConfig.emulatorName} (Emulator)`}`,indent:1,prefix:"└─ ",color:"gray"});}else{troubleshootingItems.push({text:`Selected Emulator: ${androidConfig.emulatorName||"Not configured"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:`Output Path: ${androidConfig.outputPath||"build-output/"}`,indent:1,prefix:"└─ ",color:"gray"});}}else{troubleshootingItems.push({text:"Configuration could not be loaded",indent:1,prefix:"├─ ",color:"red"},{text:"Check if config/config.json exists and has valid Android configuration",indent:1,prefix:"└─ ",color:"red"});}progress.printTreeContent("Troubleshooting Guide",troubleshootingItems);}process.exit(1);}}// Execute the main build process
148
- buildAndroidApp();
1
+ "use strict";const{createAndroidBuild,pwd}=require("./buildAndroid/index.js");const{WEBVIEW_CONFIG,BUILD_OUTPUT_PATH}=require(`${process.cwd()}/config/config.json`);async function main(){const build=createAndroidBuild({WEBVIEW_CONFIG,BUILD_OUTPUT_PATH});const{buildAndroidApp}=build;try{await buildAndroidApp();}catch(error){process.exit(1);}process.exit(0);}if(require.main===module){main();}// Legacy re-exports for any tooling that imports from this file directly
2
+ module.exports={createAndroidBuild,pwd};