catalyst-core 1.0.1 → 1.0.2-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 (114) hide show
  1. package/bin/catalyst.js +105 -1
  2. package/dist/cli/README.md +119 -0
  3. package/dist/cli/appConfig.js +50 -0
  4. package/dist/cli/buildErrors.js +170 -0
  5. package/dist/cli/buildWarnings.js +28 -0
  6. package/dist/cli/devOutput.js +524 -0
  7. package/dist/cli/diagnostic.js +388 -0
  8. package/dist/cli/hints.js +69 -0
  9. package/dist/cli/package.json +3 -0
  10. package/dist/cli/spinner.js +141 -0
  11. package/dist/cli/theme.js +155 -0
  12. package/dist/errors/index.js +7 -3
  13. package/dist/errors/registry.js +8 -0
  14. package/dist/errors-index.json +7 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.jsx +1 -1
  17. package/dist/logger.js +61 -6
  18. package/dist/native/androidProject/app/build.gradle.kts +2 -1
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +2 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +9 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +65 -34
  22. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt +109 -0
  23. package/dist/native/androidSetup.js +46 -57
  24. package/dist/native/buildAndroid/assets.js +22 -13
  25. package/dist/native/buildAndroid/build.js +98 -26
  26. package/dist/native/buildAndroid/config.js +43 -12
  27. package/dist/native/buildAndroid/index.js +49 -191
  28. package/dist/native/buildAppAndroid.js +19 -8
  29. package/dist/native/buildAppIos.js +35 -16
  30. package/dist/native/buildIos/assets.js +1 -6
  31. package/dist/native/buildIos/build.js +61 -94
  32. package/dist/native/buildIos/config.js +26 -8
  33. package/dist/native/buildIos/index.js +143 -64
  34. package/dist/native/buildIos/plugins.js +12 -15
  35. package/dist/native/internalPluginUtils.js +0 -1
  36. package/dist/native/iosnativeWebView/CoreLogic/Sources/CatalystCoreLogic/Constants/ConfigConstants.swift +2 -0
  37. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashHTMLView.swift +31 -0
  38. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +17 -1
  39. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +41 -1
  40. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +2 -0
  41. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +3 -0
  42. package/dist/native/pluginComposerAndroid.js +2 -3
  43. package/dist/native/renameAndroidProject.js +42 -20
  44. package/dist/native/setupEmulatorIos.js +28 -49
  45. package/dist/native/setupServer.js +12 -15
  46. package/dist/native/terminalProgress.js +186 -147
  47. package/dist/native/utils.js +335 -54
  48. package/dist/otel.js +15 -9
  49. package/dist/scripts/build.js +314 -42
  50. package/dist/scripts/generateOfflineManifest.js +11 -1
  51. package/dist/scripts/loadEnvironmentVariables.js +24 -2
  52. package/dist/scripts/preServerInit.js +32 -10
  53. package/dist/scripts/preflight.js +115 -11
  54. package/dist/scripts/scriptUtils.js +34 -0
  55. package/dist/scripts/serve.js +65 -9
  56. package/dist/scripts/start.js +58 -21
  57. package/dist/server/expressServer.js +167 -32
  58. package/dist/server/renderer/document/Body.d.ts +0 -11
  59. package/dist/server/renderer/document/Body.jsx +0 -10
  60. package/dist/server/renderer/document/Head.d.ts +0 -13
  61. package/dist/server/renderer/document/Head.jsx +0 -12
  62. package/dist/server/renderer/handler.jsx +87 -11
  63. package/dist/server/utils/validator.js +14 -11
  64. package/dist/vite/manifest-categorization-plugin.js +7 -5
  65. package/dist/vite/resolveDevServerConfig.js +9 -2
  66. package/dist/vite/vite.config.client.js +19 -1
  67. package/dist/vite/vite.config.js +96 -19
  68. package/dist/vite/vite.config.server.js +21 -0
  69. package/dist/web-router/components/MetaTag.jsx +20 -14
  70. package/dist/web-router/components/MetaTag.test.jsx +108 -32
  71. package/dist/web-router/components/RouterDataProvider.d.ts +25 -1
  72. package/dist/web-router/components/RouterDataProvider.jsx +35 -1
  73. package/dist/web-router/components/RouterDataProvider.test.jsx +44 -0
  74. package/dist/web-router/context.d.ts +15 -1
  75. package/dist/web-router/context.jsx +11 -0
  76. package/dist/webmcp/WebMcpProvider.jsx +234 -0
  77. package/dist/webmcp/declarative.js +207 -0
  78. package/dist/webmcp/declarative.test.js +122 -0
  79. package/dist/webmcp/errors.js +59 -0
  80. package/dist/webmcp/errors.test.js +47 -0
  81. package/dist/webmcp/index.js +40 -0
  82. package/dist/webmcp/native.js +37 -0
  83. package/dist/webmcp/native.test.js +41 -0
  84. package/dist/webmcp/pageInfo.js +119 -0
  85. package/dist/webmcp/pageInfo.test.js +85 -0
  86. package/dist/webmcp/registry.dom.test.js +175 -0
  87. package/dist/webmcp/registry.js +540 -0
  88. package/dist/webmcp/registry.test.js +228 -0
  89. package/dist/webmcp/routeContext.js +11 -0
  90. package/dist/webmcp/shim-entry.js +8 -0
  91. package/dist/webmcp/shim.js +149 -0
  92. package/dist/webmcp/useTool.js +124 -0
  93. package/package.json +4 -7
  94. package/test/cli/buildErrors.test.ts +173 -0
  95. package/test/cli/buildWarnings.test.cjs +62 -0
  96. package/test/cli/devOutput.test.ts +549 -0
  97. package/test/cli/diagnostic.test.ts +434 -0
  98. package/test/cli/output.test.cjs +90 -0
  99. package/test/cli/spinner.test.ts +177 -0
  100. package/test/cli/theme.test.ts +163 -0
  101. package/test/native/redactSecrets.test.ts +72 -0
  102. package/test/native/utils.test.ts +532 -0
  103. package/test/scripts/build.test.ts +275 -0
  104. package/test/scripts/preflight.test.ts +100 -2
  105. package/test/scripts/serve.test.ts +102 -0
  106. package/test/server/fixtures/template/src/js/routes/index.jsx +8 -0
  107. package/test/server/handler.test.ts +17 -0
  108. package/test/server/logger.test.ts +185 -0
  109. package/test/server/manifestCache.test.ts +13 -0
  110. package/test/server/otel.test.ts +556 -0
  111. package/test/server/validator.test.ts +19 -8
  112. package/vitest.config.ts +4 -0
  113. package/dist/scripts/checkVersion.js +0 -3
  114. package/dist/scripts/cleanCache.js +0 -30
