catalyst-core 1.1.0 → 1.2.0-canary.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/bin/catalyst.js +54 -6
  2. package/changelog.md +27 -1
  3. package/dist/errors/registry.js +306 -18
  4. package/dist/errors-index.json +273 -70
  5. package/dist/native/androidProject/app/build.gradle.kts +53 -79
  6. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
  10. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
  11. package/dist/native/androidSetup.js +76 -170
  12. package/dist/native/androidToolchain.js +642 -0
  13. package/dist/native/appIcon.js +100 -0
  14. package/dist/native/buildAndroid/assets.js +29 -5
  15. package/dist/native/buildAndroid/build.js +110 -223
  16. package/dist/native/buildAndroid/config.js +57 -65
  17. package/dist/native/buildAndroid/devices.js +176 -0
  18. package/dist/native/buildAndroid/index.js +271 -46
  19. package/dist/native/buildAndroid/output.js +43 -0
  20. package/dist/native/buildAndroid/signing.js +456 -0
  21. package/dist/native/buildAppAndroid.js +27 -7
  22. package/dist/native/buildAppIos.js +139 -20
  23. package/dist/native/buildIos/assets.js +76 -4
  24. package/dist/native/buildIos/build.js +31 -181
  25. package/dist/native/buildIos/config.js +126 -11
  26. package/dist/native/buildIos/index.js +354 -34
  27. package/dist/native/buildIos/localhostCert.js +87 -0
  28. package/dist/native/buildIos/output.js +28 -0
  29. package/dist/native/buildIos/signing.js +596 -0
  30. package/dist/native/credentials.js +309 -0
  31. package/dist/native/exportCredentials.js +51 -0
  32. package/dist/native/iosToolchain.js +583 -0
  33. package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
  34. package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
  35. package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
  36. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
  37. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
  38. package/dist/native/localState.js +148 -0
  39. package/dist/native/nativeConfig.js +621 -0
  40. package/dist/native/releaseOutput.js +53 -0
  41. package/dist/native/renameAndroidProject.js +154 -369
  42. package/dist/native/setupEmulatorIos.js +58 -275
  43. package/dist/native/setupServer.js +520 -124
  44. package/dist/native/utils.js +418 -115
  45. package/dist/vite/resolveDevServerConfig.js +21 -0
  46. package/mcp_v2/knowledge-base.json +13 -11
  47. package/mcp_v2/test/config.test.ts +40 -0
  48. package/mcp_v2/tools/build.js +80 -104
  49. package/mcp_v2/tools/config.js +181 -174
  50. package/package.json +1 -1
  51. package/test/cli/nativeFlags.test.ts +97 -0
  52. package/test/native/androidDevices.test.ts +230 -0
  53. package/test/native/androidElfAlignment.test.ts +148 -0
  54. package/test/native/androidSigning.test.ts +646 -0
  55. package/test/native/androidToolchain.test.ts +766 -0
  56. package/test/native/appIcon.test.ts +124 -0
  57. package/test/native/credentials.test.ts +241 -0
  58. package/test/native/interaction.test.ts +179 -0
  59. package/test/native/iosConfigPhase.test.ts +611 -0
  60. package/test/native/iosSigning.test.ts +1060 -0
  61. package/test/native/iosToolchain.test.ts +821 -0
  62. package/test/native/localState.test.ts +80 -0
  63. package/test/native/nativeConfig.test.ts +602 -0
  64. package/test/native/setupServer.test.ts +529 -0
  65. package/test/native/utils.test.ts +102 -127
  66. package/test/vite/resolveDevServerConfig.test.js +25 -0
  67. package/dist/native/androidProject/keystore/Untitled +0 -0
  68. package/dist/native/androidProject/keystore/release-key.jks +0 -0
  69. package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
@@ -52,6 +52,9 @@ class SimpleProgress {
52
52
  const mark = type === "error" ? _theme.t.bad(_theme.glyph.fail) : type === "warning" ? _theme.t.warn(_theme.glyph.warn) : type === "success" ? _theme.t.ok(_theme.glyph.done) : _theme.t.dim(_theme.glyph.info);
53
53
  console.log(`${_theme.GUTTER}${_theme.t.dim(_theme.glyph.pipe)} ${mark} ${message}`);
54
54
  }
55
+
56
+ // Gradle's current task; buildApp:android shows it on the spinner's row.
57
+ status() {}
55
58
  printTreeContent(title, items) {
56
59
  this.ensureHeader();
57
60
  console.log(`\n${_theme.GUTTER}${_theme.t.bold(title)}`);
@@ -67,238 +70,109 @@ class SimpleProgress {
67
70
  console.log("");
68
71
  }
69
72
  }
