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
@@ -4,7 +4,8 @@ const fs = require("fs");
4
4
  const os = require("os");
5
5
  const path = require("path");
6
6
  const {
7
- execSync
7
+ execSync,
8
+ execFileSync
8
9
  } = require("child_process");
9
10
  const {
10
11
  runInteractiveCommand
@@ -16,9 +17,9 @@ module.exports = function createBuildPhase(ctx) {
16
17
  SCHEME_NAME,
17
18
  APP_BUNDLE_ID,
18
19
  IPHONE_MODEL,
19
- url,
20
20
  progress,
21
- runCommand
21
+ runCommand,
22
+ runCommandStreaming
22
23
  } = ctx;
23
24
 
24
25
  // ─── Simulator helpers ────────────────────────────────────────────────────
@@ -32,24 +33,21 @@ module.exports = function createBuildPhase(ctx) {
32
33
  if (line.includes(modelName) && line.includes("Booted")) {
33
34
  const m = line.match(UUID_RE);
34
35
  if (m) {
35
- console.log(`Found booted simulator of model ${modelName}`);
36
36
  return m[0];
37
37
  }
38
38
  }
39
39
  }
40
- console.log(`No booted simulator of model ${modelName} found, checking for any booted simulator...`);
40
+ progress.status("looking for any booted simulator");
41
41
  for (const line of lines) {
42
42
  if (line.includes("Booted")) {
43
43
  const m = line.match(UUID_RE);
44
44
  if (m) {
45
- console.log("Found another booted simulator, will use it instead");
46
45
  return m[0];
47
46
  }
48
47
  }
49
48
  }
50
49
  return null;
51
50
  } catch (error) {
52
- console.log("No booted simulators found");
53
51
  return null;
54
52
  }
55
53
  }
@@ -67,7 +65,6 @@ module.exports = function createBuildPhase(ctx) {
67
65
  }
68
66
  return null;
69
67
  } catch (error) {
70
- console.log("Failed to get booted simulator info:", error.message);
71
68
  return null;
72
69
  }
73
70
  }