@@ -3,8 +3,15 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.ANDROID_SDK_HINT = void 0;
6
7
  exports.commandExists = commandExists;
8
+ exports.detectAndroidSdk = detectAndroidSdk;
9
+ exports.ensureLocalProperties = ensureLocalProperties;
10
+ exports.expandHome = expandHome;
11
+ exports.listAvds = listAvds;
7
12
  exports.promptUser = promptUser;
13
+ exports.redactError = redactError;
14
+ exports.redactSecrets = redactSecrets;
8
15
  exports.runCommand = runCommand;
9
16
  exports.runInteractiveCommand = runInteractiveCommand;
10
17
  exports.runSdkManagerCommand = runSdkManagerCommand;
@@ -12,19 +19,88 @@ exports.validateAndCompleteConfig = validateAndCompleteConfig;
12
19
  var _child_process = require("child_process");
13
20
  var _readline = _interopRequireDefault(require("readline"));
14
21
  var _fs = _interopRequireDefault(require("fs"));
22
+ var _os = _interopRequireDefault(require("os"));
23
+ var _path = _interopRequireDefault(require("path"));
24
+ var _theme = _interopRequireDefault(require("../cli/theme.js"));
25
+ var _spinner = _interopRequireDefault(require("../cli/spinner.js"));
15
26
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
16
- const rl = _readline.default.createInterface({
17
- input: process.stdin,
18
- output: process.stdout
19
- });
20
- function runCommand(command) {
27
+ const {
28
+ GUTTER,
29
+ t
30
+ } = _theme.default;
31
+ const {
32
+ withSpinnersPaused
33
+ } = _spinner.default;
34
+
35
+ // Retain only the tail of a child's output: it is used for diagnostics, and a
36
+ // long gradle build would otherwise hold every byte it ever printed in memory.
37
+ const MAX_OUTPUT_BUFFER_BYTES = 1024 * 1024;
38
+
39
+ // Prompts are short, so only a small trailing window is ever worth matching.
40
+ // This is load-bearing, not belt-and-braces: handlePrompts only trims the
41
+ // buffer when something matches, and a normal build never prompts at all --
42
+ // so without the cap the match buffer accumulates the entire build output and
43
+ // indexOf rescans all of it on every chunk.
44
+ const MAX_PROMPT_MATCH_BYTES = 8 * 1024;
45
+
46
+ // Signing commands carry keystore passwords on the command line, and this is
47
+ // the one place every native command is echoed on failure. Redact here so a
48
+ // wrong password cannot put the real one in the terminal or in CI logs.
49
+ const SECRET_ARG_PATTERNS = [/(-storepass\s+)("[^"]*"|\S+)/gi, /(-keypass\s+)("[^"]*"|\S+)/gi, /(signing\.store\.password=)("[^"]*"|\S+)/gi, /(signing\.key\.password=)("[^"]*"|\S+)/gi, /(--password[=\s]+)("[^"]*"|\S+)/gi];
50
+ function redactSecrets(text) {
51
+ if (typeof text !== "string") return text;
52
+ return SECRET_ARG_PATTERNS.reduce((acc, pattern) => acc.replace(pattern, "$1***"), text);
53
+ }
54
+
55
+ /** Redacts by pattern, and also every secret value the command carries, in case a tool echoes one back without its flag. */
56
+ function secretScrubber(command) {
57
+ const secrets = SECRET_ARG_PATTERNS.flatMap(pattern => [...String(command).matchAll(pattern)].map(match => match[2].replace(/^"|"$/g, ""))).filter(Boolean);
58
+ return text => typeof text === "string" ? secrets.reduce((acc, secret) => acc.split(secret).join("***"), redactSecrets(text)) : text;
59
+ }
60
+
61
+ /**
62
+ * Scrub a failed command's error before anyone sees it. execSync puts the
63
+ * whole command line in error.message ("Command failed: ...") and the child's
64
+ * output in stdout/stderr, and callers rethrow error.message wrapped in their
65
+ * own text -- so the passwords are removed here, at the source.
66
+ */
67
+ function redactError(error, command) {
68
+ if (!error || typeof error !== "object") return error;
69
+ const scrub = secretScrubber(command);
70
+ for (const key of ["message", "stack", "cmd", "stdout", "stderr"]) {
71
+ if (typeof error[key] === "string") error[key] = scrub(error[key]);
72
+ }
73
+ if (Array.isArray(error.output)) error.output = error.output.map(scrub);
74
+ return error;
75
+ }
76
+
77
+ /**
78
+ * @param {string} command
79
+ * @param {object} [options]
80
+ * @param {number} [options.timeout] - ms before the command is killed. Device
81
+ * probes (adb, devicectl) take one: a wedged adb server otherwise hangs the
82
+ * build forever behind a spinner that keeps saying it is working.
83
+ */
84
+ function runCommand(command, {
85
+ timeout
86
+ } = {}) {
87
+ // Redacted before it reaches the log file too -- the log is a build
88
+ // artifact that gets attached to bug reports and uploaded by CI.
21
89
  try {
22
90
  // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Native setup/build uses fixed internal command strings; preserve existing shell behavior.
91
+ // stderr is captured, not inherited. Many tools (java -version, for
92
+ // one) write to stderr on success, and an inherited fd puts that text
93
+ // at column 0 -- straight through whatever the spinner is drawing.
94
+ // The caller decides what, if anything, is worth showing.
23
95
  return (0, _child_process.execSync)(command, {
24
- encoding: "utf8"
96
+ encoding: "utf8",
97
+ stdio: ["ignore", "pipe", "pipe"],
98
+ timeout
25
99
  });
26
100
  } catch (error) {
27
- console.error(`Error executing command: ${command}`);
101
+ if (timeout && isTimeout(error)) throw toolTimeoutError(command, timeout);
102
+ redactError(error, command);
103
+ console.error(`Error executing command: ${secretScrubber(command)(command)}`);
28
104
  console.error(`Error message: ${error.message}`);
29
105
  throw error;
30
106
  }
@@ -40,49 +116,236 @@ function commandExists(command) {
40
116
  return false;
41
117
  }
42
118
  }
43
- async function promptUser(question) {
44
- return new Promise(resolve => {
45
- rl.question(question, answer => {
46
- resolve(answer.trim());
119
+
120
+ /** execSync reports a timeout as a killed child, not with a code of its own. */
121
+ function isTimeout(error) {
122
+ return error?.code === "ETIMEDOUT" || error?.signal === "SIGTERM" && error?.status === null;
123
+ }
124
+
125
+ /**
126
+ * A device tool that stopped answering. adb in particular wedges: its server
127
+ * keeps the port open and accepts connections but never replies, so every
128
+ * later call hangs too. Name the tool and the reset that clears it.
129
+ */
130
+ function toolTimeoutError(command, timeout) {
131
+ const tool = _path.default.basename(String(command).trim().split(/\s+/)[0].replace(/^"|"$/g, ""));
132
+ const seconds = Math.round(timeout / 1000);
133
+ const error = new Error(`${tool} did not respond within ${seconds}s`);
134
+ error.code = "ETOOLTIMEOUT";
135
+ error.hint = tool === "adb" ? "The adb server is stuck. Restart it with `adb kill-server && adb start-server`, then retry." : tool === "xcrun" ? "Xcode's device tools are not responding. Quit Xcode and Simulator, then retry." : `Check that ${tool} works on its own, then retry.`;
136
+ return error;
137
+ }
138
+
139
+ /** A required value with no terminal to ask for it on, e.g. in CI. */
140
+ function missingConfigError(question) {
141
+ const label = question.replace(/[:\s]+$/, "");
142
+ const error = new Error(`Missing configuration, and no terminal to ask on: ${label}`);
143
+ error.code = "ENOTTY";
144
+ error.hint = "Set it in config/config.json (WEBVIEW_CONFIG), or run the command in an interactive terminal.";
145
+ return error;
146
+ }
147
+
148
+ /**
149
+ * Ask a question on the terminal.
150
+ *
151
+ * Every live spinner is stood down first: the native steps run under one, and
152
+ * its next frame used to erase the question, so a missing config field looked
153
+ * like a hang that never ended. The interface is created per question and
154
+ * closed after it -- one held open at module load kept stdin referenced, so
155
+ * every command that imported this file had to process.exit() to finish.
156
+ */
157
+ async function promptUser(question, {
158
+ defaultValue
159
+ } = {}) {
160
+ if (!process.stdin.isTTY) {
161
+ if (defaultValue !== undefined) return defaultValue;
162
+ throw missingConfigError(question);
163
+ }
164
+ return withSpinnersPaused(() => new Promise(resolve => {
165
+ const rl = _readline.default.createInterface({
166
+ input: process.stdin,
167
+ output: process.stdout
168
+ });
169
+ const label = question.replace(/[:\s]+$/, "");
170
+ const suffix = defaultValue ? t.dim(` (${defaultValue})`) : "";
171
+ rl.question(`${GUTTER}${t.warn("?")} ${label}${suffix} `, answer => {
172
+ rl.close();
173
+ const value = answer.trim() || defaultValue || "";
174
+ // Settle the question into a record of what was chosen,
175
+ // in place: one line up, cleared, rewritten.
176
+ const esc = String.fromCharCode(27);
177
+ process.stdout.write(`${esc}[1A\r${esc}[2K${GUTTER}${t.ok("✓")} ${label} ${t.dim(value)}\n`);
178
+ resolve(value);
179
+ });
180
+ }));
181
+ }
182
+
183
+ /**
184
+ * Where the Android SDK most likely lives: the environment first, then the
185
+ * locations Android Studio installs to. Returns null when none has adb, so a
186
+ * stray empty directory is not mistaken for an SDK.
187
+ */
188
+ function detectAndroidSdk(env = process.env) {
189
+ const home = _os.default.homedir();
190
+ const candidates = [env.ANDROID_HOME, env.ANDROID_SDK_ROOT, _path.default.join(home, "Library", "Android", "sdk"), _path.default.join(home, "Android", "Sdk"), env.LOCALAPPDATA && _path.default.join(env.LOCALAPPDATA, "Android", "Sdk")].filter(Boolean);
191
+ const adb = process.platform === "win32" ? "adb.exe" : "adb";
192
+ return candidates.find(dir => _fs.default.existsSync(_path.default.join(dir, "platform-tools", adb))) || null;
193
+ }
194
+
195
+ /** AVD names known to the SDK's emulator, or [] when it cannot be asked. */
196
+ function listAvds(sdkPath) {
197
+ if (!sdkPath) return [];
198
+ try {
199
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - sdkPath is a local SDK directory from config or auto-detection.
200
+ const out = (0, _child_process.execSync)(`"${_path.default.join(expandHome(sdkPath), "emulator", "emulator")}" -list-avds`, {
201
+ encoding: "utf8",
202
+ stdio: ["ignore", "pipe", "ignore"],
203
+ timeout: 15000
47
204
  });
48
- });
205
+ return out.split("\n").map(line => line.trim()).filter(line => line && !line.startsWith("INFO"));
206
+ } catch (error) {
207
+ return [];
208
+ }
209
+ }
210
+
211
+ /** Expand a leading `~` (as in the SDK hint below); nothing else in the chain does. */
212
+ function expandHome(dir) {
213
+ if (typeof dir !== "string") return dir;
214
+ if (dir === "~") return _os.default.homedir();
215
+ return dir.startsWith("~/") || dir.startsWith("~\\") ? _path.default.join(_os.default.homedir(), dir.slice(2)) : dir;
216
+ }
217
+
218
+ /** The hint for a missing or wrong SDK path, naming the one field to set. */
219
+ const ANDROID_SDK_HINT = exports.ANDROID_SDK_HINT = 'Install Android Studio, or set WEBVIEW_CONFIG.android.sdkPath in config/config.json (e.g. "~/Library/Android/sdk").';
220
+
221
+ /**
222
+ * Write the SDK location gradle reads. `setupEmulator:android` used to be the
223
+ * only thing that wrote it, so a `buildApp:android` without a prior setup died
224
+ * in gradle with "SDK location not found" despite a configured sdkPath.
225
+ */
226
+ function ensureLocalProperties(projectDir, sdkPath) {
227
+ const file = _path.default.join(projectDir, "local.properties");
228
+ const line = `sdk.dir=${expandHome(sdkPath).replace(/\\/g, "\\\\")}`;
229
+ let content = "";
230
+ try {
231
+ content = _fs.default.readFileSync(file, "utf8");
232
+ } catch (error) {
233
+ // Missing is the case being fixed.
234
+ }
235
+ if (content.split("\n").some(existing => existing.trim() === line)) return false;
236
+ const kept = content.split("\n").filter(existing => existing.trim() && !existing.startsWith("sdk.dir="));
237
+ _fs.default.writeFileSync(file, [...kept, line].join("\n") + "\n");
238
+ return true;
49
239
  }
50
- async function runInteractiveCommand(command, args, promptResponses = {}) {
240
+
241
+ /**
242
+ * Run a command, keeping its output rather than printing it.
243
+ *
244
+ * @param {string} command
245
+ * @param {string[]} args
246
+ * @param {object} [promptResponses] - text to answer prompts with
247
+ * @param {object} [options]
248
+ * @param {(line: string) => void} [options.onLine] - called per output line.
249
+ * Without it the output is captured silently. Gradle and xcodebuild emit
250
+ * hundreds of lines that nobody reads on success and that are redundant on
251
+ * failure -- the error path mines `error.output`, which is retained either
252
+ * way. Callers that want progress pass a handler that summarises, rather
253
+ * than letting the raw stream scroll.
254
+ */
255
+ async function runInteractiveCommand(command, args, promptResponses = {}, {
256
+ onLine
257
+ } = {}) {
51
258
  return new Promise((resolve, reject) => {
259
+ // Named `child`, not `process`: the previous name shadowed the global
260
+ // `process` for the whole function body.
52
261
  // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Native build commands are fixed internal commands passed with argv arrays.
53
- const process = (0, _child_process.spawn)(command, args, {
262
+ const child = (0, _child_process.spawn)(command, args, {
54
263
  stdio: ["pipe", "pipe", "pipe"]
55
264
  });
56
- let buffer = "";
265
+ let promptBuffer = "";
57
266
  let outputBuffer = "";
58
- process.stdout.on("data", data => {
59
- buffer += data.toString();
60
- outputBuffer += data.toString();
61
- console.log(data.toString());
62
- handlePrompts(process, buffer, promptResponses);
63
- });
64
- process.stderr.on("data", data => {
65
- buffer += data.toString();
66
- outputBuffer += data.toString();
67
- console.error(data.toString());
68
- handlePrompts(process, buffer, promptResponses);
267
+ const partial = {
268
+ stdout: "",
269
+ stderr: ""
270
+ };
271
+ const consume = (data, stream) => {
272
+ const text = data.toString();
273
+
274
+ // Hold back a trailing partial line until its newline arrives.
275
+ // Printing raw chunks splits lines mid-write and injects blanks.
276
+ const lines = (partial[stream] + text).split("\n");
277
+ partial[stream] = lines.pop();
278
+ if (onLine) {
279
+ for (const line of lines) onLine(line);
280
+ }
281
+
282
+ // Previously unbounded: a long gradle build retained every byte.
283
+ outputBuffer += text;
284
+ if (outputBuffer.length > MAX_OUTPUT_BUFFER_BYTES) {
285
+ outputBuffer = outputBuffer.slice(-MAX_OUTPUT_BUFFER_BYTES);
286
+ }
287
+ promptBuffer = handlePrompts(child, promptBuffer + text, promptResponses);
288
+ if (promptBuffer.length > MAX_PROMPT_MATCH_BYTES) {
289
+ promptBuffer = promptBuffer.slice(-MAX_PROMPT_MATCH_BYTES);
290
+ }
291
+ };
292
+ child.stdout.on("data", data => consume(data, "stdout"));
293
+ child.stderr.on("data", data => consume(data, "stderr"));
294
+
295
+ // Without this the promise never settles when the binary is missing or
296
+ // not executable, so a missing gradle/adb hangs the CLI indefinitely.
297
+ child.on("error", error => {
298
+ if (error.code === "ENOENT") {
299
+ reject(new Error(`Command not found: ${command}`, {
300
+ cause: error
301
+ }));
302
+ } else {
303
+ reject(new Error(`Failed to start ${command}: ${error.message}`, {
304
+ cause: error
305
+ }));
306
+ }
69
307
  });
70
- process.on("close", code => {
308
+ child.on("close", code => {
309
+ // Flush whatever never got its closing newline.
310
+ for (const stream of ["stdout", "stderr"]) {
311
+ if (partial[stream]) {
312
+ if (onLine) onLine(partial[stream]);
313
+ partial[stream] = "";
314
+ }
315
+ }
71
316
  if (code === 0) {
72
317
  resolve(outputBuffer);
73
- } else {
74
- reject(new Error(`Command failed with exit code ${code}`));
318
+ return;
75
319
  }
320
+
321
+ // Carry the output on the error. Without it the caller only has
322
+ // "exit code 1" and the compiler error that actually broke the
323
+ // build is lost -- which is why failures used to mean opening
324
+ // Android Studio to find out what happened.
325
+ const failure = new Error(`Command failed with exit code ${code}`);
326
+ failure.code = code;
327
+ failure.output = outputBuffer;
328
+ reject(failure);
76
329
  });
77
330
  });
78
331
  }
79
- function handlePrompts(process, buffer, promptResponses) {
332
+
333
+ /**
334
+ * Answer a prompt found in the accumulated output.
335
+ *
336
+ * Returns the buffer to keep looking in. The match must be consumed: the buffer
337
+ * grows with every chunk, so leaving a matched prompt in it re-answers that
338
+ * prompt on every subsequent chunk, spraying `y\n` into the child's stdin.
339
+ */
340
+ function handlePrompts(child, buffer, promptResponses) {
80
341
  for (const [prompt, response] of Object.entries(promptResponses)) {
81
- if (buffer.includes(prompt)) {
82
- process.stdin.write(response + "\n");
83
- return;
342
+ const index = buffer.indexOf(prompt);
343
+ if (index !== -1) {
344
+ child.stdin.write(response + "\n");
345
+ return buffer.slice(index + prompt.length);
84
346
  }
85
347
  }
348
+ return buffer;
86
349
  }
87
350
  async function runSdkManagerCommand(sdkManagerPath, args) {
88
351
  const promptResponses = {
@@ -110,22 +373,35 @@ async function validateAndCompleteConfig(platform, configPath) {
110
373
  // Check if WEBVIEW_CONFIG already has all required fields
111
374
  const webviewConfig = config.WEBVIEW_CONFIG;
112
375
  const commonFields = {
113
- port: "Enter port number (e.g., 3005): "
376
+ port: "Port the app is served on"
114
377
  };
378
+
379
+ // cachePattern is optional -- the iOS build emits an empty pattern list
380
+ // when it is absent and Android never required it -- so asking for it on
381
+ // every first setup only stalled people on a question they could not answer.
115
382
  const platformConfigs = {
116
383
  android: {
117
- buildType: "Enter Android build type (debug/release): ",
118
- sdkPath: "Enter Android SDK path: ",
119
- emulatorName: "Enter Android emulator name (e.g., Small_Phone_API_35): ",
120
- cachePattern: "Enter Cache pattern (e.g., *.css): "
384
+ buildType: "Android build type (debug/release)",
385
+ sdkPath: "Android SDK path",
386
+ emulatorName: "Android emulator (AVD) name"
121
387
  },
122
388
  ios: {
123
- buildType: "Enter iOS build type (debug/release): ",
124
- appBundleId: "Enter iOS bundle ID (e.g., com.test.test): ",
125
- simulatorName: "Enter iOS simulator name (e.g., iPhone 16 Pro): ",
126
- cachePattern: "Enter Cache pattern (e.g., *.css): "
389
+ buildType: "iOS build type (debug/release)",
390
+ appBundleId: "iOS bundle ID (e.g. com.example.app)",
391
+ simulatorName: "iOS simulator name (e.g. iPhone 16 Pro)"
127
392
  }
128
393
  };
394
+
395
+ // Answers we can work out are filled in rather than asked; the rest are
396
+ // asked with the best guess offered as the default.
397
+ const sdkGuess = platform === "android" ? webviewConfig.android?.sdkPath || detectAndroidSdk() : null;
398
+ const defaults = {
399
+ port: config.NODE_SERVER_PORT ? String(config.NODE_SERVER_PORT) : undefined,
400
+ buildType: "debug",
401
+ sdkPath: sdkGuess || undefined,
402
+ emulatorName: platform === "android" ? listAvds(sdkGuess)[0] : undefined
403
+ };
404
+ const filled = [];
129
405
  if (!platformConfigs[platform]) {
130
406
  throw new Error('Invalid platform. Must be either "android" or "ios"');
131
407
  }
@@ -162,24 +438,29 @@ async function validateAndCompleteConfig(platform, configPath) {
162
438
  };
163
439
  }
164
440
 
165
- // Handle common fields
166
- for (const [key, prompt] of Object.entries(commonFields)) {
167
- if (!webviewConfig[key]) {
168
- webviewConfig[key] = await promptUser(prompt);
169
- }
170
- }
171
-
172
- // Handle platform-specific fields
441
+ // Fields with a sure answer (detected or conventional) are filled without
442
+ // asking; only genuinely unknown ones reach the prompt.
443
+ const autoFill = new Set(["port", "buildType", "sdkPath"]);
444
+ const fill = async (target, key, prompt) => {
445
+ if (target[key]) return;
446
+ const guess = defaults[key];
447
+ target[key] = guess && autoFill.has(key) ? guess : await promptUser(prompt, {
448
+ defaultValue: guess
449
+ });
450
+ filled.push(key);
451
+ };
452
+ for (const [key, prompt] of Object.entries(commonFields)) await fill(webviewConfig, key, prompt);
173
453
  for (const [key, prompt] of Object.entries(platformConfigs[platform])) {
174
- if (!webviewConfig[platform][key]) {
175
- webviewConfig[platform][key] = await promptUser(prompt);
176
- }
454
+ await fill(webviewConfig[platform], key, prompt);
177
455
  }
178
456
 
179
457
  // Save updated config back to file
180
458
  try {
181
459
  _fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
182
- console.log("Configuration updated successfully.");
460
+ if (filled.length) {
461
+ // Under a live spinner a plain log lands on the spinner's own row.
462
+ await withSpinnersPaused(async () => console.log(`${GUTTER}${t.accent("›")} Saved ${filled.join(", ")} to ${t.accent("config/config.json")}`));
463
+ }
183
464
  } catch (error) {
184
465
  console.error(`Error saving config file: ${error.message}`);
185
466
  throw error;
package/dist/otel.js CHANGED
@@ -16,6 +16,12 @@ export const IS_BOT_KEY = createContextKey("catalyst.is_bot")
16
16
  import semanticConventions from "@opentelemetry/semantic-conventions"
17
17
  const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, ATTR_DEPLOYMENT_ENVIRONMENT } = semanticConventions
18
18
 
19
+ // The app-wide logger only exists once the app calls configureLogger() from
20
+ // catalyst-core/logger, which nothing in core does for it. Resolve it per call,
21
+ // so a logger configured after this module loads is still picked up, and fall
22
+ // back to console so init() never throws a ReferenceError on a bare `logger`.
23
+ const getLogger = () => global.logger ?? console
24
+
19
25
  function init(config = {}) {
20
26
  // OpenTelemetry is opt-in — bail out unless explicitly enabled. Mirrors the
21
27
  // OTEL_ENABLE guard in server/expressServer.js and server/renderer/handler.jsx
@@ -84,7 +90,7 @@ function init(config = {}) {
84
90
  const sdk = new NodeSDK(sdkConfig)
85
91
 
86
92
  sdk.start()
87
- logger.info("✅ OpenTelemetry started successfully")
93
+ getLogger().info("✅ OpenTelemetry started successfully")
88
94
 
89
95
  // Initialize custom metrics only if metrics are enabled
90
96
  let meter = null
@@ -93,11 +99,11 @@ function init(config = {}) {
93
99
  }
94
100
 
95
101
  const gracefulShutdown = (signal) => {
96
- logger.info(`📡 Received ${signal}, shutting down OpenTelemetry gracefully...`)
102
+ getLogger().info(`📡 Received ${signal}, shutting down OpenTelemetry gracefully...`)
97
103
  sdk.shutdown()
98
- .then(() => logger.info("✅ OpenTelemetry shutdown completed"))
104
+ .then(() => getLogger().info("✅ OpenTelemetry shutdown completed"))
99
105
  .catch((error) => {
100
- logger.error("❌ Error terminating OpenTelemetry:", error)
106
+ getLogger().error("❌ Error terminating OpenTelemetry:", error)
101
107
  })
102
108
  .finally(() => process.exit())
103
109
  }
@@ -107,7 +113,7 @@ function init(config = {}) {
107
113
 
108
114
  return { sdk, meter }
109
115
  } catch (error) {
110
- logger.error("❌ Failed to initialize OpenTelemetry:", error)
116
+ getLogger().error("❌ Failed to initialize OpenTelemetry:", error)
111
117
  throw error
112
118
  }
113
119
  }
@@ -122,13 +128,13 @@ function init(config = {}) {
122
128
  */
123
129
  function createTraceExporter(protocol, url, headers = {}, grpcCredentials) {
124
130
  if (protocol.toLowerCase() === "http") {
125
- logger.info(`📡 Creating HTTP trace exporter for URL: ${url}`)
131
+ getLogger().info(`📡 Creating HTTP trace exporter for URL: ${url}`)
126
132
  return new OTLPTraceExporterHTTP({
127
133
  url: url,
128
134
  headers: headers,
129
135
  })
130
136
  } else if (protocol.toLowerCase() === "grpc") {
131
- logger.info(`📡 Creating gRPC trace exporter for URL: ${url}`)
137
+ getLogger().info(`📡 Creating gRPC trace exporter for URL: ${url}`)
132
138
  return new OTLPTraceExporter({
133
139
  url: url,
134
140
  headers: headers,
@@ -151,13 +157,13 @@ function createTraceExporter(protocol, url, headers = {}, grpcCredentials) {
151
157
  */
152
158
  function createMetricExporter(protocol, url, headers = {}, grpcCredentials) {
153
159
  if (protocol.toLowerCase() === "http") {
154
- logger.info(`📊 Creating HTTP metric exporter for URL: ${url}`)
160
+ getLogger().info(`📊 Creating HTTP metric exporter for URL: ${url}`)
155
161
  return new OTLPMetricExporterHTTP({
156
162
  url: url,
157
163
  headers: headers,
158
164
  })
159
165
  } else if (protocol.toLowerCase() === "grpc") {
160
- logger.info(`📊 Creating gRPC metric exporter for URL: ${url}`)
166
+ getLogger().info(`📊 Creating gRPC metric exporter for URL: ${url}`)
161
167
  return new OTLPMetricExporter({
162
168
  url: url,
163
169
  headers: headers,