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,6 +4,7 @@
4
4
  var _fs = _interopRequireDefault(require("fs"));
5
5
  var _path = _interopRequireDefault(require("path"));
6
6
  var _utils = require("./utils.js");
7
+ var _theme = require("../cli/theme.js");
7
8
  function _interopRequireDefault(e) {
8
9
  return e && e.__esModule ? e : {
9
10
  default: e
@@ -15,27 +16,45 @@ class SimpleProgress {
15
16
  constructor(title) {
16
17
  this.title = title;
17
18
  this.currentStep = null;
18
- console.log(`\n=== ${title} ===\n`);
19
+ this.startedAt = 0;
20
+ // Deferred: this module is imported by the Android build, which prints
21
+ // its own header. Printing here on import produced two headers, the
22
+ // first of them for work that had not started and often never runs.
23
+ this.headerShown = false;
24
+ }
25
+ ensureHeader() {
26
+ if (this.headerShown) return;
27
+ this.headerShown = true;
28
+ console.log(_theme.header(this.title));
19
29
  }
20
30
  start(stepId) {
31
+ this.ensureHeader();
21
32
  this.currentStep = {
22
33
  id: stepId
23
34
  };
24
- console.log(`⏳ Starting: ${stepId}`);
35
+ this.startedAt = Date.now();
36
+ console.log(`${_theme.GUTTER}${_theme.t.accent(_theme.glyph.running)} ${stepId}`);
25
37
  }
26
38
  complete(stepId) {
27
- console.log(`✅ Completed: ${stepId}`);
39
+ this.ensureHeader();
40
+ console.log(_theme.step(stepId, this.startedAt ? Date.now() - this.startedAt : null));
28
41
  this.currentStep = null;
29
42
  }
30
43
  fail(stepId, message) {
31
- console.log(`❌ Failed: ${stepId} - ${message}`);
44
+ this.ensureHeader();
45
+ console.log(_theme.step(stepId, null, "fail"));
46
+ // Backstop: runCommand already scrubs its errors, but this is where any
47
+ // failure in the signing flow ends up on screen.
48
+ if (message) console.log(`${_theme.GUTTER}${_theme.t.dim(_theme.glyph.last)} ${_theme.t.bad((0, _utils.redactSecrets)(message))}`);
32
49
  }
33
50
  log(message, type = "info") {
34
- const prefix = type === "error" ? "❌" : type === "warning" ? "⚠️ " : type === "success" ? "✅" : "ℹ️ ";
35
- console.log(` ${prefix} ${message}`);
51
+ this.ensureHeader();
52
+ const mark = type === "error" ? _theme.t.bad(_theme.glyph.fail) : type === "warning" ? _theme.t.warn(_theme.glyph.warn) : type === "success" ? _theme.t.ok(_theme.glyph.done) : _theme.t.dim(_theme.glyph.info);
53
+ console.log(`${_theme.GUTTER}${_theme.t.dim(_theme.glyph.pipe)} ${mark} ${message}`);
36
54
  }
37
55
  printTreeContent(title, items) {
38
- console.log(`\n📋 ${title}:`);
56
+ this.ensureHeader();
57
+ console.log(`\n${_theme.GUTTER}${_theme.t.bold(title)}`);
39
58
  items.forEach(item => {
40
59
  if (typeof item === "string") {
41
60
  console.log(item);
@@ -142,7 +161,7 @@ async function createKeystore(projectPaths, androidConfig) {
142
161
  } catch (testError) {
143
162
  throw new Error(`Created keystore failed verification: ${testError.message}`);
144
163
  }
145
- progress.log(`Keystore created successfully at: ${keystorePath}`, "success");
164
+ progress.log(`Keystore created at: ${keystorePath}`, "note");
146
165
  progress.log(`Key alias: ${keystoreConfig.keyAlias}`, "info");
147
166
  progress.log(`Validity: ${keystoreConfig.validityYears || 25} years`, "info");
148
167
  progress.log(`Store type: PKCS12`, "info");
@@ -220,8 +239,11 @@ async function createSignedAAB(projectPaths, androidConfig, keystorePath) {
220
239
 
221
240
  // For PKCS12 keystores, use store password for both store and key
222
241
  const effectiveKeyPassword = keystoreConfig.storePassword;
223
- const bundleCommand = ["./gradlew bundleRelease --quiet --console=rich", `-Pandroid.injected.signing.store.file="${keystorePath}"`, `-Pandroid.injected.signing.store.password="${keystoreConfig.storePassword}"`, `-Pandroid.injected.signing.key.alias="${keystoreConfig.keyAlias}"`, `-Pandroid.injected.signing.key.password="${effectiveKeyPassword}"`].join(" ");
224
- progress.log(`Executing: ${bundleCommand}`, "info");
242
+ const bundleCommand = ["./gradlew bundleRelease --console=plain", `-Pandroid.injected.signing.store.file="${keystorePath}"`, `-Pandroid.injected.signing.store.password="${keystoreConfig.storePassword}"`, `-Pandroid.injected.signing.key.alias="${keystoreConfig.keyAlias}"`, `-Pandroid.injected.signing.key.password="${effectiveKeyPassword}"`].join(" ");
243
+
244
+ // Never log this command as-is: it carries the keystore and key
245
+ // passwords, which would put them in the terminal and in CI logs.
246
+ progress.log("Executing: ./gradlew bundleRelease (signing args redacted)", "info");
225
247
  (0, _utils.runCommand)(bundleCommand);
226
248
  progress.log("Bundle build completed", "success");
227
249
 
@@ -404,10 +426,10 @@ async function diagnoseAndroidProject(projectPath) {
404
426
  ...pattern,
405
427
  foundFile
406
428
  });
407
- progress.log(`✅ Found: ${pattern.description}${foundFile ? ` (${foundFile})` : ""}`, "success");
429
+ progress.log(`Found: ${pattern.description}${foundFile ? ` (${foundFile})` : ""}`, "success");
408
430
  } else {
409
431
  diagnostics.missingFiles.push(pattern);
410
- progress.log(`❌ Missing: ${pattern.description}`, "warning");
432
+ progress.log(`Missing: ${pattern.description}`, "warning");
411
433
  }
412
434
  }
413
435
 
@@ -585,7 +607,7 @@ async function createTempDeploymentProject(projectPaths, androidConfig) {
585
607
  filesToCheck.forEach(file => {
586
608
  const fullPath = _path.default.join(tempDir, file.path);
587
609
  const exists = _fs.default.existsSync(fullPath);
588
- progress.log(` ${file.desc}: ${exists ? "✅ Found" : "⚠️ Not found"}`, exists ? "info" : "warning");
610
+ progress.log(` ${file.desc}: ${exists ? "found" : "not found"}`, exists ? "info" : "warning");
589
611
  });
590
612
 
591
613
  // List all files in temp directory for debugging
@@ -647,7 +669,7 @@ async function finalizeAABAndCleanup(projectPaths, androidConfig, aabResult, tem
647
669
  const finalAabName = `${androidConfig.newProjectName}-release-${timestamp}.aab`;
648
670
  const finalAabPath = _path.default.join(outputDir, finalAabName);
649
671
  _fs.default.copyFileSync(aabResult.outputAabPath, finalAabPath);
650
- progress.log(`AAB saved to final location: ${finalAabPath}`, "success");
672
+ progress.log(`AAB saved to: ${finalAabPath}`, "note");
651
673
 
652
674
  // Copy keystore to output directory for future use
653
675
  const keystorePath = _path.default.join(tempDeploymentPath, "app", "keystore.jks");
@@ -704,7 +726,7 @@ async function updateFileContents(projectPaths, androidConfig) {
704
726
  content = content.replace(/namespace = "io\.yourname\.androidproject"/g, `namespace = "${packageName}"`);
705
727
  content = content.replace(/applicationId = "io\.yourname\.androidproject"/g, `applicationId = "${packageName}"`);
706
728
  _fs.default.writeFileSync(buildGradlePath, content, "utf8");
707
- progress.log(`✅ Updated build.gradle.kts with package: ${packageName}`, "success");
729
+ progress.log(`Updated build.gradle.kts with package: ${packageName}`, "success");
708
730
  }
709
731
 
710
732
  // Update source directory structure for all source sets
@@ -726,7 +748,7 @@ async function updateFileContents(projectPaths, androidConfig) {
726
748
  // Move source files
727
749
  ;
728
750
  (0, _utils.runCommand)(`mv "${oldPackagePath}" "${newPackagePath}"`);
729
- progress.log(`✅ Moved ${sourceSet} source files to: ${newPackagePath}`, "success");
751
+ progress.log(`Moved ${sourceSet} source files to: ${newPackagePath}`, "success");
730
752
  } else {
731
753
  progress.log(`Package path unchanged for ${sourceSet}; skipping source directory move`, "info");
732
754
  }
@@ -744,7 +766,7 @@ async function updateFileContents(projectPaths, androidConfig) {
744
766
  }
745
767
  }
746
768
  totalUpdatedFiles += updatedCount;
747
- progress.log(`✅ Updated package declarations in ${updatedCount} ${sourceSet} files`, "success");
769
+ progress.log(`Updated package declarations in ${updatedCount} ${sourceSet} files`, "success");
748
770
 
749
771
  // Clean up old empty directories
750
772
  try {
@@ -756,7 +778,7 @@ async function updateFileContents(projectPaths, androidConfig) {
756
778
  }
757
779
  }
758
780
  if (totalUpdatedFiles > 0) {
759
- progress.log(`✅ Total package declarations updated: ${totalUpdatedFiles} files`, "success");
781
+ progress.log(`Total package declarations updated: ${totalUpdatedFiles} files`, "success");
760
782
  }
761
783
 
762
784
  // Update AndroidManifest.xml
@@ -766,7 +788,7 @@ async function updateFileContents(projectPaths, androidConfig) {
766
788
  if (content.includes(OLD_PACKAGE)) {
767
789
  content = content.replace(new RegExp(OLD_PACKAGE, "g"), packageName);
768
790
  _fs.default.writeFileSync(manifestPath, content, "utf8");
769
- progress.log(`✅ Updated AndroidManifest.xml with package: ${packageName}`, "success");
791
+ progress.log(`Updated AndroidManifest.xml with package: ${packageName}`, "success");
770
792
  }
771
793
  }
772
794
 
@@ -777,7 +799,7 @@ async function updateFileContents(projectPaths, androidConfig) {
777
799
  if (content.includes(OLD_PACKAGE)) {
778
800
  content = content.replace(new RegExp(OLD_PACKAGE.replace(/\./g, "\\."), "g"), packageName);
779
801
  _fs.default.writeFileSync(proguardPath, content, "utf8");
780
- progress.log(`✅ Updated proguard-rules.pro with package: ${packageName}`, "success");
802
+ progress.log(`Updated proguard-rules.pro with package: ${packageName}`, "success");
781
803
  }
782
804
  }
783
805
  }
@@ -11,22 +11,16 @@ const {
11
11
  } = require("./setupServer.js");
12
12
  const ITEMS_PER_PAGE = 10;
13
13
  const steps = {
14
- platform: "Check Platform Compatibility",
15
- config: "Initialize Configuration",
16
- simulator: "Configure iOS Simulator",
17
- launch: "Launch iOS Simulator",
18
- saveConfig: "Saving configuration",
19
- setupServer: "Setup Server"
14
+ platform: "Check platform compatibility",
15
+ config: "Initialize configuration",
16
+ simulator: "Configure iOS simulator",
17
+ launch: "Launch iOS simulator",
18
+ saveConfig: "Save configuration",
19
+ setupServer: "Set up server"
20
20
  };
21
- const progressPaddingConfig = {
22
- titlePaddingTop: 2,
23
- titlePaddingBottom: 1,
24
- stepPaddingLeft: 4,
25
- stepSpacing: 1,
26
- errorPaddingLeft: 6,
27
- bottomMargin: 2
28
- };
29
- const progress = new _terminalProgress.default(steps, "Catalyst Universal Ios Setup", progressPaddingConfig);
21
+ const progress = new _terminalProgress.default(steps, "catalyst setupEmulator", {
22
+ subject: "ios"
23
+ });
30
24
  async function initializeConfig() {
31
25
  const configFile = _fs.default.readFileSync(configPath, "utf8");
32
26
  const config = JSON.parse(configFile);
@@ -72,7 +66,7 @@ async function setupIOSEnvironment() {
72
66
  try {
73
67
  progress.start("platform");
74
68
  if (process.platform !== "darwin") {
75
- progress.log("iOS Simulator is only available on macOS.");
69
+ progress.log("iOS Simulator is only available on macOS.", "error");
76
70
  process.exit(1);
77
71
  }
78
72
  progress.complete("platform");
@@ -91,40 +85,26 @@ async function setupIOSEnvironment() {
91
85
  const config = await (0, _utils.validateAndCompleteConfig)("ios", configPath);
92
86
  progress.complete("saveConfig");
93
87
  progress.start("setupServer");
94
- await setupServer(configPath);
95
- progress.start("setupServer");
96
- progress.printTreeContent("Configuration Explanation", ["WEBVIEW_CONFIG: Main configuration object for the WebView setup", {
97
- text: "port: Port number for the WebView server",
98
- indent: 1,
99
- prefix: "├─ ",
100
- color: "gray"
101
- }, {
102
- text: "ios: iOS-specific configuration",
103
- indent: 1,
104
- prefix: "└─ ",
105
- color: "gray"
106
- }, {
107
- text: "buildType: Build type (debug/release)",
108
- indent: 2,
109
- prefix: "├─ ",
88
+ await setupServer(configPath, message => progress.log(message, "info"));
89
+ progress.complete("setupServer");
90
+ const iosCfg = config.ios || {};
91
+ progress.printTreeContent("Config config/config.json", [{
92
+ text: `port ${config.port ?? ""}`,
110
93
  color: "gray"
111
94
  }, {
112
- text: "appBundleId: iOS application bundle identifier",
113
- indent: 2,
114
- prefix: "├─ ",
95
+ text: `appBundleId ${iosCfg.appBundleId ?? ""}`,
115
96
  color: "gray"
116
97
  }, {
117
- text: "simulatorName: Selected iOS simulator name",
118
- indent: 2,
119
- prefix: "└─ ",
98
+ text: `simulatorName ${iosCfg.simulatorName ?? ""}`,
120
99
  color: "gray"
121
100
  }]);
122
- progress.printTreeContent("Final Configuration", [JSON.stringify(config, null, 2)]);
101
+ progress.summary("Ready", "Run catalyst buildApp:ios to install the app");
123
102
  process.exit(0);
124
103
  } catch (error) {
125
104
  if (progress.currentStep) {
126
105
  progress.fail(progress.currentStep.id, error.message);
127
106
  }
107
+ progress.hint(error.hint);
128
108
  process.exit(1);
129
109
  }
130
110
  }
@@ -147,13 +127,13 @@ async function getRuntime() {
147
127
  if (!availableRuntime) {
148
128
  const runningSimulators = (0, _child_process.execSync)("xcrun simctl list devices booted").toString();
149
129
  if (runningSimulators.includes("(Booted)") && parsedRuntimes.length > 0) {
150
- console.log(`Using runtime: ${parsedRuntimes[0].name}`);
130
+ progress.status(`runtime ${parsedRuntimes[0].name}`);
151
131
  return parsedRuntimes[0];
152
132
  }
153
- console.error("No available iOS runtime found. Please install one through Xcode.");
133
+ progress.log("No available iOS runtime found. Install one through Xcode.", "error");
154
134
  process.exit(1);
155
135
  }
156
- console.log(`Using runtime: ${availableRuntime.name}`);
136
+ progress.status(`runtime ${availableRuntime.name}`);
157
137
  return availableRuntime;
158
138
  }
159
139
  async function configureSimulator(config) {
@@ -269,7 +249,7 @@ function resolveSimulatorAppBundleId() {
269
249
  // instead of throwing.
270
250
  const SIMULATOR_APP_NOT_FOUND_WARNING = "[IOS-001] Could not find an installed iOS Simulator app (checked DeviceHub.app and the legacy Simulator.app). " + "The simulator is booted but its window may not be visible. Suggested action: confirm a full Xcode.app is installed and selected via xcode-select -p, then open it once from Spotlight.";
271
251
  async function launchIOSSimulator(simulatorName) {
272
- progress.log("Launching iOS Simulator...");
252
+ progress.status("launching simulator");
273
253
  try {
274
254
  // Get all simulators including both available and booted ones
275
255
  const allSimulatorInfo = (0, _child_process.execSync)("xcrun simctl list devices -j").toString();
@@ -301,20 +281,20 @@ async function launchIOSSimulator(simulatorName) {
301
281
  });
302
282
  });
303
283
  if (!foundSimulator) {
304
- console.log(`Configured simulator "${simulatorName}" not found.`);
284
+ progress.log(`Configured simulator "${simulatorName}" not found`, "warning");
305
285
  return;
306
286
  }
307
287
  if (!isBooted) {
308
- console.log(`Booting simulator: ${simulatorName}`);
288
+ progress.status(`booting ${simulatorName}`);
309
289
  (0, _utils.runCommand)(`xcrun simctl boot ${foundSimulatorId}`);
310
290
  } else {
311
- console.log(`Simulator ${simulatorName} is already booted`);
291
+ progress.status("already booted");
312
292
  }
313
293
 
314
294
  // Open the simulator UI (DeviceHub on Xcode 27+, Simulator.app on older Xcode) and focus it
315
295
  const simulatorAppBundleId = resolveSimulatorAppBundleId();
316
296
  if (simulatorAppBundleId) {
317
- progress.log("Opening simulator UI...");
297
+ progress.status("opening simulator");
318
298
  (0, _utils.runCommand)(`open -b ${simulatorAppBundleId}`);
319
299
 
320
300
  // Give the simulator a moment to open/focus
@@ -325,9 +305,8 @@ async function launchIOSSimulator(simulatorName) {
325
305
  } else {
326
306
  progress.log(SIMULATOR_APP_NOT_FOUND_WARNING, "warning");
327
307
  }
328
- console.log("iOS Simulator launched successfully.");
329
308
  } catch (error) {
330
- console.error("Failed to launch iOS Simulator. Error:", error.message);
309
+ progress.log(`Failed to launch iOS Simulator: ${error.message}`, "error");
331
310
  }
332
311
  // process.exit(0);
333
312
  }
@@ -44,14 +44,12 @@ async function isServerRunning(port) {
44
44
  * Start server in background using npm start
45
45
  */
46
46
  function startServerBackground() {
47
- console.log("Starting server in background...");
48
47
  const serverProcess = exec("npm start", {
49
48
  detached: true,
50
49
  stdio: "ignore"
51
50
  });
52
51
  if (serverProcess.pid) {
53
52
  serverProcess.unref(); // Allow parent to exit
54
- console.log("Server started in background");
55
53
  }
56
54
  }
57
55
 
@@ -90,7 +88,6 @@ function replaceIPInObject(obj, localIP, path = "") {
90
88
  shouldUpdate = true;
91
89
  }
92
90
  if (shouldUpdate && newValue !== value) {
93
- console.log(`Updating ${currentPath}: ${value} -> ${newValue}`);
94
91
  obj[key] = newValue;
95
92
  updated = true;
96
93
  }
@@ -106,7 +103,7 @@ function replaceIPInObject(obj, localIP, path = "") {
106
103
  /**
107
104
  * Update config.json with current local IP address
108
105
  */
109
- function updateConfigWithLocalIP(configPath, localIP) {
106
+ function updateConfigWithLocalIP(configPath, localIP, log = () => {}) {
110
107
  try {
111
108
  const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
112
109
 
@@ -116,9 +113,7 @@ function updateConfigWithLocalIP(configPath, localIP) {
116
113
  // Write back if updated
117
114
  if (updated) {
118
115
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
119
- console.log("Configuration updated with current IP address");
120
- } else {
121
- console.log("No IP addresses needed updating in configuration");
116
+ log(`Updated config addresses to ${localIP}`);
122
117
  }
123
118
  return config;
124
119
  } catch (error) {
@@ -132,15 +127,18 @@ function updateConfigWithLocalIP(configPath, localIP) {
132
127
  * @param {string} configPath - Path to config.json file
133
128
  * @returns {Promise<{serverURL: string, localIP: string, port: number}>}
134
129
  */
135
- async function setupServer(configPath) {
136
- console.log("Setting up server...");
137
-
130
+ /**
131
+ * @param {string} configPath
132
+ * @param {(message: string, type?: string) => void} [log] - progress sink.
133
+ * Defaults to silence: this runs inside a progress sequence, where a stray
134
+ * console.log lands at column 0 and collides with the live spinner row.
135
+ */
136
+ async function setupServer(configPath, log = () => {}) {
138
137
  // Get local IP
139
138
  const localIP = getLocalIPAddress();
140
- console.log(`Local IP: ${localIP}`);
141
139
 
142
140
  // Update config with current IP
143
- const config = updateConfigWithLocalIP(configPath, localIP);
141
+ const config = updateConfigWithLocalIP(configPath, localIP, log);
144
142
 
145
143
  // Get port from config
146
144
  const port = config.WEBVIEW_CONFIG?.port || 3005;
@@ -149,14 +147,13 @@ async function setupServer(configPath) {
149
147
  // Check if server is running
150
148
  const running = await isServerRunning(port);
151
149
  if (!running) {
152
- console.log(`Server not running on ${serverURL}, starting...`);
150
+ log(`Starting server at ${serverURL}`);
153
151
  startServerBackground();
154
152
  // Give it a moment to start
155
153
  await new Promise(resolve => setTimeout(resolve, 3000));
156
154
  } else {
157
- console.log(`Server already running on ${serverURL}`);
155
+ log(`Server already running at ${serverURL}`);
158
156
  }
159
- console.log(`Server setup complete: ${serverURL}`);
160
157
  return {
161
158
  serverURL,
162
159
  localIP,