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,309 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.MAX_CREDENTIALS_SECRET_BYTES = void 0;
7
+ exports.cleanupOnSignal = cleanupOnSignal;
8
+ exports.credentialsDir = credentialsDir;
9
+ exports.credentialsEnvName = credentialsEnvName;
10
+ exports.maskSecret = maskSecret;
11
+ exports.packCredentials = packCredentials;
12
+ exports.resolveCredentials = resolveCredentials;
13
+ exports.unpackCredentials = unpackCredentials;
14
+ var _child_process = require("child_process");
15
+ var _fs = _interopRequireDefault(require("fs"));
16
+ var _os = _interopRequireDefault(require("os"));
17
+ var _path = _interopRequireDefault(require("path"));
18
+ var _utils = require("./utils.js");
19
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
20
+ // Signing files live in git-ignored credentials/<platform>/ in the app. CI
21
+ // gets the same folder from one secret per platform, CATALYST_<PLATFORM>_CREDENTIALS:
22
+ // a base64 gzipped tar of that folder, made by `catalyst credentials:export`.
23
+ // Everything that reads credentials goes through resolveCredentials, so the
24
+ // folder and the secret behave the same.
25
+
26
+ const PLATFORMS = ["android", "ios"];
27
+
28
+ // GitHub's limit for one secret value.
29
+ const MAX_CREDENTIALS_SECRET_BYTES = exports.MAX_CREDENTIALS_SECRET_BYTES = 48 * 1024;
30
+
31
+ // macOS tar adds AppleDouble (._*) files for extended attributes unless told not to.
32
+ const TAR_ENV = {
33
+ ...process.env,
34
+ COPYFILE_DISABLE: "1"
35
+ };
36
+ function assertPlatform(platform) {
37
+ if (!PLATFORMS.includes(platform)) {
38
+ throw new Error(`Unknown platform "${platform}": expected android or ios`);
39
+ }
40
+ }
41
+
42
+ /** @param {"android"|"ios"} platform */
43
+ function credentialsEnvName(platform) {
44
+ assertPlatform(platform);
45
+ return `CATALYST_${platform.toUpperCase()}_CREDENTIALS`;
46
+ }
47
+
48
+ /**
49
+ * The project's credentials folder for a platform (it may not exist).
50
+ *
51
+ * @param {"android"|"ios"} platform
52
+ * @param {object} [options]
53
+ * @param {string} [options.cwd]
54
+ */
55
+ function credentialsDir(platform, {
56
+ cwd = process.cwd()
57
+ } = {}) {
58
+ assertPlatform(platform);
59
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - platform is one of two fixed names; cwd is the app root.
60
+ return _path.default.join(cwd, "credentials", platform);
61
+ }
62
+ function isDirectory(dir) {
63
+ try {
64
+ return _fs.default.statSync(dir).isDirectory();
65
+ } catch (error) {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ /** GitHub Actions' escaping for workflow command data. */
71
+ function escapeCommandData(value) {
72
+ return String(value).replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
73
+ }
74
+
75
+ /**
76
+ * Hide a secret in GitHub Actions logs from here on. A no-op elsewhere.
77
+ * Call it for every password read from credentials before it can reach a
78
+ * log line or a child process's echoed output.
79
+ *
80
+ * @param {string} value
81
+ * @param {object} [options]
82
+ * @param {Record<string, string | undefined>} [options.env]
83
+ * @param {(text: string) => void} [options.write]
84
+ * @returns {boolean} whether a mask was printed
85
+ */
86
+ function maskSecret(value, {
87
+ env = process.env,
88
+ write = text => process.stdout.write(text)
89
+ } = {}) {
90
+ if (env.GITHUB_ACTIONS !== "true" || value === undefined || value === null || value === "") return false;
91
+ write(`::add-mask::${escapeCommandData(value)}\n`);
92
+ return true;
93
+ }
94
+
95
+ /**
96
+ * Pack credentials/<platform>/ into the CATALYST_<PLATFORM>_CREDENTIALS value.
97
+ *
98
+ * @param {"android"|"ios"} platform
99
+ * @param {object} [options]
100
+ * @param {string} [options.cwd]
101
+ * @returns {string} base64 of a gzipped tar of the folder's contents
102
+ */
103
+ function packCredentials(platform, {
104
+ cwd = process.cwd()
105
+ } = {}) {
106
+ const dir = credentialsDir(platform, {
107
+ cwd
108
+ });
109
+ const files = isDirectory(dir) ? _fs.default.readdirSync(dir).filter(name => name !== ".DS_Store") : [];
110
+ if (files.length === 0) {
111
+ throw (0, _utils.nativeError)("NATIVE-008", `credentials/${platform}/ is missing or empty in ${cwd}`);
112
+ }
113
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - fixed tar argv; dir is the app's own credentials folder.
114
+ const archive = (0, _child_process.execFileSync)("tar", ["-czf", "-", "--exclude", ".DS_Store", "-C", dir, "."], {
115
+ env: TAR_ENV,
116
+ maxBuffer: 64 * 1024 * 1024,
117
+ stdio: ["ignore", "pipe", "pipe"]
118
+ });
119
+ const value = archive.toString("base64");
120
+ if (value.length > MAX_CREDENTIALS_SECRET_BYTES) {
121
+ throw (0, _utils.nativeError)("NATIVE-009", `credentials/${platform}/ packs to ${Math.ceil(value.length / 1024)} KB; the limit is 48 KB`);
122
+ }
123
+ return value;
124
+ }
125
+
126
+ /** Tar entries that could write outside the destination, or are links. */
127
+ function unsafeEntries(listing, verbose) {
128
+ const names = listing.split("\n").filter(Boolean);
129
+ const bad = names.filter(name => {
130
+ const clean = name.replace(/^\.\//, "");
131
+ return clean.startsWith("/") || clean.split("/").includes("..");
132
+ });
133
+ // Long listings start each line with the entry type: "-" file, "d" folder.
134
+ const links = verbose.split("\n").filter(Boolean).filter(line => !/^[-d]/.test(line));
135
+ return [...bad, ...links];
136
+ }
137
+
138
+ /**
139
+ * Unpack a CATALYST_<PLATFORM>_CREDENTIALS value into `destDir`.
140
+ * Only plain files and folders inside `destDir` are accepted.
141
+ *
142
+ * @param {string} value
143
+ * @param {string} destDir - created when missing
144
+ * @param {object} [options]
145
+ * @param {string} [options.envName] - for the error message
146
+ */
147
+ function unpackCredentials(value, destDir, {
148
+ envName = "the credentials secret"
149
+ } = {}) {
150
+ const archive = Buffer.from(String(value || "").trim(), "base64");
151
+ if (archive.length < 2 || archive[0] !== 0x1f || archive[1] !== 0x8b) {
152
+ throw (0, _utils.nativeError)("NATIVE-007", `${envName} is not a base64 gzipped tar`);
153
+ }
154
+ const work = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), "catalyst-credentials-archive-"));
155
+ try {
156
+ const file = _path.default.join(work, "credentials.tgz");
157
+ _fs.default.writeFileSync(file, archive, {
158
+ mode: 0o600
159
+ });
160
+ let listing;
161
+ let verbose;
162
+ try {
163
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - fixed tar argv on a temp file.
164
+ listing = (0, _child_process.execFileSync)("tar", ["-tzf", file], {
165
+ encoding: "utf8",
166
+ stdio: ["ignore", "pipe", "pipe"]
167
+ });
168
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - fixed tar argv on a temp file.
169
+ verbose = (0, _child_process.execFileSync)("tar", ["-tvzf", file], {
170
+ encoding: "utf8",
171
+ stdio: ["ignore", "pipe", "pipe"]
172
+ });
173
+ } catch (error) {
174
+ throw (0, _utils.nativeError)("NATIVE-007", `${envName} could not be read as a tar archive`);
175
+ }
176
+ const unsafe = unsafeEntries(listing, verbose);
177
+ if (unsafe.length) {
178
+ throw (0, _utils.nativeError)("NATIVE-007", `${envName} holds links or paths outside the folder`);
179
+ }
180
+ _fs.default.mkdirSync(destDir, {
181
+ recursive: true,
182
+ mode: 0o700
183
+ });
184
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - fixed tar argv; entries were checked above.
185
+ (0, _child_process.execFileSync)("tar", ["-xzf", file, "-C", destDir], {
186
+ stdio: ["ignore", "pipe", "pipe"]
187
+ });
188
+ } finally {
189
+ _fs.default.rmSync(work, {
190
+ recursive: true,
191
+ force: true
192
+ });
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Run `cleanup` on SIGINT and SIGTERM too: a Ctrl+C ends the process
198
+ * without running any `finally`, which would leave unpacked secrets in the
199
+ * temp folder. After cleaning up it exits (130 or 143), unless another
200
+ * handler for the signal is still to run, in which case that one exits.
201
+ *
202
+ * @param {() => void} cleanup
203
+ * @param {object} [options]
204
+ * @param {{ once: Function, removeListener: Function, listenerCount?: Function }} [options.signals]
205
+ * @param {(code: number) => void} [options.exit]
206
+ * @returns {() => void} removes the handlers; call it from the normal cleanup
207
+ */
208
+ function cleanupOnSignal(cleanup, {
209
+ signals = process,
210
+ exit = code => process.exit(code)
211
+ } = {}) {
212
+ const release = () => {
213
+ signals.removeListener("SIGINT", onSignal);
214
+ signals.removeListener("SIGTERM", onSignal);
215
+ };
216
+ function onSignal(signal) {
217
+ release();
218
+ cleanup();
219
+ const others = typeof signals.listenerCount === "function" ? signals.listenerCount(signal) : 0;
220
+ if (others === 0) exit(signal === "SIGINT" ? 130 : 143);
221
+ }
222
+ signals.once("SIGINT", onSignal);
223
+ signals.once("SIGTERM", onSignal);
224
+ return release;
225
+ }
226
+
227
+ /**
228
+ * Where this build reads its credentials for a platform.
229
+ *
230
+ * 1. credentials/<platform>/ in the app, when it exists.
231
+ * 2. Otherwise CATALYST_<PLATFORM>_CREDENTIALS, unpacked into a private temp
232
+ * folder that `cleanup()` removes, and that an interrupted build (Ctrl+C,
233
+ * SIGTERM) removes too. The project folder is never written.
234
+ * 3. Otherwise `source: "none"` and `dir` is the (missing) project folder,
235
+ * where a first-time setup should create it.
236
+ *
237
+ * The caller still masks each password it reads with maskSecret().
238
+ *
239
+ * @param {"android"|"ios"} platform
240
+ * @param {object} [options]
241
+ * @param {string} [options.cwd]
242
+ * @param {Record<string, string | undefined>} [options.env]
243
+ * @param {(message: string, type?: string) => void} [options.log]
244
+ * @param {object} [options.signals] - for tests; see cleanupOnSignal
245
+ * @param {(code: number) => void} [options.exit] - for tests
246
+ * @returns {{ dir: string, source: "folder" | "env" | "none", envName: string, cleanup: () => void }}
247
+ */
248
+ function resolveCredentials(platform, {
249
+ cwd = process.cwd(),
250
+ env = process.env,
251
+ log = () => {},
252
+ signals,
253
+ exit
254
+ } = {}) {
255
+ const dir = credentialsDir(platform, {
256
+ cwd
257
+ });
258
+ const envName = credentialsEnvName(platform);
259
+ if (isDirectory(dir)) return {
260
+ dir,
261
+ source: "folder",
262
+ envName,
263
+ cleanup: () => {}
264
+ };
265
+ const value = env[envName];
266
+ if (value && String(value).trim()) {
267
+ const temp = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), `catalyst-${platform}-credentials-`));
268
+ _fs.default.chmodSync(temp, 0o700);
269
+ try {
270
+ unpackCredentials(value, temp, {
271
+ envName
272
+ });
273
+ } catch (error) {
274
+ _fs.default.rmSync(temp, {
275
+ recursive: true,
276
+ force: true
277
+ });
278
+ throw error;
279
+ }
280
+ log(`Using ${platform} credentials from ${envName}`, "info");
281
+ const remove = () => _fs.default.rmSync(temp, {
282
+ recursive: true,
283
+ force: true
284
+ });
285
+ const releaseSignals = cleanupOnSignal(remove, {
286
+ ...(signals ? {
287
+ signals
288
+ } : {}),
289
+ ...(exit ? {
290
+ exit
291
+ } : {})
292
+ });
293
+ return {
294
+ dir: temp,
295
+ source: "env",
296
+ envName,
297
+ cleanup: () => {
298
+ releaseSignals();
299
+ remove();
300
+ }
301
+ };
302
+ }
303
+ return {
304
+ dir,
305
+ source: "none",
306
+ envName,
307
+ cleanup: () => {}
308
+ };
309
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.runExportCredentials = runExportCredentials;
7
+ var _credentials = require("./credentials.js");
8
+ // `catalyst credentials:export <android|ios>`: print credentials/<platform>/
9
+ // as the value of the CATALYST_<PLATFORM>_CREDENTIALS CI secret. The value
10
+ // goes to stdout alone (so it can be piped to pbcopy); the one line on where
11
+ // to paste it goes to stderr. The same value is the backup of the folder.
12
+
13
+ const GUTTER = " ";
14
+
15
+ /**
16
+ * @param {object} [options]
17
+ * @param {string[]} [options.argv]
18
+ * @param {string} [options.cwd]
19
+ * @param {(text: string) => void} [options.stdout]
20
+ * @param {(text: string) => void} [options.stderr]
21
+ * @returns {number} the exit code
22
+ */
23
+ function runExportCredentials({
24
+ argv = process.argv.slice(2),
25
+ cwd = process.cwd(),
26
+ stdout = text => process.stdout.write(text),
27
+ stderr = text => process.stderr.write(text)
28
+ } = {}) {
29
+ const platform = argv.find(arg => !arg.startsWith("-"));
30
+ if (platform !== "android" && platform !== "ios") {
31
+ stderr(`\n${GUTTER}Usage: catalyst credentials:export <android|ios>\n\n`);
32
+ return 1;
33
+ }
34
+ try {
35
+ const value = (0, _credentials.packCredentials)(platform, {
36
+ cwd
37
+ });
38
+ stdout(`${value}\n`);
39
+ stderr(`\n${GUTTER}Paste this into a GitHub Actions secret named ${(0, _credentials.credentialsEnvName)(platform)} ` + "(Settings > Secrets and variables > Actions), and keep a copy somewhere safe as your backup.\n\n");
40
+ return 0;
41
+ } catch (error) {
42
+ const lines = [`\n${GUTTER}error ${error.code ? `[${error.code}] ` : ""}${error.message}`];
43
+ if (error.hint) lines.push(`${GUTTER}hint ${error.hint}`);
44
+ if (error.docUrl) lines.push(`${GUTTER}docs ${error.docUrl}`);
45
+ stderr(`${lines.join("\n")}\n\n`);
46
+ return 1;
47
+ }
48
+ }
49
+ if (typeof require !== "undefined" && typeof module !== "undefined" && require.main === module) {
50
+ process.exit(runExportCredentials());
51
+ }