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
@@ -0,0 +1,456 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.PLAY_UPLOAD_KEY_RESET_URL = exports.CREDENTIALS_DIR = void 0;
7
+ exports.configHasPasswords = configHasPasswords;
8
+ exports.createUploadKeystore = createUploadKeystore;
9
+ exports.distinguishedName = distinguishedName;
10
+ exports.ensureGitignored = ensureGitignored;
11
+ exports.exportUploadCertificate = exportUploadCertificate;
12
+ exports.keyPaths = keyPaths;
13
+ exports.legacyKeystorePath = legacyKeystorePath;
14
+ exports.readKeystoreProperties = readKeystoreProperties;
15
+ exports.resolveAndroidSigning = resolveAndroidSigning;
16
+ exports.signingGradleEnv = signingGradleEnv;
17
+ exports.verifyKeystore = verifyKeystore;
18
+ exports.writeKeystoreProperties = writeKeystoreProperties;
19
+ var _child_process = require("child_process");
20
+ var _crypto = _interopRequireDefault(require("crypto"));
21
+ var _fs = _interopRequireDefault(require("fs"));
22
+ var _path = _interopRequireDefault(require("path"));
23
+ var _utils = require("../utils.js");
24
+ var _credentials = require("../credentials.js");
25
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
26
+ // Release signing for Android: one persistent upload key per app (#508).
27
+ //
28
+ // The key used to be generated inside the temp_build_<pkg> copy, which is
29
+ // wiped on every run, so every release was signed with a new key and Play
30
+ // rejected all but the first upload. It now lives in the app, outside git:
31
+ //
32
+ // <app>/credentials/android/upload.jks
33
+ // <app>/credentials/android/keystore.properties (0600: alias + passwords)
34
+ //
35
+ // CI gets the same folder from the CATALYST_ANDROID_CREDENTIALS secret
36
+ // (catalyst credentials:export android), through resolveCredentials.
37
+ // Passwords never go on a command line: keytool reads them with
38
+ // -storepass:env, Gradle through ORG_GRADLE_PROJECT_* variables. No code
39
+ // path here deletes or overwrites an existing key.
40
+
41
+ const CREDENTIALS_DIR = exports.CREDENTIALS_DIR = _path.default.join("credentials", "android");
42
+ const DEFAULT_ALIAS = "upload";
43
+ const DEFAULT_STORE_FILE = "upload.jks";
44
+ const GITIGNORE_ENTRY = "credentials/";
45
+ // Play Console Help, "Use Play App Signing": resetting a lost or compromised upload key.
46
+ const PLAY_UPLOAD_KEY_RESET_URL = exports.PLAY_UPLOAD_KEY_RESET_URL = "https://support.google.com/googleplay/android-developer/answer/9842756";
47
+ const BACKUP_REMINDER = "Back it up now: run catalyst credentials:export android and keep the value somewhere safe, such as a password manager. Play only accepts updates signed with this key.";
48
+
49
+ /** The key files in a credentials folder; keystore.properties may name another storeFile in the same folder. */
50
+ function keyPaths(dir) {
51
+ const propertiesFile = _path.default.join(dir, "keystore.properties");
52
+ const named = _path.default.basename(String(readKeystoreProperties(propertiesFile).storeFile || DEFAULT_STORE_FILE));
53
+ return {
54
+ dir,
55
+ storeFile: _path.default.join(dir, named),
56
+ propertiesFile
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Where releases before 1.1.1 left a copy of their keystore:
62
+ * <android.outputPath or build-output>/<name>-keystore.jks, with the name
63
+ * those releases used (newProjectName, then appName, then the last part of
64
+ * packageName). Pass WEBVIEW_CONFIG.android as written in config.json, not
65
+ * the resolved block: appId and appName overwrite packageName and appName.
66
+ *
67
+ * @param {Record<string, any>} [androidConfig]
68
+ * @param {object} [options]
69
+ * @param {string} [options.cwd]
70
+ */
71
+ function legacyKeystorePath(androidConfig = {}, {
72
+ cwd = process.cwd()
73
+ } = {}) {
74
+ const name = androidConfig.newProjectName || androidConfig.appName || androidConfig.packageName?.split(".").pop() || "catalystapp";
75
+ const outputPath = androidConfig.outputPath || _path.default.join(cwd, "build-output");
76
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - outputPath and the name come from the app's own config.
77
+ return _path.default.resolve(cwd, outputPath, `${name}-keystore.jks`);
78
+ }
79
+
80
+ /** Java .properties, the subset Catalyst writes: key=value lines, # comments. */
81
+ function readKeystoreProperties(file) {
82
+ const values = {};
83
+ if (!_fs.default.existsSync(file)) return values;
84
+ for (const line of _fs.default.readFileSync(file, "utf8").split(/\r?\n/)) {
85
+ const trimmed = line.trim();
86
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
87
+ const index = trimmed.search(/[=:]/);
88
+ if (index === -1) continue;
89
+ values[trimmed.slice(0, index).trim()] = trimmed.slice(index + 1).trim();
90
+ }
91
+ return values;
92
+ }
93
+
94
+ /** Write keystore.properties for a new key. Refuses to replace an existing file. */
95
+ function writeKeystoreProperties(file, {
96
+ keyAlias,
97
+ storePassword,
98
+ keyPassword
99
+ }) {
100
+ _fs.default.mkdirSync(_path.default.dirname(file), {
101
+ recursive: true,
102
+ mode: 0o700
103
+ });
104
+ const body = ["# Catalyst Android upload key. Keep this file and upload.jks out of git and backed up:", "# Play only accepts updates signed with this key.", "storeFile=upload.jks", `keyAlias=${keyAlias}`, `storePassword=${storePassword}`, `keyPassword=${keyPassword}`, ""].join("\n");
105
+ // "wx": fail rather than replace the passwords of a key that exists.
106
+ _fs.default.writeFileSync(file, body, {
107
+ mode: 0o600,
108
+ flag: "wx"
109
+ });
110
+ _fs.default.chmodSync(file, 0o600);
111
+ }
112
+
113
+ /** Append `credentials/` to the app's .gitignore unless something already covers it. */
114
+ function ensureGitignored(appRoot) {
115
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the local app directory; the file name is fixed.
116
+ const file = _path.default.join(appRoot, ".gitignore");
117
+ // One descriptor for the read and the append ("a+" creates the file when
118
+ // it is missing), so nothing can swap the file between the two.
119
+ const fd = _fs.default.openSync(file, "a+");
120
+ try {
121
+ const content = _fs.default.readFileSync(fd, "utf8");
122
+ const covered = content.split(/\r?\n/).map(line => line.trim()).some(line => ["credentials", "credentials/", "/credentials", "/credentials/"].includes(line));
123
+ if (covered) return false;
124
+ const prefix = content && !content.endsWith("\n") ? "\n" : "";
125
+ _fs.default.writeSync(fd, `${prefix}${GITIGNORE_ENTRY}\n`);
126
+ return true;
127
+ } finally {
128
+ _fs.default.closeSync(fd);
129
+ }
130
+ }
131
+ function generatePassword() {
132
+ return _crypto.default.randomBytes(24).toString("base64url");
133
+ }
134
+ function keytoolPath(env = process.env) {
135
+ if (env.JAVA_HOME) {
136
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - JAVA_HOME is the local JDK location.
137
+ const candidate = _path.default.join(env.JAVA_HOME, "bin", process.platform === "win32" ? "keytool.exe" : "keytool");
138
+ if (_fs.default.existsSync(candidate)) return candidate;
139
+ }
140
+ return "keytool";
141
+ }
142
+
143
+ /**
144
+ * keytool with the passwords handed over in its environment, never argv.
145
+ *
146
+ * @param {string[]} args
147
+ * @param {{ storePassword: string, keyPassword?: string }} passwords
148
+ * @param {{ env?: Record<string, string | undefined>, exec?: Function }} [options]
149
+ */
150
+ function runKeytool(args, passwords, {
151
+ env = process.env,
152
+ exec = _child_process.execFileSync
153
+ } = {}) {
154
+ const childEnv = {
155
+ ...env,
156
+ CATALYST_KEYTOOL_STOREPASS: passwords.storePassword
157
+ };
158
+ if (passwords.keyPassword !== undefined) childEnv.CATALYST_KEYTOOL_KEYPASS = passwords.keyPassword;
159
+ return exec(keytoolPath(env), args, {
160
+ env: childEnv,
161
+ encoding: "utf8",
162
+ stdio: ["ignore", "pipe", "pipe"]
163
+ });
164
+ }
165
+ function distinguishedName(organizationInfo = {}, commonName) {
166
+ const escape = value => String(value).replace(/([,\\+"<>;=])/g, "\\$1");
167
+ const org = organizationInfo.organization || organizationInfo.companyName;
168
+ const parts = [["CN", organizationInfo.companyName || commonName || "Catalyst App"], ["OU", organizationInfo.department], ["O", org], ["L", organizationInfo.city], ["ST", organizationInfo.state], ["C", organizationInfo.countryCode]];
169
+ return parts.filter(([, value]) => value).map(([key, value]) => `${key}=${escape(value)}`).join(", ");
170
+ }
171
+
172
+ /**
173
+ * Create a PKCS12 upload keystore. PKCS12 uses one password for the store
174
+ * and the key, so `password` is both. Never replaces an existing file.
175
+ *
176
+ * @param {string} storeFile
177
+ * @param {object} key
178
+ * @param {string} [key.keyAlias]
179
+ * @param {string} key.password
180
+ * @param {Record<string, string>} [key.organizationInfo] - from android.keystoreConfig
181
+ * @param {string} [key.commonName] - CN when organizationInfo has no companyName
182
+ * @param {number} [key.validityYears]
183
+ * @param {{ env?: Record<string, string | undefined>, exec?: Function }} [options]
184
+ */
185
+ function createUploadKeystore(storeFile, {
186
+ keyAlias = DEFAULT_ALIAS,
187
+ password,
188
+ organizationInfo,
189
+ commonName,
190
+ validityYears = 25
191
+ }, options = {}) {
192
+ if (_fs.default.existsSync(storeFile)) {
193
+ throw new Error(`Refusing to replace the existing upload key at ${storeFile}`);
194
+ }
195
+ _fs.default.mkdirSync(_path.default.dirname(storeFile), {
196
+ recursive: true,
197
+ mode: 0o700
198
+ });
199
+ const args = ["-genkeypair", "-storetype", "PKCS12", "-keystore", storeFile, "-alias", keyAlias, "-keyalg", "RSA", "-keysize", "2048", "-validity", String(validityYears * 365), "-storepass:env", "CATALYST_KEYTOOL_STOREPASS", "-keypass:env", "CATALYST_KEYTOOL_KEYPASS", "-dname", distinguishedName(organizationInfo, commonName)];
200
+ try {
201
+ runKeytool(args, {
202
+ storePassword: password,
203
+ keyPassword: password
204
+ }, options);
205
+ } catch (error) {
206
+ const detail = String(error.stderr || error.message || "").trim().split("\n")[0];
207
+ throw new Error(`keytool could not create the upload key: ${detail}`);
208
+ }
209
+ _fs.default.chmodSync(storeFile, 0o600);
210
+ return storeFile;
211
+ }
212
+
213
+ /**
214
+ * Fail before Gradle when the store, password or alias don't match.
215
+ *
216
+ * @param {{ storeFile: string, keyAlias: string, storePassword: string }} signing
217
+ * @param {{ env?: Record<string, string | undefined>, exec?: Function }} [options]
218
+ */
219
+ function verifyKeystore(signing, options = {}) {
220
+ try {
221
+ runKeytool(["-list", "-keystore", signing.storeFile, "-alias", signing.keyAlias, "-storepass:env", "CATALYST_KEYTOOL_STOREPASS"], {
222
+ storePassword: signing.storePassword
223
+ }, options);
224
+ } catch (error) {
225
+ if (error.code === "ENOENT") throw error;
226
+ const detail = String(error.stderr || error.stdout || error.message || "").trim().split("\n").pop();
227
+ throw (0, _utils.nativeError)("ANDROID-006", `keytool could not open ${signing.storeFile} with alias "${signing.keyAlias}"${detail ? ` (${detail})` : ""}`);
228
+ }
229
+ }
230
+
231
+ /** True when config.json carries keystore passwords, which belong in credentials/ or CI secrets. */
232
+ function configHasPasswords(androidConfig = {}) {
233
+ const blocks = [androidConfig.keystoreConfig, androidConfig.keystore].filter(Boolean);
234
+ return blocks.some(block => block.storePassword || block.keyPassword);
235
+ }
236
+ function legacyPasswords(androidConfig = {}) {
237
+ if (androidConfig.keystoreConfig?.storePassword) {
238
+ return {
239
+ keyAlias: androidConfig.keystoreConfig.keyAlias || "release",
240
+ password: androidConfig.keystoreConfig.storePassword
241
+ };
242
+ }
243
+ if (androidConfig.keystore?.storePassword) {
244
+ return {
245
+ keyAlias: androidConfig.keystore.alias || "release",
246
+ password: androidConfig.keystore.storePassword
247
+ };
248
+ }
249
+ return null;
250
+ }
251
+
252
+ /**
253
+ * Write the upload certificate as PEM (`keytool -exportcert -rfc`). Play asks
254
+ * for it when an upload key is registered or reset, and it identifies which
255
+ * key signed a bundle (#508).
256
+ *
257
+ * @param {{ storeFile: string, keyAlias: string, storePassword: string }} signing
258
+ * @param {string} destFile
259
+ * @param {{ env?: Record<string, string | undefined>, exec?: Function }} [options]
260
+ * @returns {string} destFile
261
+ */
262
+ function exportUploadCertificate(signing, destFile, options = {}) {
263
+ _fs.default.mkdirSync(_path.default.dirname(destFile), {
264
+ recursive: true
265
+ });
266
+ try {
267
+ runKeytool(["-exportcert", "-rfc", "-keystore", signing.storeFile, "-alias", signing.keyAlias, "-storepass:env", "CATALYST_KEYTOOL_STOREPASS", "-file", destFile], {
268
+ storePassword: signing.storePassword
269
+ }, options);
270
+ } catch (error) {
271
+ const detail = String(error.stderr || error.message || "").trim().split("\n")[0];
272
+ throw new Error(`keytool could not export the upload certificate: ${detail}`);
273
+ }
274
+ return destFile;
275
+ }
276
+
277
+ /**
278
+ * Work out which upload key signs this release.
279
+ *
280
+ * The key comes from credentials/android/ (upload.jks and keystore.properties),
281
+ * or in CI from the CATALYST_ANDROID_CREDENTIALS secret (resolveCredentials).
282
+ * When there is none, a person at a terminal may adopt a keystore an older
283
+ * release left in build-output/, or create a new key. Without a terminal no
284
+ * key is ever created, whatever the flags. A new key is refused for an app
285
+ * that is already on Google Play: its updates must be signed with the key
286
+ * Play knows, and only Play can reset that.
287
+ *
288
+ * Every password read is masked in GitHub Actions logs (maskSecret).
289
+ *
290
+ * @param {object} [options]
291
+ * @param {string} [options.appRoot]
292
+ * @param {Record<string, any>} [options.androidConfig] - WEBVIEW_CONFIG.android
293
+ * @param {Record<string, string | undefined>} [options.env]
294
+ * @param {boolean} [options.isTTY]
295
+ * @param {(question: string, options?: object) => Promise<string>} [options.prompt]
296
+ * @param {(message: string, type?: string) => void} [options.log]
297
+ * @param {string} [options.legacyKeystorePath] - build-output/<name>-keystore.jks from earlier releases
298
+ * @param {string} [options.commonName] - certificate CN for a new key
299
+ * @param {Function} [options.exec] - execFileSync stand-in, for tests
300
+ * @param {(value: string, options?: object) => unknown} [options.mask] - maskSecret stand-in, for tests
301
+ * @returns {Promise<{storeFile: string, storePassword: string, keyAlias: string, keyPassword: string, source: string, created: boolean, cleanup: () => void}>}
302
+ */
303
+ async function resolveAndroidSigning({
304
+ appRoot = process.cwd(),
305
+ androidConfig = {},
306
+ env = process.env,
307
+ isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY),
308
+ prompt = _utils.promptUser,
309
+ log = () => {},
310
+ legacyKeystorePath,
311
+ commonName,
312
+ exec,
313
+ mask = _credentials.maskSecret
314
+ } = {}) {
315
+ const keytoolOptions = {
316
+ env,
317
+ ...(exec ? {
318
+ exec
319
+ } : {})
320
+ };
321
+ if (configHasPasswords(androidConfig)) {
322
+ log("config/config.json holds keystore passwords (android.keystoreConfig or android.keystore). " + "Move them to credentials/android/keystore.properties and delete them from the config.", "warning");
323
+ }
324
+ const credentials = (0, _credentials.resolveCredentials)("android", {
325
+ cwd: appRoot,
326
+ env,
327
+ log
328
+ });
329
+ const {
330
+ cleanup
331
+ } = credentials;
332
+ const paths = keyPaths(credentials.dir);
333
+ const where = credentials.source === "env" ? credentials.envName : _path.default.relative(appRoot, paths.dir) || CREDENTIALS_DIR;
334
+ const fail = (code, detail, extra) => {
335
+ cleanup();
336
+ return (0, _utils.nativeError)(code, detail, extra);
337
+ };
338
+ const finish = (storeFile, source, created = false) => {
339
+ const values = readKeystoreProperties(paths.propertiesFile);
340
+ const storePassword = values.storePassword;
341
+ const keyAlias = values.keyAlias || DEFAULT_ALIAS;
342
+ const keyPassword = values.keyPassword || storePassword;
343
+ if (!storePassword) {
344
+ throw fail("ANDROID-006", `no storePassword in keystore.properties (${where})`);
345
+ }
346
+ mask(storePassword, {
347
+ env
348
+ });
349
+ if (keyPassword !== storePassword) mask(keyPassword, {
350
+ env
351
+ });
352
+ const signing = {
353
+ storeFile,
354
+ storePassword,
355
+ keyAlias,
356
+ keyPassword,
357
+ source,
358
+ created,
359
+ cleanup
360
+ };
361
+ try {
362
+ verifyKeystore(signing, keytoolOptions);
363
+ } catch (error) {
364
+ cleanup();
365
+ throw error;
366
+ }
367
+ return signing;
368
+ };
369
+
370
+ // 1. The key is there: the app's folder, or the CI secret.
371
+ if (_fs.default.existsSync(paths.storeFile)) {
372
+ if (credentials.source === "folder") ensureGitignored(appRoot);
373
+ return finish(paths.storeFile, credentials.source === "env" ? credentials.envName : CREDENTIALS_DIR);
374
+ }
375
+ if (credentials.source === "env") {
376
+ throw fail("ANDROID-006", `${credentials.envName} has no ${_path.default.basename(paths.storeFile)}`);
377
+ }
378
+ // Settings without their key: the key was lost or not copied. A new key
379
+ // would not match the one Play knows, so stop here.
380
+ if (_fs.default.existsSync(paths.propertiesFile)) {
381
+ throw fail("ANDROID-006", `${where}/keystore.properties is there but ${_path.default.basename(paths.storeFile)} is not; restore the key from your backup`);
382
+ }
383
+
384
+ // 2. Nothing yet. Adopting or creating a key needs a person to say yes.
385
+ const legacy = legacyKeystorePath && _fs.default.existsSync(legacyKeystorePath) ? legacyKeystorePath : null;
386
+ const legacyPw = legacyPasswords(androidConfig);
387
+ const relStore = _path.default.relative(appRoot, paths.storeFile);
388
+ if (!isTTY) {
389
+ const detail = legacy ? `found ${legacy} from an earlier build; run the release build once in a terminal to adopt it` : `nothing in ${CREDENTIALS_DIR} and no ${credentials.envName}`;
390
+ throw fail("ANDROID-005", detail);
391
+ }
392
+ if (legacy && legacyPw) {
393
+ const adopt = await (0, _utils.askConsent)("key", `Found ${_path.default.relative(appRoot, legacy)} from an earlier release build. Keep using it as the upload key (copies it to ${relStore})`, {
394
+ isTTY,
395
+ prompt
396
+ });
397
+ if (adopt) {
398
+ _fs.default.mkdirSync(paths.dir, {
399
+ recursive: true,
400
+ mode: 0o700
401
+ });
402
+ _fs.default.copyFileSync(legacy, paths.storeFile, _fs.default.constants.COPYFILE_EXCL);
403
+ _fs.default.chmodSync(paths.storeFile, 0o600);
404
+ writeKeystoreProperties(paths.propertiesFile, {
405
+ keyAlias: legacyPw.keyAlias,
406
+ storePassword: legacyPw.password,
407
+ keyPassword: legacyPw.password
408
+ });
409
+ if (ensureGitignored(appRoot)) log("Added credentials/ to .gitignore", "info");
410
+ log(`Upload key adopted at ${relStore}. ${BACKUP_REMINDER}`, "warning");
411
+ return finish(paths.storeFile, "adopted");
412
+ }
413
+ }
414
+ const create = await (0, _utils.askConsent)("key", `No upload key found. Create one in ${CREDENTIALS_DIR}/ (PKCS12, kept out of git)`, {
415
+ isTTY,
416
+ prompt
417
+ });
418
+ if (!create) throw fail("ANDROID-005", "creating a new upload key was declined");
419
+ const onPlay = await (0, _utils.confirm)("Has this app been uploaded to Google Play before", {
420
+ defaultYes: false,
421
+ prompt,
422
+ isTTY: true
423
+ });
424
+ if (onPlay) {
425
+ throw fail("ANDROID-005", "this app is already on Google Play, so a new upload key would be rejected", {
426
+ hint: `Put the upload key the app was signed with in ${CREDENTIALS_DIR}/ (upload.jks and keystore.properties). If it is lost, ask Google Play to reset the upload key: ${PLAY_UPLOAD_KEY_RESET_URL}`
427
+ });
428
+ }
429
+ const keyAlias = DEFAULT_ALIAS;
430
+ const password = generatePassword();
431
+ createUploadKeystore(paths.storeFile, {
432
+ keyAlias,
433
+ password,
434
+ organizationInfo: androidConfig.keystoreConfig?.organizationInfo,
435
+ commonName,
436
+ validityYears: androidConfig.keystoreConfig?.validityYears || 25
437
+ }, keytoolOptions);
438
+ writeKeystoreProperties(paths.propertiesFile, {
439
+ keyAlias,
440
+ storePassword: password,
441
+ keyPassword: password
442
+ });
443
+ if (ensureGitignored(appRoot)) log("Added credentials/ to .gitignore", "info");
444
+ log(`Created the upload key at ${relStore}. ${BACKUP_REMINDER}`, "warning");
445
+ return finish(paths.storeFile, "created", true);
446
+ }
447
+
448
+ /** The Gradle project properties that sign the release, as environment variables. */
449
+ function signingGradleEnv(signing) {
450
+ return {
451
+ ORG_GRADLE_PROJECT_catalystStoreFile: signing.storeFile,
452
+ ORG_GRADLE_PROJECT_catalystStorePassword: signing.storePassword,
453
+ ORG_GRADLE_PROJECT_catalystKeyAlias: signing.keyAlias,
454
+ ORG_GRADLE_PROJECT_catalystKeyPassword: signing.keyPassword
455
+ };
456
+ }
@@ -11,18 +11,35 @@ const {
11
11
  const {
12
12
  loadAppConfig
13
13
  } = require("../cli/appConfig.js");
14
+ const {
15
+ buildFailure
16
+ } = require("../cli/diagnostic.js");
17
+ const {
18
+ parseNativeArgs
19
+ } = require("./utils.js");
14
20
  const {
15
21
  WEBVIEW_CONFIG,
16
22
  BUILD_OUTPUT_PATH
17
23
  } = loadAppConfig();
18
24
  async function main() {
19
- const build = createAndroidBuild({
20
- WEBVIEW_CONFIG,
21
- BUILD_OUTPUT_PATH
22
- });
25
+ let flags;
26
+ try {
27
+ flags = parseNativeArgs(process.argv.slice(2));
28
+ } catch (error) {
29
+ // `--url` without a value (NATIVE-004), before any build step ran.
30
+ process.stderr.write(buildFailure({
31
+ error,
32
+ stage: "Reading the flags",
33
+ code: "ANDROID-000"
34
+ }));
35
+ process.exit(1);
36
+ }
23
37
  const {
24
38
  buildAndroidApp
25
- } = build;
39
+ } = createAndroidBuild({
40
+ WEBVIEW_CONFIG,
41
+ BUILD_OUTPUT_PATH
42
+ }, flags);
26
43
  try {
27
44
  await buildAndroidApp();
28
45
  } catch (error) {
@@ -30,9 +47,12 @@ async function main() {
30
47
  // the troubleshooting guide, so the default mode does not repeat it.
31
48
  // --verbose/--debug add the registry's boxed report on top.
32
49
  if (resolveOutputMode() !== "default") {
50
+ // A coded error (ANDROID-005 and on, NATIVE-*) is our own; anything else is Gradle's.
51
+ const code = /^(ANDROID|NATIVE)-\d{3}$/.test(error?.code) ? error.code : "ANDROID-000";
52
+ const category = code.split("-")[0];
33
53
  console.error(formatBuildError({
34
- code: "ANDROID-000",
35
- category: "ANDROID",
54
+ code,
55
+ category,
36
56
  upstreamName: "Gradle",
37
57
  error
38
58
  }));