catalyst-core 1.0.2-canary.1 → 1.0.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 (90) hide show
  1. package/bin/catalyst.js +105 -1
  2. package/changelog.md +34 -0
  3. package/dist/cli/README.md +119 -0
  4. package/dist/cli/appConfig.js +51 -0
  5. package/dist/cli/buildErrors.js +170 -0
  6. package/dist/cli/buildWarnings.js +28 -0
  7. package/dist/cli/devOutput.js +524 -0
  8. package/dist/cli/diagnostic.js +388 -0
  9. package/dist/cli/hints.js +69 -0
  10. package/dist/cli/package.json +3 -0
  11. package/dist/cli/spinner.js +141 -0
  12. package/dist/cli/theme.js +155 -0
  13. package/dist/errors/index.js +7 -3
  14. package/dist/logger.js +61 -6
  15. package/dist/native/androidProject/app/build.gradle.kts +2 -1
  16. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +2 -0
  17. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +9 -0
  18. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +65 -34
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt +109 -0
  20. package/dist/native/androidSetup.js +46 -57
  21. package/dist/native/buildAndroid/assets.js +22 -13
  22. package/dist/native/buildAndroid/build.js +98 -26
  23. package/dist/native/buildAndroid/config.js +43 -12
  24. package/dist/native/buildAndroid/index.js +49 -191
  25. package/dist/native/buildAppAndroid.js +19 -8
  26. package/dist/native/buildAppIos.js +35 -16
  27. package/dist/native/buildIos/assets.js +1 -6
  28. package/dist/native/buildIos/build.js +66 -95
  29. package/dist/native/buildIos/config.js +26 -8
  30. package/dist/native/buildIos/index.js +143 -64
  31. package/dist/native/buildIos/plugins.js +12 -15
  32. package/dist/native/internalPluginUtils.js +0 -1
  33. package/dist/native/iosnativeWebView/CoreLogic/Sources/CatalystCoreLogic/Constants/ConfigConstants.swift +2 -0
  34. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashHTMLView.swift +31 -0
  35. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +17 -1
  36. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +41 -1
  37. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +2 -0
  38. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +3 -0
  39. package/dist/native/pluginComposerAndroid.js +2 -3
  40. package/dist/native/renameAndroidProject.js +42 -20
  41. package/dist/native/setupEmulatorIos.js +28 -49
  42. package/dist/native/setupServer.js +12 -15
  43. package/dist/native/terminalProgress.js +186 -147
  44. package/dist/native/utils.js +340 -54
  45. package/dist/otel.js +15 -9
  46. package/dist/scripts/build.js +313 -42
  47. package/dist/scripts/generateOfflineManifest.js +11 -1
  48. package/dist/scripts/loadEnvironmentVariables.js +24 -2
  49. package/dist/scripts/preServerInit.js +32 -10
  50. package/dist/scripts/preflight.js +35 -11
  51. package/dist/scripts/scriptUtils.js +34 -0
  52. package/dist/scripts/serve.js +64 -15
  53. package/dist/scripts/start.js +58 -21
  54. package/dist/server/expressServer.js +167 -32
  55. package/dist/server/renderer/handler.jsx +87 -11
  56. package/dist/server/utils/validator.js +14 -11
  57. package/dist/vite/manifest-categorization-plugin.js +7 -5
  58. package/dist/vite/resolveDevServerConfig.js +9 -2
  59. package/dist/vite/vite.config.client.js +18 -0
  60. package/dist/vite/vite.config.js +73 -13
  61. package/dist/vite/vite.config.server.js +21 -0
  62. package/dist/web-router/components/MetaTag.jsx +15 -2
  63. package/dist/web-router/components/MetaTag.test.jsx +71 -2
  64. package/dist/webmcp/WebMcpProvider.jsx +3 -2
  65. package/dist/webmcp/useTool.js +4 -1
  66. package/mcp_v2/conversion-tasks.json +6 -5
  67. package/mcp_v2/knowledge-base.json +124 -7
  68. package/mcp_v2/test/config.test.ts +59 -0
  69. package/mcp_v2/tools/build.js +6 -6
  70. package/mcp_v2/tools/config.js +35 -18
  71. package/mcp_v2/tools/conversion.js +7 -13
  72. package/package.json +5 -5
  73. package/test/cli/buildErrors.test.ts +173 -0
  74. package/test/cli/buildWarnings.test.cjs +62 -0
  75. package/test/cli/devOutput.test.ts +549 -0
  76. package/test/cli/diagnostic.test.ts +434 -0
  77. package/test/cli/output.test.cjs +90 -0
  78. package/test/cli/spinner.test.ts +177 -0
  79. package/test/cli/theme.test.ts +163 -0
  80. package/test/native/redactSecrets.test.ts +72 -0
  81. package/test/native/utils.test.ts +532 -0
  82. package/test/scripts/build.test.ts +275 -0
  83. package/test/scripts/serve.test.ts +102 -0
  84. package/test/server/fixtures/template/src/js/routes/index.jsx +8 -0
  85. package/test/server/handler.test.ts +17 -0
  86. package/test/server/logger.test.ts +185 -0
  87. package/test/server/otel.test.ts +556 -0
  88. package/test/server/validator.test.ts +19 -8
  89. package/dist/scripts/checkVersion.js +0 -3
  90. 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 {
90
+ // stderr is captured, not inherited. Many tools (java -version, for
91
+ // one) write to stderr on success, and an inherited fd puts that text
92
+ // at column 0 -- straight through whatever the spinner is drawing.
93
+ // The caller decides what, if anything, is worth showing.
22
94
  // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Native setup/build uses fixed internal command strings; preserve existing shell behavior.
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,241 @@ 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"),
191
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
192
+ env.LOCALAPPDATA && _path.default.join(env.LOCALAPPDATA, "Android", "Sdk")].filter(Boolean);
193
+ const adb = process.platform === "win32" ? "adb.exe" : "adb";
194
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
195
+ return candidates.find(dir => _fs.default.existsSync(_path.default.join(dir, "platform-tools", adb))) || null;
196
+ }
197
+
198
+ /** AVD names known to the SDK's emulator, or [] when it cannot be asked. */
199
+ function listAvds(sdkPath) {
200
+ if (!sdkPath) return [];
201
+ try {
202
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process, javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - sdkPath is a local SDK directory from config or auto-detection, never remote input.
203
+ const out = (0, _child_process.execSync)(`"${_path.default.join(expandHome(sdkPath), "emulator", "emulator")}" -list-avds`, {
204
+ encoding: "utf8",
205
+ stdio: ["ignore", "pipe", "ignore"],
206
+ timeout: 15000
47
207
  });
48
- });
208
+ return out.split("\n").map(line => line.trim()).filter(line => line && !line.startsWith("INFO"));
209
+ } catch (error) {
210
+ return [];
211
+ }
212
+ }
213
+
214
+ /** Expand a leading `~` (as in the SDK hint below); nothing else in the chain does. */
215
+ function expandHome(dir) {
216
+ if (typeof dir !== "string") return dir;
217
+ if (dir === "~") return _os.default.homedir();
218
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
219
+ return dir.startsWith("~/") || dir.startsWith("~\\") ? _path.default.join(_os.default.homedir(), dir.slice(2)) : dir;
220
+ }
221
+
222
+ /** The hint for a missing or wrong SDK path, naming the one field to set. */
223
+ 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").';
224
+
225
+ /**
226
+ * Write the SDK location gradle reads. `setupEmulator:android` used to be the
227
+ * only thing that wrote it, so a `buildApp:android` without a prior setup died
228
+ * in gradle with "SDK location not found" despite a configured sdkPath.
229
+ */
230
+ function ensureLocalProperties(projectDir, sdkPath) {
231
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - Paths come from env vars, the user's own SDK location or the project dir, never remote input.
232
+ const file = _path.default.join(projectDir, "local.properties");
233
+ const line = `sdk.dir=${expandHome(sdkPath).replace(/\\/g, "\\\\")}`;
234
+ let content = "";
235
+ try {
236
+ content = _fs.default.readFileSync(file, "utf8");
237
+ } catch (error) {
238
+ // Missing is the case being fixed.
239
+ }
240
+ if (content.split("\n").some(existing => existing.trim() === line)) return false;
241
+ const kept = content.split("\n").filter(existing => existing.trim() && !existing.startsWith("sdk.dir="));
242
+ _fs.default.writeFileSync(file, [...kept, line].join("\n") + "\n");
243
+ return true;
49
244
  }