70
- const progress = new SimpleProgress("Android AAB Builder");
71
- async function createKeystore(projectPaths, androidConfig) {
72
- const {
73
- newProjectPath
74
- } = projectPaths;
75
- const {
76
- keystoreConfig
77
- } = androidConfig;
78
- if (!keystoreConfig) {
79
- progress.log("Keystore configuration not found, skipping keystore creation", "info");
80
- return null;
81
- }
82
- progress.log("Checking keystore configuration...", "info");
83
-
84
- // Validate keystore passwords are not placeholder values
85
- if (keystoreConfig.storePassword === "your_store_password" || keystoreConfig.keyPassword === "your_key_password" || keystoreConfig.storePassword === "your_password" || keystoreConfig.keyPassword === "your_password") {
86
- throw new Error('Please update keystore passwords in config file. Do not use placeholder values like "your_store_password" or "your_key_password".');
87
- }
88
-
89
- // Define keystore path
90
- const keystorePath = _path.default.join(newProjectPath, "app", "keystore.jks");
91
-
92
- // Check if keystore already exists
93
- if (_fs.default.existsSync(keystorePath)) {
94
- progress.log("Keystore already exists, verifying accessibility...", "info");
95
-
96
- // Test keystore accessibility
97
- try {
98
- const testCommand = `keytool -list -keystore "${keystorePath}" -storepass "${keystoreConfig.storePassword}" -alias "${keystoreConfig.keyAlias}"`;
99
- (0, _utils.runCommand)(testCommand);
100
- progress.log("Existing keystore verified successfully", "success");
101
- return keystorePath;
102
- } catch (testError) {
103
- progress.log("Existing keystore verification failed, creating new one...", "warning");
104
- // Remove the faulty keystore
105
- _fs.default.unlinkSync(keystorePath);
106
- }
107
- }
108
- progress.log("Creating new keystore...", "info");
109
- try {
110
- // Validate required keystore configuration
111
- const requiredFields = ["keyAlias", "storePassword", "keyPassword", "organizationInfo"];
112
- for (const field of requiredFields) {
113
- if (!keystoreConfig[field]) {
114
- throw new Error(`Missing required keystore config field: ${field}`);
115
- }
116
- }
117
- const {
118
- organizationInfo
119
- } = keystoreConfig;
120
- const requiredOrgFields = ["companyName", "city", "state", "countryCode"];
121
- for (const field of requiredOrgFields) {
122
- if (!organizationInfo[field]) {
123
- throw new Error(`Missing required organization info field: ${field}`);
124
- }
125
- }
126
-
127
- // Create keystore directory if it doesn't exist
128
- const keystoreDir = _path.default.dirname(keystorePath);
129
- if (!_fs.default.existsSync(keystoreDir)) {
130
- _fs.default.mkdirSync(keystoreDir, {
131
- recursive: true
132
- });
133
- }
134
73
 
135
- // Build the distinguished name (DN) for the certificate
136
- const dn = [`CN=${organizationInfo.companyName}`, `OU=${organizationInfo.department || organizationInfo.companyName}`, `O=${organizationInfo.organization || organizationInfo.companyName}`, `L=${organizationInfo.city}`, `ST=${organizationInfo.state}`, `C=${organizationInfo.countryCode}`].join(", ");
137
-
138
- // For PKCS12 keystores (default), store and key passwords must be the same
139
- // Use store password for both to avoid PKCS12 compatibility issues
140
- const effectiveKeyPassword = keystoreConfig.storePassword;
141
- if (keystoreConfig.keyPassword !== keystoreConfig.storePassword) {
142
- progress.log("Note: Using store password for key password (PKCS12 requirement)", "info");
143
- }
144
-
145
- // Generate keystore using keytool with PKCS12 format explicitly
146
- const validityDays = (keystoreConfig.validityYears || 25) * 365;
147
- const keystoreCommand = ["keytool", "-genkeypair", "-v", `-storetype PKCS12`, `-keystore "${keystorePath}"`, `-alias "${keystoreConfig.keyAlias}"`, `-keyalg RSA`, `-keysize 2048`, `-validity ${validityDays}`, `-storepass "${keystoreConfig.storePassword}"`, `-keypass "${effectiveKeyPassword}"`, `-dname "${dn}"`].join(" ");
148
- progress.log("Executing keytool command...", "info");
149
- (0, _utils.runCommand)(keystoreCommand);
150
-
151
- // Verify keystore was created
152
- if (!_fs.default.existsSync(keystorePath)) {
153
- throw new Error("Keystore file was not created successfully");
154
- }
74
+ // Run on its own, this script prints every step and detail line itself.
75
+ // buildApp:android passes its progress instead (buildAndroidAAB's `progress`
76
+ // option): the steps and details then ride on its "Build signed AAB" spinner
77
+ // like the rest of buildApp, printed as lines only with --verbose/--debug,
78
+ // while warnings and errors still print.
79
+ let progress = new SimpleProgress("Android AAB Builder");
80
+ const STEP_DETAILS = {
81
+ config: "reading the configuration",
82
+ validation: "checking the Android project",
83
+ backup: "backing up the project",
84
+ createTempDeployment: "copying the project to a build folder",
85
+ updateFileContents: "applying the app id and name",
86
+ createSignedAAB: "running Gradle",
87
+ finalizeAAB: "saving the AAB"
88
+ };
89
+
90
+ /**
91
+ * This script's progress calls, reported through a TerminalProgress that
92
+ * already has a step running. `info` and `success` become spinner detail
93
+ * (lines under --verbose), warnings and errors print. No summary box and no
94
+ * failure line: the caller prints its own summary and diagnostic.
95
+ */
96
+ function nestedProgress(outer) {
97
+ return {
98
+ currentStep: null,
99
+ start(stepId) {
100
+ this.currentStep = {
101
+ id: stepId
102
+ };
103
+ outer.log(STEP_DETAILS[stepId] || stepId, "info");
104
+ },
105
+ complete() {
106
+ this.currentStep = null;
107
+ },
108
+ fail() {
109
+ this.currentStep = null;
110
+ },
111
+ log(message, type = "info") {
112
+ outer.log(message, type);
113
+ },
114
+ status(detail) {
115
+ outer.status(detail);
116
+ },
117
+ printTreeContent() {}
118
+ };
119
+ }
155
120
 
156
- // Test the newly created keystore
157
- try {
158
- const testCommand = `keytool -list -keystore "${keystorePath}" -storepass "${keystoreConfig.storePassword}" -alias "${keystoreConfig.keyAlias}"`;
159
- (0, _utils.runCommand)(testCommand);
160
- progress.log("New keystore verified successfully", "success");
161
- } catch (testError) {
162
- throw new Error(`Created keystore failed verification: ${testError.message}`);
163
- }
164
- progress.log(`Keystore created at: ${keystorePath}`, "note");
165
- progress.log(`Key alias: ${keystoreConfig.keyAlias}`, "info");
166
- progress.log(`Validity: ${keystoreConfig.validityYears || 25} years`, "info");
167
- progress.log(`Store type: PKCS12`, "info");
168
- return keystorePath;
169
- } catch (error) {
170
- throw new Error(`Failed to create keystore: ${error.message}`);
171
- }
121
+ /** "> Task :app:bundleRelease" becomes "app:bundleRelease"; null otherwise. */
122
+ function gradleTask(line) {
123
+ const match = /^>\s*Task\s+:(\S+)/.exec(String(line).trim());
124
+ return match ? match[1] : null;
172
125
  }
173
- async function createSignedAAB(projectPaths, androidConfig, keystorePath) {
126
+
127
+ /**
128
+ * Generate the release webview config and build the signed bundle inside the
129
+ * temp copy. Two Gradle runs: the manifest's allowBackup placeholder is read
130
+ * from webview_config.properties while Gradle configures the project, so the
131
+ * file must exist before bundleRelease is configured.
132
+ *
133
+ * Signing and version values arrive in `gradle.env` as ORG_GRADLE_PROJECT_*
134
+ * variables; nothing secret is put on the command line.
135
+ */
136
+ async function createSignedAAB(projectPaths, androidConfig) {
174
137
  const {
175
138
  newProjectPath
176
139
  } = projectPaths;
177
140
  const {
178
- keystoreConfig
141
+ gradle
179
142
  } = androidConfig;
180
- if (!keystoreConfig || !keystorePath) {
181
- throw new Error("Keystore configuration and keystore path are required for signed AAB creation");
143
+ if (!gradle || !gradle.configPath || !gradle.env) {
144
+ throw new Error("A resolved config and upload key are required to build the signed AAB");
182
145
  }
183
146
  progress.log("Building signed Android App Bundle (AAB)...", "info");
184
- try {
185
- // Verify keystore exists
186
- if (!_fs.default.existsSync(keystorePath)) {
187
- throw new Error(`Keystore not found at: ${keystorePath}`);
188
- }
189
-
190
- // Get absolute path for the project directory
191
- const absoluteProjectPath = _path.default.resolve(newProjectPath);
192
-
193
- // Change to project directory
194
- const originalCwd = process.cwd();
195
- process.chdir(absoluteProjectPath);
196
- progress.log(`Changed to project directory: ${absoluteProjectPath}`, "info");
197
-
198
- // Verify gradlew exists using absolute path
199
- const gradlewPath = _path.default.join(absoluteProjectPath, "gradlew");
200
- progress.log(`Looking for gradlew at: ${gradlewPath}`, "info");
201
- if (!_fs.default.existsSync(gradlewPath)) {
202
- // List files in the project directory for debugging
203
- try {
204
- const files = _fs.default.readdirSync(absoluteProjectPath);
205
- progress.log(`Files in project directory: ${files.join(", ")}`, "info");
206
-
207
- // Check if there's a gradlew file with different permissions or name
208
- const gradlewFiles = files.filter(file => file.toLowerCase().includes("gradle"));
209
- if (gradlewFiles.length > 0) {
210
- progress.log(`Found gradle-related files: ${gradlewFiles.join(", ")}`, "info");
211
- }
212
- } catch (listError) {
213
- progress.log(`Could not list directory contents: ${listError.message}`, "warning");
214
- }
215
- throw new Error(`gradlew not found at: ${gradlewPath}. Ensure this is a valid Android project.`);
216
- }
217
-
218
- // Make gradlew executable
219
- try {
220
- ;
221
- (0, _utils.runCommand)(`chmod +x ./gradlew`);
222
- } catch (chmodError) {
223
- progress.log(`Warning: Could not make gradlew executable: ${chmodError.message}`, "warning");
224
- }
225
- try {
226
- // Clean previous builds
227
- progress.log("Cleaning previous builds...", "info");
228
- try {
229
- ;
230
- (0, _utils.runCommand)("./gradlew clean");
231
- progress.log("Clean completed successfully", "success");
232
- } catch (cleanError) {
233
- progress.log(`Warning: Clean failed: ${cleanError.message}`, "warning");
234
- // Continue anyway as this might not be critical
235
- }
236
-
237
- // Build signed AAB with PKCS12-compatible parameters
238
- progress.log("Building signed AAB...", "info");
239
-
240
- // For PKCS12 keystores, use store password for both store and key
241
- const effectiveKeyPassword = keystoreConfig.storePassword;
242
- const bundleCommand = ["./gradlew bundleRelease --console=plain", `-Pandroid.injected.signing.store.file="${keystorePath}"`, `-Pandroid.injected.signing.store.password="${keystoreConfig.storePassword}"`, `-Pandroid.injected.signing.key.alias="${keystoreConfig.keyAlias}"`, `-Pandroid.injected.signing.key.password="${effectiveKeyPassword}"`].join(" ");
243
-
244
- // Never log this command as-is: it carries the keystore and key
245
- // passwords, which would put them in the terminal and in CI logs.
246
- progress.log("Executing: ./gradlew bundleRelease (signing args redacted)", "info");
247
- (0, _utils.runCommand)(bundleCommand);
248
- progress.log("Bundle build completed", "success");
249
-
250
- // Find the generated AAB file
251
- const aabPath = _path.default.join(newProjectPath, "app", "build", "outputs", "bundle", "release", "app-release.aab");
252
- progress.log(`Looking for AAB file at: ${aabPath}`, "info");
253
- if (!_fs.default.existsSync(aabPath)) {
254
- // Try to find AAB files in the build output directory
255
- const bundleDir = _path.default.join(newProjectPath, "app", "build", "outputs", "bundle");
256
- progress.log(`AAB not found at expected location. Checking bundle directory: ${bundleDir}`, "warning");
257
- if (_fs.default.existsSync(bundleDir)) {
258
- try {
259
- const findAabCommand = `find "${bundleDir}" -name "*.aab" -type f`;
260
- const foundAabs = (0, _utils.runCommand)(findAabCommand);
261
- if (foundAabs.trim()) {
262
- progress.log(`Found AAB files: ${foundAabs.trim()}`, "info");
263
- } else {
264
- progress.log("No AAB files found in bundle directory", "warning");
265
- }
266
- } catch (findError) {
267
- progress.log(`Error searching for AAB files: ${findError.message}`, "warning");
268
- }
269
- }
270
- throw new Error("Signed AAB file was not generated successfully");
271
- }
272
-
273
- // Get file size
274
- const stats = _fs.default.statSync(aabPath);
275
- const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
276
- progress.log(`Signed AAB created successfully!`, "success");
277
- progress.log(`AAB location: ${aabPath}`, "info");
278
- progress.log(`AAB size: ${fileSizeMB} MB`, "info");
279
-
280
- // Optionally copy AAB to a more accessible location
281
- const outputDir = _path.default.join(newProjectPath, "release");
282
- if (!_fs.default.existsSync(outputDir)) {
283
- _fs.default.mkdirSync(outputDir, {
284
- recursive: true
285
- });
286
- }
287
- const outputAabPath = _path.default.join(outputDir, `${androidConfig.newProjectName}-release.aab`);
288
- _fs.default.copyFileSync(aabPath, outputAabPath);
289
- progress.log(`AAB copied to: ${outputAabPath}`, "success");
290
- return {
291
- aabPath,
292
- outputAabPath,
293
- fileSize: fileSizeMB
294
- };
295
- } finally {
296
- // Restore original working directory
297
- process.chdir(originalCwd);
298
- }
299
- } catch (error) {
300
- throw new Error(`Failed to create signed AAB: ${error.message}`);
301
- }
147
+ const absoluteProjectPath = _path.default.resolve(newProjectPath);
148
+ const gradlewPath = _path.default.join(absoluteProjectPath, "gradlew");
149
+ if (!_fs.default.existsSync(gradlewPath)) {
150
+ throw new Error(`gradlew not found at: ${gradlewPath}. Ensure this is a valid Android project.`);
151
+ }
152
+ _fs.default.chmodSync(gradlewPath, 0o755);
153
+ const common = [`-PconfigPath=${gradle.configPath}`, "-PcatalystBuildType=release", "--console=plain"];
154
+ const run = args => (0, _utils.runInteractiveCommand)(gradlewPath, args, {}, {
155
+ env: gradle.env,
156
+ cwd: absoluteProjectPath,
157
+ onLine: line => progress.status(gradleTask(line))
158
+ });
159
+ progress.log("Generating the release webview configuration...", "info");
160
+ await run(["generateWebViewConfig", `-PbuildOptimisation=${Boolean(gradle.buildOptimisation)}`, ...common]);
161
+ progress.log("Building signed AAB (clean bundleRelease)...", "info");
162
+ await run(["clean", "bundleRelease", ...common]);
163
+ progress.log("Bundle build completed", "success");
164
+ const aabPath = _path.default.join(newProjectPath, "app", "build", "outputs", "bundle", "release", "app-release.aab");
165
+ if (!_fs.default.existsSync(aabPath)) {
166
+ throw new Error(`Signed AAB file was not generated at ${aabPath}`);
167
+ }
168
+ const stats = _fs.default.statSync(aabPath);
169
+ const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
170
+ progress.log(`Signed AAB created (${fileSizeMB} MB)`, "success");
171
+ return {
172
+ aabPath,
173
+ outputAabPath: aabPath,
174
+ fileSize: fileSizeMB
175
+ };
302
176
  }
303
177
  async function initializeConfig(configPath) {
304
178
  if (!configPath) {
@@ -539,7 +413,8 @@ async function createTempDeploymentProject(projectPaths, androidConfig) {
539
413
  progress.log("Creating temporary deployment project...", "info");
540
414
 
541
415
  // Create a temporary directory based on package name (if available) or timestamp
542
- const tempDirSuffix = packageName ? packageName.replace(/\./g, "_") : Date.now().toString();
416
+ // Only [A-Za-z0-9_] in the name: the path reaches shell commands below.
417
+ const tempDirSuffix = packageName ? packageName.replace(/[^A-Za-z0-9_]/g, "_") : Date.now().toString();
543
418
  const tempDir = _path.default.join(_path.default.dirname(oldProjectPath), `temp_build_${tempDirSuffix}`);
544
419
 
545
420
  // Remove existing temp directory with same name if it exists
@@ -604,10 +479,13 @@ async function createTempDeploymentProject(projectPaths, androidConfig) {
604
479
  path: "app/src/main/AndroidManifest.xml",
605
480
  desc: "Android manifest"
606
481
  }];
482
+
483
+ // Both the Groovy and the Kotlin DSL names are listed, so one of each
484
+ // pair is always absent: info, not a warning.
607
485
  filesToCheck.forEach(file => {
608
486
  const fullPath = _path.default.join(tempDir, file.path);
609
487
  const exists = _fs.default.existsSync(fullPath);
610
- progress.log(` ${file.desc}: ${exists ? "found" : "not found"}`, exists ? "info" : "warning");
488
+ progress.log(` ${file.desc}: ${exists ? "found" : "not found"}`, "info");
611
489
  });
612
490
 
613
491
  // List all files in temp directory for debugging
@@ -637,8 +515,8 @@ async function createTempDeploymentProject(projectPaths, androidConfig) {
637
515
  for (const dir of dirsToRename) {
638
516
  const newDirName = dir.split(oldProjectName).join(newProjectName);
639
517
  if (dir !== newDirName) {
640
- ;
641
- (0, _utils.runCommand)(`mv "${dir}" "${newDirName}"`);
518
+ // fs, not a shell: newProjectName comes from the app's config.
519
+ _fs.default.renameSync(dir, newDirName);
642
520
  progress.log(`Renamed subdirectory: ${_path.default.basename(dir)} → ${_path.default.basename(newDirName)}`, "success");
643
521
  }
644
522
  }
@@ -655,29 +533,21 @@ async function finalizeAABAndCleanup(projectPaths, androidConfig, aabResult, tem
655
533
  } = projectPaths;
656
534
  progress.log("Finalizing AAB and cleaning up temporary files...", "info");
657
535
  try {
658
- // Create final output directory next to original project
659
- const outputDir = androidConfig.outputPath || _path.default.join(_path.default.dirname(oldProjectPath), "build-output");
536
+ // buildApp:android passes the exact destination
537
+ // (build-output/android/<version>-<build>/<name>.aab). Run on its
538
+ // own, this script keeps its dated name in the output folder.
539
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").split("T")[0];
540
+ const finalAabPath = androidConfig.finalAabPath || _path.default.join(androidConfig.outputPath || _path.default.join(_path.default.dirname(oldProjectPath), "build-output"), `${androidConfig.newProjectName}-release-${timestamp}.aab`);
541
+ const outputDir = _path.default.dirname(finalAabPath);
542
+ const finalAabName = _path.default.basename(finalAabPath);
660
543
  if (!_fs.default.existsSync(outputDir)) {
661
544
  _fs.default.mkdirSync(outputDir, {
662
545
  recursive: true
663
546
  });
664
547
  progress.log(`Created output directory: ${outputDir}`, "success");
665
548
  }
666
-
667
- // Copy AAB to final location with descriptive name
668
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-").split("T")[0];
669
- const finalAabName = `${androidConfig.newProjectName}-release-${timestamp}.aab`;
670
- const finalAabPath = _path.default.join(outputDir, finalAabName);
671
549
  _fs.default.copyFileSync(aabResult.outputAabPath, finalAabPath);
672
- progress.log(`AAB saved to: ${finalAabPath}`, "note");
673
-
674
- // Copy keystore to output directory for future use
675
- const keystorePath = _path.default.join(tempDeploymentPath, "app", "keystore.jks");
676
- if (_fs.default.existsSync(keystorePath)) {
677
- const finalKeystorePath = _path.default.join(outputDir, `${androidConfig.newProjectName}-keystore.jks`);
678
- _fs.default.copyFileSync(keystorePath, finalKeystorePath);
679
- progress.log(`Keystore copied to: ${finalKeystorePath}`, "success");
680
- }
550
+ progress.log(`AAB saved to: ${finalAabPath}`, "success");
681
551
 
682
552
  // Clean up temporary deployment directory
683
553
  await cleanupTempDeployment(tempDeploymentPath);
@@ -743,19 +613,21 @@ async function updateFileContents(projectPaths, androidConfig) {
743
613
  const newPackageDir = _path.default.dirname(newPackagePath);
744
614
  _fs.default.mkdirSync(newPackageDir, {
745
615
  recursive: true
746
- })
616
+ });
747
617
 
748
618
  // Move source files
749
- ;
750
- (0, _utils.runCommand)(`mv "${oldPackagePath}" "${newPackagePath}"`);
619
+ // fs, not a shell: the path is built from the app id.
620
+ _fs.default.renameSync(oldPackagePath, newPackagePath);
751
621
  progress.log(`Moved ${sourceSet} source files to: ${newPackagePath}`, "success");
752
622
  } else {
753
623
  progress.log(`Package path unchanged for ${sourceSet}; skipping source directory move`, "info");
754
624
  }
755
625
 
756
626
  // Update package declarations in source files
757
- const findCommand = `find "${newPackagePath}" -type f \\( -name "*.kt" -o -name "*.java" \\)`;
758
- const sourceFiles = (0, _utils.runCommand)(findCommand).trim().split("\n").filter(f => f.trim());
627
+ const sourceFiles = _fs.default.readdirSync(newPackagePath, {
628
+ recursive: true,
629
+ withFileTypes: true
630
+ }).filter(entry => entry.isFile() && /\.(kt|java)$/.test(entry.name)).map(entry => _path.default.join(entry.parentPath || entry.path, entry.name));
759
631
  let updatedCount = 0;
760
632
  for (const file of sourceFiles) {
761
633
  let content = _fs.default.readFileSync(file, "utf8");
@@ -812,8 +684,7 @@ async function updateFileContents(projectPaths, androidConfig) {
812
684
  for (const file of filesToRename) {
813
685
  const newFileName = file.split(oldProjectName).join(newProjectName);
814
686
  if (file !== newFileName) {
815
- ;
816
- (0, _utils.runCommand)(`mv "${file}" "${newFileName}"`);
687
+ _fs.default.renameSync(file, newFileName);
817
688
  progress.log(`Renamed file: ${_path.default.basename(file)} → ${_path.default.basename(newFileName)}`, "success");
818
689
  }
819
690
  }
@@ -850,7 +721,26 @@ async function updateFileContents(projectPaths, androidConfig) {
850
721
  throw new Error(`Error updating file contents: ${error.message}`);
851
722
  }
852
723
  }
853
- async function buildAndroidAAB(configPathOrConfig) {
724
+
725
+ /**
726
+ * Build the signed AAB in a renamed temp copy of the Android project.
727
+ *
728
+ * @param {string | object} configPathOrConfig - a config.json path, or the config object buildApp:android builds
729
+ * @param {object} [options]
730
+ * @param {object} [options.progress] - the caller's TerminalProgress, with a step running; see nestedProgress
731
+ */
732
+ async function buildAndroidAAB(configPathOrConfig, {
733
+ progress: outer
734
+ } = {}) {
735
+ const previous = progress;
736
+ if (outer) progress = nestedProgress(outer);
737
+ try {
738
+ return await runAABBuild(configPathOrConfig);
739
+ } finally {
740
+ progress = previous;
741
+ }
742
+ }
743
+ async function runAABBuild(configPathOrConfig) {
854
744
  let androidConfig;
855
745
  let projectPaths;
856
746
  let backupPath;
@@ -909,44 +799,19 @@ async function buildAndroidAAB(configPathOrConfig) {
909
799
  newProjectPath: tempDeploymentPath
910
800
  }, androidConfig);
911
801
  progress.complete("updateFileContents");
912
-
913
- // Create keystore if needed
914
- progress.start("createKeystore");
915
- const keystorePath = await createKeystore({
802
+ progress.start("createSignedAAB");
803
+ let aabResult = await createSignedAAB({
916
804
  ...projectPaths,
917
805
  newProjectPath: tempDeploymentPath
918
806
  }, androidConfig);
919
- progress.complete("createKeystore");
920
-
921
- // Create signed AAB if keystore was created/found and enabled in config
922
- let aabResult = null;
923
- if (keystorePath && androidConfig.createSignedAAB) {
924
- progress.start("createSignedAAB");
925
- aabResult = await createSignedAAB({
926
- ...projectPaths,
927
- newProjectPath: tempDeploymentPath
928
- }, androidConfig, keystorePath);
929
- progress.complete("createSignedAAB");
807
+ progress.complete("createSignedAAB");
930
808
 
931
- // Copy AAB to final location and cleanup temp deployment
932
- if (aabResult) {
933
- progress.start("finalizeAAB");
934
- aabResult = await finalizeAABAndCleanup(projectPaths, androidConfig, aabResult, tempDeploymentPath);
935
- progress.complete("finalizeAAB");
936
- }
937
- } else if (!androidConfig.createSignedAAB) {
938
- progress.log("Signed AAB creation skipped (not enabled in config)", "info");
939
- // DON'T Clean up temp deployment - keep it for verification
940
- progress.log(`Temp renamed project kept at: ${tempDeploymentPath}`, "info");
941
- // await cleanupTempDeployment(tempDeploymentPath)
942
- } else if (!keystorePath) {
943
- progress.log("Signed AAB creation skipped (no keystore available)", "warning");
944
- // DON'T Clean up temp deployment - keep it for verification
945
- progress.log(`Temp renamed project kept at: ${tempDeploymentPath}`, "info");
946
- // await cleanupTempDeployment(tempDeploymentPath)
947
- }
809
+ // Copy AAB to final location and cleanup temp deployment
810
+ progress.start("finalizeAAB");
811
+ aabResult = await finalizeAABAndCleanup(projectPaths, androidConfig, aabResult, tempDeploymentPath);
812
+ progress.complete("finalizeAAB");
948
813
 
949
- // Print completion summary
814
+ // Completion summary, standalone only: buildApp:android prints its own.
950
815
  progress.printTreeContent("Build Summary", ["Android AAB build completed successfully:", {
951
816
  text: `Original project: ${projectPaths.oldProjectPath}`,
952
817
  indent: 1,
@@ -972,11 +837,6 @@ async function buildAndroidAAB(configPathOrConfig) {
972
837
  indent: 1,
973
838
  prefix: "├─ ",
974
839
  color: "gray"
975
- }, {
976
- text: `Keystore: ${keystorePath ? "Created/Verified" : "Skipped"}`,
977
- indent: 1,
978
- prefix: "├─ ",
979
- color: "gray"
980
840
  }, {
981
841
  text: `Signed AAB: ${aabResult ? "Created" : "Skipped"}`,
982
842
  indent: 1,
@@ -1008,92 +868,17 @@ async function buildAndroidAAB(configPathOrConfig) {
1008
868
  prefix: "└─ ",
1009
869
  color: "gray"
1010
870
  }])]);
1011
- process.exit(0);
871
+ return aabResult;
1012
872
  } catch (error) {
1013
873
  // Clean up temp deployment on error
1014
874
  if (tempDeploymentPath) {
1015
875
  await cleanupTempDeployment(tempDeploymentPath);
1016
876
  }
1017
- if (progress.currentStep) {
1018
- progress.fail(progress.currentStep.id, error.message);
1019
- progress.printTreeContent("Troubleshooting Guide", ["Build operation failed. Please check the following:", {
1020
- text: "Verify config file exists and contains required android configuration",
1021
- indent: 1,
1022
- prefix: "├─ ",
1023
- color: "yellow"
1024
- }, {
1025
- text: "Ensure project path exists and is accessible",
1026
- indent: 1,
1027
- prefix: "├─ ",
1028
- color: "yellow"
1029
- }, {
1030
- text: "Check file/directory permissions",
1031
- indent: 1,
1032
- prefix: "├─ ",
1033
- color: "yellow"
1034
- }, {
1035
- text: "Verify no processes are using the project directory",
1036
- indent: 1,
1037
- prefix: "├─ ",
1038
- color: "yellow"
1039
- }, {
1040
- text: "Ensure sufficient disk space for project copy",
1041
- indent: 1,
1042
- prefix: "├─ ",
1043
- color: "yellow"
1044
- }, {
1045
- text: 'Update keystore passwords (do not use "your_store_password")',
1046
- indent: 1,
1047
- prefix: "└─ ",
1048
- color: "yellow"
1049
- }, "\nConfiguration Details:", {
1050
- text: `Config: ${typeof configPathOrConfig === "string" ? configPathOrConfig : "Direct object from buildAppAndroid.js"}`,
1051
- indent: 1,
1052
- prefix: "├─ ",
1053
- color: "gray"
1054
- }, {
1055
- text: `Project path: ${androidConfig?.projectPath || "Not loaded"}`,
1056
- indent: 1,
1057
- prefix: "├─ ",
1058
- color: "gray"
1059
- }, {
1060
- text: `Deployment path: ${androidConfig?.deploymentPath || "Not loaded"}`,
1061
- indent: 1,
1062
- prefix: "├─ ",
1063
- color: "gray"
1064
- }, {
1065
- text: `Old project name: ${androidConfig?.oldProjectName || "Not loaded"}`,
1066
- indent: 1,
1067
- prefix: "├─ ",
1068
- color: "gray"
1069
- }, {
1070
- text: `New project name: ${androidConfig?.newProjectName || "Not loaded"}`,
1071
- indent: 1,
1072
- prefix: "├─ ",
1073
- color: "gray"
1074
- }, {
1075
- text: `Overwrite existing: ${androidConfig?.overwriteExisting !== undefined ? androidConfig.overwriteExisting : "Not loaded"}`,
1076
- indent: 1,
1077
- prefix: "└─ ",
1078
- color: "gray"
1079
- }, "\nFor keystore issues, ensure your config has:", {
1080
- text: "Real passwords (not placeholder values)",
1081
- indent: 1,
1082
- prefix: "├─ ",
1083
- color: "cyan"
1084
- }, {
1085
- text: "Valid organization information",
1086
- indent: 1,
1087
- prefix: "├─ ",
1088
- color: "cyan"
1089
- }, {
1090
- text: "Consistent store and key passwords for PKCS12",
1091
- indent: 1,
1092
- prefix: "└─ ",
1093
- color: "cyan"
1094
- }]);
1095
- }
1096
- process.exit(1);
877
+
878
+ // The caller (buildApp:android) prints the full diagnostic, including
879
+ // Gradle's own error, from `error.output`.
880
+ if (progress.currentStep) progress.fail(progress.currentStep.id, error.message);
881
+ throw error;
1097
882
  }
1098
883
  }
1099
884