@@ -143,32 +140,30 @@ module.exports = function createBuildPhase(ctx) {
143
140
  async function buildProject(scheme, sdk, destination, bundleId, derivedDataPath, projectName) {
144
141
  const bootedInfo = await getBootedSimulatorInfo();
145
142
  if (!bootedInfo) throw new Error("No booted simulator found");
146
- console.log(`Building with destination: platform=iOS Simulator,id=${bootedInfo.udid}`);
143
+ progress.status(`simulator ${bootedInfo.udid.slice(0, 8)}`);
147
144
  const isNotificationsEnabled = WEBVIEW_CONFIG.notifications?.enabled ?? false;
148
145
  progress.log(isNotificationsEnabled ? "Building with notifications enabled (CatalystNotifications module included)" : "Building without notifications (Firebase excluded)", "info");
149
- return runCommand(makeBuildCommand(scheme, sdk, bundleId, derivedDataPath, projectName, bootedInfo.udid, "build"), {
150
- maxBuffer: 1024 * 1024 * 10
151
- });
146
+ // Streamed: xcodebuild runs for minutes, and buffered output made that
147
+ // look like a hang with nothing on screen until it finished.
148
+ return runCommandStreaming(makeBuildCommand(scheme, sdk, bundleId, derivedDataPath, projectName, bootedInfo.udid, "build"));
152
149
  }
153
150
  async function buildProjectForTesting(scheme, sdk, destination, bundleId, derivedDataPath, projectName) {
154
151
  const bootedInfo = await getBootedSimulatorInfo();
155
152
  if (!bootedInfo) throw new Error("No booted simulator found");
156
- console.log(`Building for testing with destination: platform=iOS Simulator,id=${bootedInfo.udid}`);
153
+ progress.status(`simulator ${bootedInfo.udid.slice(0, 8)}`);
157
154
  const isNotificationsEnabled = WEBVIEW_CONFIG.notifications?.enabled ?? false;
158
155
  progress.log(isNotificationsEnabled ? "Building for testing with notifications enabled" : "Building for testing without notifications", "info");
159
- return runCommand(makeBuildCommand(scheme, sdk, bundleId, derivedDataPath, projectName, bootedInfo.udid, "build-for-testing"), {
160
- maxBuffer: 1024 * 1024 * 10
161
- });
156
+ return runCommandStreaming(makeBuildCommand(scheme, sdk, bundleId, derivedDataPath, projectName, bootedInfo.udid, "build-for-testing"));
162
157
  }
163
158
  async function buildXcodeProject() {
164
159
  progress.start("build");
165
160
  try {
166
161
  const derivedDataPath = path.join(os.homedir(), "Library/Developer/Xcode/DerivedData");
167
162
  try {
168
- progress.log("Building for specified simulator...", "info");
163
+ progress.status("building for specified simulator");
169
164
  await buildProject(SCHEME_NAME, "iphonesimulator", `platform=iOS Simulator,name=${IPHONE_MODEL}`, APP_BUNDLE_ID, derivedDataPath, PROJECT_NAME);
170
165
  } catch (buildError) {
171
- progress.log("Initial build failed, attempting fallback...", "warning");
166
+ progress.log("Initial build failed, attempting fallback...", "info");
172
167
  const bootedUUID = await getBootedSimulatorUUID(IPHONE_MODEL);
173
168
  if (bootedUUID) {
174
169
  progress.log(`Found booted simulator, retrying build...`, "info");
@@ -180,32 +175,6 @@ module.exports = function createBuildPhase(ctx) {
180
175
  progress.complete("build");
181
176
  } catch (error) {
182
177
  progress.fail("build", error.message);
183
- progress.printTreeContent("Troubleshooting Guide", ["Build failed. Please try the following steps:", {
184
- text: 'Run "npm run setupEmulator:ios" to reconfigure iOS settings',
185
- indent: 1,
186
- prefix: "├─ ",
187
- color: "yellow"
188
- }, {
189
- text: "Check if Xcode is properly installed and updated",
190
- indent: 1,
191
- prefix: "├─ ",
192
- color: "yellow"
193
- }, {
194
- text: "Verify selected simulator exists",
195
- indent: 1,
196
- prefix: "└─ ",
197
- color: "yellow"
198
- }, "\nVerify Configuration:", {
199
- text: `Selected Simulator: ${IPHONE_MODEL}`,
200
- indent: 1,
201
- prefix: "├─ ",
202
- color: "gray"
203
- }, {
204
- text: `Server URL: ${url}`,
205
- indent: 1,
206
- prefix: "└─ ",
207
- color: "gray"
208
- }]);
209
178
  throw error;
210
179
  }
211
180
  }
@@ -221,7 +190,7 @@ module.exports = function createBuildPhase(ctx) {
221
190
  APP_PATH = execSync(`find "${DERIVED_DATA_DIR}" -path "*${PROJECT_NAME}-*" -prune -not -path "*/Index.noindex*" -path "*/Build/Products/Debug-iphonesimulator/${PROJECT_NAME}.app" -type d | head -n 1` // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - DERIVED_DATA_DIR and PROJECT_NAME are internal iOS build paths.
222
191
  ).toString().trim();
223
192
  } catch (error) {
224
- progress.log("Primary app path search failed, trying fallback...", "warning");
193
+ progress.log("Primary app path search failed, trying fallback...", "info");
225
194
  }
226
195
  if (!APP_PATH) {
227
196
  try {
@@ -248,13 +217,15 @@ module.exports = function createBuildPhase(ctx) {
248
217
  const buildType = iosConfig.buildType || "debug";
249
218
  const appName = iosConfig.appName || "app";
250
219
  const currentDate = new Date().toLocaleDateString("en-GB").replace(/\//g, "-");
251
- const currentTime = new Date().toLocaleTimeString("en-US", {
252
- hour12: true,
253
- hour: "2-digit",
254
- minute: "2-digit",
255
- second: "2-digit"
256
- }).replace(/:/g, "-");
257
- const destinationDir = path.join(process.cwd(), BUILD_OUTPUT_PATH,
220
+ // 24-hour and locale-free: "02-05-09 PM" put a space in every
221
+ // artifact name, which breaks unquoted shell use of the path.
222
+ const currentTime = new Date().toTimeString().slice(0, 8).replace(/:/g, "-");
223
+ // The app root, not process.cwd(): the iOS build chdirs into the
224
+ // Xcode project inside node_modules, so the artifact landed there
225
+ // and was wiped by the next npm install.
226
+ const destinationDir = path.join(
227
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the local project root from the CLI context.
228
+ ctx.appRoot || process.cwd(), BUILD_OUTPUT_PATH,
258
229
  // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - BUILD_OUTPUT_PATH is trusted application config.
259
230
  "native", "ios", currentDate, buildType // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - buildType is "debug" or "release" from config.
260
231
  );
@@ -270,24 +241,21 @@ module.exports = function createBuildPhase(ctx) {
270
241
  }
271
242
  }
272
243
  async function uninstallExistingApp() {
273
- console.log("Uninstalling the app if it exists...");
244
+ progress.status("uninstalling previous build");
274
245
  await runCommand(`xcrun simctl uninstall booted "${APP_BUNDLE_ID}"`);
275
246
  }
276
247
  async function installApp(APP_PATH) {
277
- console.log("Installing the app...");
278
- try {
279
- await runCommand(`xcrun simctl install booted "${APP_PATH}"`);
280
- } catch (error) {
281
- console.error("Error installing the app:", error);
282
- throw error;
283
- }
248
+ progress.status("installing");
249
+ await runCommand(`xcrun simctl install booted "${APP_PATH}"`);
284
250
  }
285
251
  async function waitForInstallation() {
286
- console.log("Waiting for the app to be fully installed...");
252
+ progress.status("waiting for install");
287
253
  for (let i = 0; i < 30; i++) {
288
254
  try {
289
- execSync(`xcrun simctl get_app_container booted "${APP_BUNDLE_ID}"`); // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - APP_BUNDLE_ID is generated from trusted native config.
290
- console.log("App installed successfully.");
255
+ // No shell: the bundle id is passed as an argv entry, never interpolated.
256
+ execFileSync("xcrun", ["simctl", "get_app_container", "booted", APP_BUNDLE_ID], {
257
+ stdio: ["ignore", "pipe", "ignore"]
258
+ });
291
259
  break;
292
260
  } catch (error) {
293
261
  if (i === 29) throw new Error("Timeout: App installation took too long.");
@@ -296,32 +264,32 @@ module.exports = function createBuildPhase(ctx) {
296
264
  }
297
265
  }
298
266
  async function handleLaunchError(error) {
299
- console.error("Error launching the app:", error);
300
- console.log("Checking app container...");
267
+ progress.status("checking app container");
301
268
  try {
302
269
  await runCommand(`xcrun simctl get_app_container booted "${APP_BUNDLE_ID}"`);
303
270
  } catch {
304
- console.log("App container not found");
271
+ progress.log("App container not found", "warning");
305
272
  }
306
273
  throw error;
307
274
  }
308
275
  async function verifyAppLaunch() {
309
- console.log("Waiting for the app to launch...");
276
+ progress.status("waiting for launch");
310
277
  for (let i = 0; i < 10; i++) {
311
278
  try {
312
- const launchResult = execSync(`xcrun simctl launch booted "${APP_BUNDLE_ID}"`).toString(); // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
279
+ const launchResult = execFileSync("xcrun", ["simctl", "launch", "booted", APP_BUNDLE_ID], {
280
+ stdio: ["ignore", "pipe", "ignore"]
281
+ }).toString();
313
282
  if (launchResult.includes("already launched")) {
314
- console.log("App launched successfully.");
315
283
  break;
316
284
  }
317
285
  } catch (error) {
318
- if (i === 9) console.log("Warning: App launch might have failed or taken too long.");
286
+ if (i === 9) progress.log("App launch may have failed or timed out", "warning");
319
287
  }
320
288
  await new Promise(resolve => setTimeout(resolve, 1000));
321
289
  }
322
290
  }
323
291
  async function launchAndVerifyApp() {
324
- console.log("Launching the app...");
292
+ progress.status("launching");
325
293
  try {
326
294
  await runCommand(`xcrun simctl launch booted "${APP_BUNDLE_ID}"`);
327
295
  await verifyAppLaunch();
@@ -330,7 +298,7 @@ module.exports = function createBuildPhase(ctx) {
330
298
  }
331
299
  }
332
300
  async function focusSimulator() {
333
- console.log("Focusing on Simulator...");
301
+ progress.status("focusing simulator");
334
302
  const simulatorAppBundleId = resolveSimulatorAppBundleId();
335
303
  if (!simulatorAppBundleId) {
336
304
  progress.log(SIMULATOR_APP_NOT_FOUND_WARNING, "warning");
@@ -384,7 +352,10 @@ module.exports = function createBuildPhase(ctx) {
384
352
  function tryDevicectl() {
385
353
  const tmpPath = path.join(os.tmpdir(), `catalyst-devicectl-${process.pid}-${Date.now()}.json`);
386
354
  try {
387
- execSync(`xcrun devicectl list devices --json-output "${tmpPath}"`); // nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - tmpPath is built from os.tmpdir()/process.pid/Date.now(), not user input.
355
+ execSync(`xcrun devicectl list devices --json-output "${tmpPath}"`, {
356
+ stdio: ["ignore", "ignore", "ignore"],
357
+ timeout: 30000
358
+ }); // nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - tmpPath is built from os.tmpdir()/process.pid/Date.now(), not user input.
388
359
  const raw = fs.readFileSync(tmpPath, "utf8");
389
360
  const parsed = JSON.parse(raw);
390
361
  const devices = parsed?.result?.devices ?? [];
@@ -402,7 +373,8 @@ module.exports = function createBuildPhase(ctx) {
402
373
  type: "physical"
403
374
  })).filter(d => typeof d.udid === "string" && VALID_UDID_RE.test(d.udid));
404
375
  } catch (error) {
405
- progress.log(`devicectl detection failed (${error.message}); falling back to xcodebuild destinations`, "warning");
376
+ // A fallback is not a failure the user can act on; keep it for --verbose.
377
+ progress.log(`devicectl detection failed (${error.message}); falling back to xcodebuild destinations`, "info");
406
378
  return [];
407
379
  } finally {
408
380
  try {
@@ -418,21 +390,21 @@ module.exports = function createBuildPhase(ctx) {
418
390
  iosConfig
419
391
  } = ctx;
420
392
  try {
421
- progress.log("Scanning for connected physical devices...", "info");
393
+ progress.status("scanning for connected physical devices");
422
394
  let physicalDevices = [];
423
395
  const configuredUDID = iosConfig.deviceUDID;
424
396
  if (configuredUDID) {
425
397
  progress.log(`Using configured device UDID: ${configuredUDID}`, "info");
426
398
  const configuredDevice = tryDevicectl().find(d => d.udid === configuredUDID);
427
399
  if (configuredDevice) {
428
- progress.log(`✅ Found configured physical device: ${configuredDevice.name} (${configuredDevice.version})`, "success");
400
+ progress.log(`Found configured physical device: ${configuredDevice.name} (${configuredDevice.version})`, "success");
429
401
  progress.complete("deviceDetection");
430
402
  return configuredDevice;
431
403
  }
432
- progress.log(`⚠️ Configured device UDID not found in connected devices`, "warning");
404
+ progress.log(`Configured device UDID not found in connected devices`, "warning");
433
405
  progress.log("Falling back to auto-detection...", "info");
434
406
  } else {
435
- progress.log("No device UDID configured, using auto-detection", "info");
407
+ progress.status("no device udid configured, using auto-detection");
436
408
  }
437
409
  physicalDevices = tryDevicectl();
438
410
  if (physicalDevices.length > 0) progress.log(`Found ${physicalDevices.length} physical device(s) via devicectl`, "success");
@@ -440,14 +412,14 @@ module.exports = function createBuildPhase(ctx) {
440
412
  try {
441
413
  const xcodebuildOutput = execSync(`xcodebuild -scheme "${SCHEME_NAME}" -showdestinations` // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - SCHEME_NAME is generated from trusted iOS project config.
442
414
  ).toString();
443
- progress.log("Scanning xcodebuild destinations for physical devices...", "info");
415
+ progress.status("scanning xcodebuild destinations for physical devices");
444
416
  for (const line of xcodebuildOutput.split("\n")) {
445
417
  const m = line.match(/\{\s*platform:iOS,\s*arch:(\w+),\s*id:([A-F0-9-]+),\s*name:(.+?)\s*\}/);
446
418
  if (m) {
447
419
  const [, arch, udid, name] = m;
448
420
  progress.log(`Found device candidate: ${name.trim()} - ${udid}`, "info");
449
421
  if (!udid.includes("placeholder") && udid.length > 20) {
450
- progress.log(`✅ Valid physical device: ${name.trim()}`, "success");
422
+ progress.log(`Valid physical device: ${name.trim()}`, "success");
451
423
  physicalDevices.push({
452
424
  name: name.trim(),
453
425
  version: "Unknown",
@@ -456,22 +428,23 @@ module.exports = function createBuildPhase(ctx) {
456
428
  type: "physical"
457
429
  });
458
430
  } else {
459
- progress.log(`❌ Skipping placeholder: ${name.trim()}`, "warning");
431
+ progress.log(`Skipping placeholder: ${name.trim()}`, "info");
460
432
  }
461
433
  }
462
434
  }
463
435
  } catch (error) {
464
- progress.log("xcodebuild destinations failed", "warning");
436
+ progress.log("xcodebuild destinations failed", "info");
465
437
  progress.log(`Error: ${error.message}`, "error");
466
438
  }
467
439
  }
468
440
  if (physicalDevices.length > 0) {
469
441
  progress.log(`Found ${physicalDevices.length} physical device(s):`, "success");
470
- physicalDevices.forEach(d => progress.log(` 📱 ${d.name} (${d.version || "Unknown iOS"}) - ${d.udid}`, "info"));
442
+ physicalDevices.forEach(d => progress.log(` ${d.name} (${d.version || "Unknown iOS"}) - ${d.udid}`, "info"));
471
443
  progress.complete("deviceDetection");
472
444
  return physicalDevices[0];
473
445
  } else {
474
- progress.log("No physical devices detected", "warning");
446
+ // The normal case when building for the simulator, not a warning.
447
+ progress.log("No physical devices detected", "info");
475
448
  progress.complete("deviceDetection");
476
449
  return null;
477
450
  }
@@ -494,10 +467,9 @@ module.exports = function createBuildPhase(ctx) {
494
467
  PRODUCT_BUNDLE_IDENTIFIER=${bundleId} \
495
468
  ONLY_ACTIVE_ARCH=YES \
496
469
  build`;
497
- progress.log(`Executing command: ${buildCommand}`, "info");
498
- return runCommand(buildCommand, {
499
- maxBuffer: 1024 * 1024 * 10
500
- });
470
+ // The full command is a dozen wrapped lines of build settings; the
471
+ // streamed output below is what actually tells you how it is going.
472
+ return runCommandStreaming(buildCommand);
501
473
  }
502
474
  async function findPhysicalDeviceAppPath() {
503
475
  const DERIVED_DATA_DIR = path.join(os.homedir(), "Library/Developer/Xcode/DerivedData");
@@ -506,7 +478,7 @@ module.exports = function createBuildPhase(ctx) {
506
478
  APP_PATH = execSync(`find "${DERIVED_DATA_DIR}" -name "${PROJECT_NAME}.app" -path "*/Build/Products/Debug-iphoneos/*" -not -path "*/Index.noindex/*" -type d | head -n 1` // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - DERIVED_DATA_DIR and PROJECT_NAME are internal iOS build paths.
507
479
  ).toString().trim();
508
480
  } catch {
509
- progress.log("Primary app path search failed for physical device, trying fallback...", "warning");
481
+ progress.log("Primary app path search failed for physical device, trying fallback...", "info");
510
482
  }
511
483
  if (!APP_PATH) {
512
484
  try {
@@ -537,7 +509,7 @@ module.exports = function createBuildPhase(ctx) {
537
509
  progress.log("App launched successfully using devicectl", "success");
538
510
  } catch (launchError) {
539
511
  progress.log("App installation completed, but launch failed. You can manually launch the app on your device.", "warning");
540
- progress.log(`To launch manually, look for the app with bundle ID: ${APP_BUNDLE_ID}`, "info");
512
+ progress.log(`To launch manually, look for the app with bundle ID: ${APP_BUNDLE_ID}`, "note");
541
513
  }
542
514
  progress.complete("launch");
543
515
  } catch (error) {
@@ -588,7 +560,7 @@ module.exports = function createBuildPhase(ctx) {
588
560
  async function launchIOSSimulator(simulatorName) {
589
561
  progress.start("launchSimulator");
590
562
  try {
591
- progress.log("Launching iOS Simulator...");
563
+ progress.status("launching simulator");
592
564
  const simulatorsJson = JSON.parse(execSync("xcrun simctl list devices -j").toString());
593
565
  // Multiple devices can share a name across runtimes (e.g. leftover devices
594
566
  // from an old Xcode install after an upgrade) — simctl marks the ones whose
@@ -614,11 +586,11 @@ module.exports = function createBuildPhase(ctx) {
614
586
  });
615
587
  });
616
588
  if (!foundSimulator) {
617
- console.log(`Configured simulator "${simulatorName}" not found.`);
589
+ progress.log(`Configured simulator "${simulatorName}" not found`, "warning");
618
590
  return;
619
591
  }
620
592
  if (!isBooted) {
621
- progress.log(`Attempting to boot simulator: ${simulatorName} (${foundSimulatorId})`, "warning");
593
+ progress.log(`Attempting to boot simulator: ${simulatorName} (${foundSimulatorId})`, "info");
622
594
  if (foundSimulator.state === "Shutdown") {
623
595
  progress.log(`Simulator is shutdown, attempting to boot...`, "info");
624
596
  try {
@@ -642,14 +614,13 @@ module.exports = function createBuildPhase(ctx) {
642
614
  }
643
615
  const simulatorAppBundleId = resolveSimulatorAppBundleId();
644
616
  if (simulatorAppBundleId) {
645
- progress.log("Opening simulator UI...");
617
+ progress.status("opening simulator");
646
618
  await runCommand(`open -b ${simulatorAppBundleId}`);
647
619
  await new Promise(resolve => setTimeout(resolve, 1000));
648
620
  await runCommand(`osascript -e 'tell application id "${simulatorAppBundleId}" to activate'`);
649
621
  } else {
650
622
  progress.log(SIMULATOR_APP_NOT_FOUND_WARNING, "warning");
651
623
  }
652
- progress.log("iOS Simulator launched successfully.", "success");
653
624
  progress.complete("launchSimulator");
654
625
  } catch (error) {
655
626
  progress.fail("launchSimulator", error.message);
@@ -82,6 +82,7 @@ module.exports = function createConfigPhase(ctx) {
82
82
  WEBVIEW_CONFIG,
83
83
  iosConfig,
84
84
  PROJECT_DIR,
85
+ PUBLIC_PATH,
85
86
  url,
86
87
  progress,
87
88
  restoreManagedFileFromBaseline,
@@ -185,18 +186,35 @@ public enum ConfigConstants {
185
186
  configContent += `\n public static let splashScreenImageWidth: CGFloat = ${splashConfig.imageWidth || 120}`;
186
187
  configContent += `\n public static let splashScreenImageHeight: CGFloat = ${splashConfig.imageHeight || 120}`;
187
188
  configContent += `\n public static let splashScreenCornerRadius: CGFloat = ${splashConfig.cornerRadius || 20}`;
189
+
190
+ // Optional HTML splash: public/splashscreen.html is embedded as base64 so no
191
+ // bundle resource or Xcode project registration is needed.
192
+ const splashHtmlPath = path.join(PUBLIC_PATH, "splashscreen.html"); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - PUBLIC_PATH is the app's fixed public directory.
193
+ if (fs.existsSync(splashHtmlPath)) {
194
+ const splashHtml = fs.readFileSync(splashHtmlPath);
195
+ if (splashHtml.length > 1024 * 1024) {
196
+ progress.log(`splashscreen.html is ${(splashHtml.length / (1024 * 1024)).toFixed(1)} MB; it is embedded in the app binary, keep it small`, "warning");
197
+ }
198
+ configContent += `\n public static let splashScreenHtml: String? = "${splashHtml.toString("base64")}"`;
199
+ } else {
200
+ configContent += `\n public static let splashScreenHtml: String? = nil`;
201
+ }
202
+ const minDuration = splashConfig.minDuration;
203
+ const validMinDuration = typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration >= 0;
204
+ if (minDuration !== undefined && !validMinDuration) {
205
+ progress.log(`Ignoring invalid splashScreen.minDuration: ${JSON.stringify(minDuration)}`, "warning");
206
+ }
207
+ configContent += `\n public static let splashScreenMinDuration: TimeInterval = ${validMinDuration ? minDuration / 1000.0 : 0}`;
188
208
  } else {
189
- configContent += `\n\n // Splash Screen Configuration\n public static let splashScreenEnabled = false\n public static let splashScreenDuration: TimeInterval? = nil\n public static let splashScreenBackgroundColor = "#ffffff"\n public static let splashScreenImageWidth: CGFloat = 120\n public static let splashScreenImageHeight: CGFloat = 120\n public static let splashScreenCornerRadius: CGFloat = 20`;
209
+ configContent += `\n\n // Splash Screen Configuration\n public static let splashScreenEnabled = false\n public static let splashScreenDuration: TimeInterval? = nil\n public static let splashScreenBackgroundColor = "#ffffff"\n public static let splashScreenImageWidth: CGFloat = 120\n public static let splashScreenImageHeight: CGFloat = 120\n public static let splashScreenCornerRadius: CGFloat = 20\n public static let splashScreenHtml: String? = nil\n public static let splashScreenMinDuration: TimeInterval = 0`;
190
210
  }
191
211
  if (!addedKeys.has("notifications")) {
192
- progress.log("Notifications not found in config, adding default (false)", "info");
193
212
  configContent += "\n public enum Notifications {\n public static let enabled = false\n }";
194
213
  addedKeys.add("notifications");
195
214
  } else {
196
215
  progress.log("Notifications config was processed from WEBVIEW_CONFIG", "info");
197
216
  }
198
217
  if (!addedKeys.has("googleSignIn")) {
199
- progress.log("Google Sign-In not found in config, adding defaults (disabled)", "info");
200
218
  configContent += '\n public enum GoogleSignIn {\n public static let enabled = false\n public static let clientId = ""\n public static let iosClientId = ""\n }';
201
219
  addedKeys.add("googleSignIn");
202
220
  }
@@ -208,8 +226,6 @@ public enum ConfigConstants {
208
226
  progress.log("EdgeToEdge not found in config, adding default (false)", "info");
209
227
  configContent += "\n public enum EdgeToEdge {\n public static let enabled = false\n }";
210
228
  addedKeys.add("edgeToEdge");
211
- } else {
212
- progress.log("EdgeToEdge config was processed from WEBVIEW_CONFIG", "info");
213
229
  }
214
230
  if (!addedKeys.has("initial_url")) {
215
231
  configContent += '\n public static let initial_url = ""';
@@ -233,7 +249,6 @@ public enum ConfigConstants {
233
249
  }
234
250
  configContent += `\n}`;
235
251
  fs.writeFileSync(appConfigPath, configContent, "utf8");
236
- progress.log("Configuration constants generated successfully (SPM Package)", "success");
237
252
  progress.complete("config");
238
253
  } catch (error) {
239
254
  progress.fail("config", error.message);
@@ -314,9 +329,12 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPort
314
329
  INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
315
330
  `;
316
331
  fs.writeFileSync(xconfigPath, xconfigContent, "utf8");
317
- console.log(`✅ Generated Shared.xcconfig with Swift ${swiftVersion}, iOS ${deploymentTarget}, Bundle ID: ${bundleId}`);
332
+ // Detail the step's own check line already covers, so it rides the
333
+ // spinner row rather than escaping the gutter at column 0.
334
+ progress.status(`xcconfig · Swift ${swiftVersion} · iOS ${deploymentTarget}`);
318
335
  } catch (error) {
319
- console.error(`❌ Failed to generate Shared.xcconfig: ${error.message}`);
336
+ // Keep the original error (its code and stack); only say which file.
337
+ error.message = `Failed to generate Shared.xcconfig: ${error.message}`;
320
338
  throw error;
321
339
  }
322
340
  }