50
- async function runInteractiveCommand(command, args, promptResponses = {}) {
245
+
246
+ /**
247
+ * Run a command, keeping its output rather than printing it.
248
+ *
249
+ * @param {string} command
250
+ * @param {string[]} args
251
+ * @param {object} [promptResponses] - text to answer prompts with
252
+ * @param {object} [options]
253
+ * @param {(line: string) => void} [options.onLine] - called per output line.
254
+ * Without it the output is captured silently. Gradle and xcodebuild emit
255
+ * hundreds of lines that nobody reads on success and that are redundant on
256
+ * failure -- the error path mines `error.output`, which is retained either
257
+ * way. Callers that want progress pass a handler that summarises, rather
258
+ * than letting the raw stream scroll.
259
+ */
260
+ async function runInteractiveCommand(command, args, promptResponses = {}, {
261
+ onLine
262
+ } = {}) {
51
263
  return new Promise((resolve, reject) => {
264
+ // Named `child`, not `process`: the previous name shadowed the global
265
+ // `process` for the whole function body.
52
266
  // 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, {
267
+ const child = (0, _child_process.spawn)(command, args, {
54
268
  stdio: ["pipe", "pipe", "pipe"]
55
269
  });
56
- let buffer = "";
270
+ let promptBuffer = "";
57
271
  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);
272
+ const partial = {
273
+ stdout: "",
274
+ stderr: ""
275
+ };
276
+ const consume = (data, stream) => {
277
+ const text = data.toString();
278
+
279
+ // Hold back a trailing partial line until its newline arrives.
280
+ // Printing raw chunks splits lines mid-write and injects blanks.
281
+ const lines = (partial[stream] + text).split("\n");
282
+ partial[stream] = lines.pop();
283
+ if (onLine) {
284
+ for (const line of lines) onLine(line);
285
+ }
286
+
287
+ // Previously unbounded: a long gradle build retained every byte.
288
+ outputBuffer += text;
289
+ if (outputBuffer.length > MAX_OUTPUT_BUFFER_BYTES) {
290
+ outputBuffer = outputBuffer.slice(-MAX_OUTPUT_BUFFER_BYTES);
291
+ }
292
+ promptBuffer = handlePrompts(child, promptBuffer + text, promptResponses);
293
+ if (promptBuffer.length > MAX_PROMPT_MATCH_BYTES) {
294
+ promptBuffer = promptBuffer.slice(-MAX_PROMPT_MATCH_BYTES);
295
+ }
296
+ };
297
+ child.stdout.on("data", data => consume(data, "stdout"));
298
+ child.stderr.on("data", data => consume(data, "stderr"));
299
+
300
+ // Without this the promise never settles when the binary is missing or
301
+ // not executable, so a missing gradle/adb hangs the CLI indefinitely.
302
+ child.on("error", error => {
303
+ if (error.code === "ENOENT") {
304
+ reject(new Error(`Command not found: ${command}`, {
305
+ cause: error
306
+ }));
307
+ } else {
308
+ reject(new Error(`Failed to start ${command}: ${error.message}`, {
309
+ cause: error
310
+ }));
311
+ }
69
312
  });
70
- process.on("close", code => {
313
+ child.on("close", code => {
314
+ // Flush whatever never got its closing newline.
315
+ for (const stream of ["stdout", "stderr"]) {
316
+ if (partial[stream]) {
317
+ if (onLine) onLine(partial[stream]);
318
+ partial[stream] = "";
319
+ }
320
+ }
71
321
  if (code === 0) {
72
322
  resolve(outputBuffer);
73
- } else {
74
- reject(new Error(`Command failed with exit code ${code}`));
323
+ return;
75
324
  }
325
+
326
+ // Carry the output on the error. Without it the caller only has
327
+ // "exit code 1" and the compiler error that actually broke the
328
+ // build is lost -- which is why failures used to mean opening
329
+ // Android Studio to find out what happened.
330
+ const failure = new Error(`Command failed with exit code ${code}`);
331
+ failure.code = code;
332
+ failure.output = outputBuffer;
333
+ reject(failure);
76
334
  });
77
335
  });
78
336
  }
79
- function handlePrompts(process, buffer, promptResponses) {
337
+
338
+ /**
339
+ * Answer a prompt found in the accumulated output.
340
+ *
341
+ * Returns the buffer to keep looking in. The match must be consumed: the buffer
342
+ * grows with every chunk, so leaving a matched prompt in it re-answers that
343
+ * prompt on every subsequent chunk, spraying `y\n` into the child's stdin.
344
+ */
345
+ function handlePrompts(child, buffer, promptResponses) {
80
346
  for (const [prompt, response] of Object.entries(promptResponses)) {
81
- if (buffer.includes(prompt)) {
82
- process.stdin.write(response + "\n");
83
- return;
347
+ const index = buffer.indexOf(prompt);
348
+ if (index !== -1) {
349
+ child.stdin.write(response + "\n");
350
+ return buffer.slice(index + prompt.length);
84
351
  }
85
352
  }
353
+ return buffer;
86
354
  }
87
355
  async function runSdkManagerCommand(sdkManagerPath, args) {
88
356
  const promptResponses = {
@@ -110,22 +378,35 @@ async function validateAndCompleteConfig(platform, configPath) {
110
378
  // Check if WEBVIEW_CONFIG already has all required fields
111
379
  const webviewConfig = config.WEBVIEW_CONFIG;
112
380
  const commonFields = {
113
- port: "Enter port number (e.g., 3005): "
381
+ port: "Port the app is served on"
114
382
  };
383
+
384
+ // cachePattern is optional -- the iOS build emits an empty pattern list
385
+ // when it is absent and Android never required it -- so asking for it on
386
+ // every first setup only stalled people on a question they could not answer.
115
387
  const platformConfigs = {
116
388
  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): "
389
+ buildType: "Android build type (debug/release)",
390
+ sdkPath: "Android SDK path",
391
+ emulatorName: "Android emulator (AVD) name"
121
392
  },
122
393
  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): "
394
+ buildType: "iOS build type (debug/release)",
395
+ appBundleId: "iOS bundle ID (e.g. com.example.app)",
396
+ simulatorName: "iOS simulator name (e.g. iPhone 16 Pro)"
127
397
  }
128
398
  };
399
+
400
+ // Answers we can work out are filled in rather than asked; the rest are
401
+ // asked with the best guess offered as the default.
402
+ const sdkGuess = platform === "android" ? webviewConfig.android?.sdkPath || detectAndroidSdk() : null;
403
+ const defaults = {
404
+ port: config.NODE_SERVER_PORT ? String(config.NODE_SERVER_PORT) : undefined,
405
+ buildType: "debug",
406
+ sdkPath: sdkGuess || undefined,
407
+ emulatorName: platform === "android" ? listAvds(sdkGuess)[0] : undefined
408
+ };
409
+ const filled = [];
129
410
  if (!platformConfigs[platform]) {
130
411
  throw new Error('Invalid platform. Must be either "android" or "ios"');
131
412
  }
@@ -162,24 +443,29 @@ async function validateAndCompleteConfig(platform, configPath) {
162
443
  };
163
444
  }
164
445
 
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
446
+ // Fields with a sure answer (detected or conventional) are filled without
447
+ // asking; only genuinely unknown ones reach the prompt.
448
+ const autoFill = new Set(["port", "buildType", "sdkPath"]);
449
+ const fill = async (target, key, prompt) => {
450
+ if (target[key]) return;
451
+ const guess = defaults[key];
452
+ target[key] = guess && autoFill.has(key) ? guess : await promptUser(prompt, {
453
+ defaultValue: guess
454
+ });
455
+ filled.push(key);
456
+ };
457
+ for (const [key, prompt] of Object.entries(commonFields)) await fill(webviewConfig, key, prompt);
173
458
  for (const [key, prompt] of Object.entries(platformConfigs[platform])) {
174
- if (!webviewConfig[platform][key]) {
175
- webviewConfig[platform][key] = await promptUser(prompt);
176
- }
459
+ await fill(webviewConfig[platform], key, prompt);
177
460
  }
178
461
 
179
462
  // Save updated config back to file
180
463
  try {
181
464
  _fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
182
- console.log("Configuration updated successfully.");
465
+ if (filled.length) {
466
+ // Under a live spinner a plain log lands on the spinner's own row.
467
+ await withSpinnersPaused(async () => console.log(`${GUTTER}${t.accent("›")} Saved ${filled.join(", ")} to ${t.accent("config/config.json")}`));
468
+ }
183
469
  } catch (error) {
184
470
  console.error(`Error saving config file: ${error.message}`);
185
471
  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,