openmagic 0.3.0 → 0.4.0

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.
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { createInterface } from "readline";
10
10
 
11
11
  // src/proxy.ts
12
12
  import http from "http";
13
+ import { createGunzip, createInflate, createBrotliDecompress } from "zlib";
13
14
  import httpProxy from "http-proxy";
14
15
 
15
16
  // src/security.ts
@@ -45,10 +46,7 @@ function createProxyServer(targetHost, targetPort, serverPort) {
45
46
  proxyRes.pipe(res);
46
47
  return;
47
48
  }
48
- const chunks = [];
49
- proxyRes.on("data", (chunk) => chunks.push(chunk));
50
- proxyRes.on("end", () => {
51
- let body = Buffer.concat(chunks).toString("utf-8");
49
+ collectBody(proxyRes).then((body) => {
52
50
  const toolbarScript = buildInjectionScript(serverPort, token);
53
51
  if (body.includes("</body>")) {
54
52
  body = body.replace("</body>", `${toolbarScript}</body>`);
@@ -60,18 +58,27 @@ function createProxyServer(targetHost, targetPort, serverPort) {
60
58
  const headers = { ...proxyRes.headers };
61
59
  delete headers["content-length"];
62
60
  delete headers["content-encoding"];
61
+ delete headers["transfer-encoding"];
63
62
  res.writeHead(proxyRes.statusCode || 200, headers);
64
63
  res.end(body);
64
+ }).catch(() => {
65
+ try {
66
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
67
+ res.end();
68
+ } catch {
69
+ }
65
70
  });
66
71
  });
67
72
  proxy.on("error", (err, _req, res) => {
68
- console.error("[OpenMagic] Proxy error:", err.message);
69
73
  if (res instanceof http.ServerResponse && !res.headersSent) {
70
- res.writeHead(502, { "Content-Type": "text/plain" });
74
+ res.writeHead(502, { "Content-Type": "text/html" });
71
75
  res.end(
72
- `OpenMagic proxy error: Could not connect to dev server at ${targetHost}:${targetPort}
73
-
74
- Make sure your dev server is running.`
76
+ `<html><body style="font-family:system-ui;padding:40px;background:#1a1a2e;color:#e0e0e0;">
77
+ <h2 style="color:#e94560;">OpenMagic \u2014 Cannot connect to dev server</h2>
78
+ <p>Could not reach <code>${targetHost}:${targetPort}</code></p>
79
+ <p style="color:#888;">Make sure your dev server is running, then refresh this page.</p>
80
+ <p style="color:#666;font-size:13px;">${err.message}</p>
81
+ </body></html>`
75
82
  );
76
83
  }
77
84
  });
@@ -90,6 +97,58 @@ Make sure your dev server is running.`
90
97
  });
91
98
  return server;
92
99
  }
100
+ function collectBody(stream) {
101
+ return new Promise((resolve3, reject) => {
102
+ const encoding = (stream.headers["content-encoding"] || "").toLowerCase();
103
+ const chunks = [];
104
+ let source = stream;
105
+ if (encoding === "gzip" || encoding === "x-gzip") {
106
+ const gunzip = createGunzip();
107
+ stream.pipe(gunzip);
108
+ source = gunzip;
109
+ gunzip.on("error", () => {
110
+ collectRaw(stream).then(resolve3).catch(reject);
111
+ });
112
+ } else if (encoding === "deflate") {
113
+ const inflate = createInflate();
114
+ stream.pipe(inflate);
115
+ source = inflate;
116
+ inflate.on("error", () => {
117
+ collectRaw(stream).then(resolve3).catch(reject);
118
+ });
119
+ } else if (encoding === "br") {
120
+ const brotli = createBrotliDecompress();
121
+ stream.pipe(brotli);
122
+ source = brotli;
123
+ brotli.on("error", () => {
124
+ collectRaw(stream).then(resolve3).catch(reject);
125
+ });
126
+ }
127
+ source.on("data", (chunk) => chunks.push(chunk));
128
+ source.on("end", () => {
129
+ try {
130
+ resolve3(Buffer.concat(chunks).toString("utf-8"));
131
+ } catch {
132
+ reject(new Error("Failed to decode response body"));
133
+ }
134
+ });
135
+ source.on("error", (err) => reject(err));
136
+ });
137
+ }
138
+ function collectRaw(stream) {
139
+ return new Promise((resolve3, reject) => {
140
+ const chunks = [];
141
+ stream.on("data", (chunk) => chunks.push(chunk));
142
+ stream.on("end", () => {
143
+ try {
144
+ resolve3(Buffer.concat(chunks).toString("utf-8"));
145
+ } catch {
146
+ reject(new Error("Failed to decode raw body"));
147
+ }
148
+ });
149
+ stream.on("error", reject);
150
+ });
151
+ }
93
152
  function handleToolbarAsset(_req, res, _serverPort) {
94
153
  res.writeHead(404, { "Content-Type": "text/plain" });
95
154
  res.end("Not found");
@@ -143,10 +202,14 @@ function loadConfig() {
143
202
  }
144
203
  }
145
204
  function saveConfig(updates) {
146
- ensureConfigDir();
147
- const existing = loadConfig();
148
- const merged = { ...existing, ...updates };
149
- writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
205
+ try {
206
+ ensureConfigDir();
207
+ const existing = loadConfig();
208
+ const merged = { ...existing, ...updates };
209
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
210
+ } catch (e) {
211
+ console.warn(`[OpenMagic] Warning: Could not save config to ${CONFIG_FILE}: ${e.message}`);
212
+ }
150
213
  }
151
214
 
152
215
  // src/filesystem.ts
@@ -1007,8 +1070,14 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
1007
1070
  body: JSON.stringify(body)
1008
1071
  });
1009
1072
  if (!response.ok) {
1010
- const errorText = await response.text();
1011
- onError(`API error ${response.status}: ${errorText}`);
1073
+ const errorText = await response.text().catch(() => "Unknown error");
1074
+ if (response.status === 401 || response.status === 403) {
1075
+ onError(`Invalid API key for ${providerConfig.name}. Check your key in Settings.`);
1076
+ } else if (response.status === 429) {
1077
+ onError(`Rate limit exceeded for ${providerConfig.name}. Wait a moment and try again.`);
1078
+ } else {
1079
+ onError(`${providerConfig.name} API error ${response.status}: ${errorText.slice(0, 200)}`);
1080
+ }
1012
1081
  return;
1013
1082
  }
1014
1083
  if (!response.body) {
@@ -1127,8 +1196,14 @@ async function chatAnthropic(model, apiKey, messages, context, onChunk, onDone,
1127
1196
  body: JSON.stringify(body)
1128
1197
  });
1129
1198
  if (!response.ok) {
1130
- const errorText = await response.text();
1131
- onError(`Anthropic API error ${response.status}: ${errorText}`);
1199
+ const errorText = await response.text().catch(() => "Unknown error");
1200
+ if (response.status === 401 || response.status === 403) {
1201
+ onError("Invalid Anthropic API key. Check your key in Settings.");
1202
+ } else if (response.status === 429) {
1203
+ onError("Anthropic rate limit exceeded. Wait a moment and try again.");
1204
+ } else {
1205
+ onError(`Anthropic API error ${response.status}: ${errorText.slice(0, 200)}`);
1206
+ }
1132
1207
  return;
1133
1208
  }
1134
1209
  if (!response.body) {
@@ -1233,8 +1308,14 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
1233
1308
  body: JSON.stringify(body)
1234
1309
  });
1235
1310
  if (!response.ok) {
1236
- const errorText = await response.text();
1237
- onError(`Google API error ${response.status}: ${errorText}`);
1311
+ const errorText = await response.text().catch(() => "Unknown error");
1312
+ if (response.status === 401 || response.status === 403) {
1313
+ onError("Invalid Google API key. Check your key in Settings.");
1314
+ } else if (response.status === 429) {
1315
+ onError("Google API rate limit exceeded. Wait a moment and try again.");
1316
+ } else {
1317
+ onError(`Google API error ${response.status}: ${errorText.slice(0, 200)}`);
1318
+ }
1238
1319
  return;
1239
1320
  }
1240
1321
  if (!response.body) {
@@ -1296,23 +1377,32 @@ async function handleLlmChat(params, onChunk, onDone, onError) {
1296
1377
  }
1297
1378
  onDone({ content: result.content, modifications });
1298
1379
  };
1299
- if (provider === "anthropic") {
1300
- await chatAnthropic(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
1301
- } else if (provider === "google") {
1302
- await chatGoogle(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
1303
- } else if (OPENAI_COMPATIBLE_PROVIDERS.has(provider)) {
1304
- await chatOpenAICompatible(
1305
- provider,
1306
- model,
1307
- apiKey,
1308
- messages,
1309
- context,
1310
- onChunk,
1311
- wrappedOnDone,
1312
- onError
1313
- );
1314
- } else {
1315
- onError(`Unsupported provider: ${provider}`);
1380
+ try {
1381
+ if (provider === "anthropic") {
1382
+ await chatAnthropic(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
1383
+ } else if (provider === "google") {
1384
+ await chatGoogle(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
1385
+ } else if (OPENAI_COMPATIBLE_PROVIDERS.has(provider)) {
1386
+ await chatOpenAICompatible(
1387
+ provider,
1388
+ model,
1389
+ apiKey,
1390
+ messages,
1391
+ context,
1392
+ onChunk,
1393
+ wrappedOnDone,
1394
+ onError
1395
+ );
1396
+ } else {
1397
+ onError(`Unsupported provider: ${provider}. Check your Settings.`);
1398
+ }
1399
+ } catch (e) {
1400
+ const msg = e.message || "Unknown error";
1401
+ if (msg.includes("fetch") || msg.includes("ECONNREFUSED") || msg.includes("network")) {
1402
+ onError(`Network error: Could not reach the ${provider} API. Check your internet connection.`);
1403
+ } else {
1404
+ onError(`Unexpected error with ${provider}: ${msg}`);
1405
+ }
1316
1406
  }
1317
1407
  }
1318
1408
 
@@ -1329,7 +1419,7 @@ function createOpenMagicServer(proxyPort, roots) {
1329
1419
  "Content-Type": "application/json",
1330
1420
  "Access-Control-Allow-Origin": "*"
1331
1421
  });
1332
- res.end(JSON.stringify({ status: "ok", version: "0.3.0" }));
1422
+ res.end(JSON.stringify({ status: "ok", version: "0.4.0" }));
1333
1423
  return;
1334
1424
  }
1335
1425
  res.writeHead(404);
@@ -1371,6 +1461,11 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1371
1461
  switch (msg.type) {
1372
1462
  case "handshake": {
1373
1463
  const payload = msg.payload;
1464
+ if (!payload?.token) {
1465
+ sendError(ws, "invalid_payload", "Missing token in handshake", msg.id);
1466
+ ws.close();
1467
+ return;
1468
+ }
1374
1469
  if (!validateToken(payload.token)) {
1375
1470
  sendError(ws, "auth_failed", "Invalid token", msg.id);
1376
1471
  ws.close();
@@ -1382,7 +1477,7 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1382
1477
  id: msg.id,
1383
1478
  type: "handshake.ok",
1384
1479
  payload: {
1385
- version: "0.3.0",
1480
+ version: "0.4.0",
1386
1481
  roots,
1387
1482
  config: {
1388
1483
  provider: config.provider,
@@ -1395,6 +1490,10 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1395
1490
  }
1396
1491
  case "fs.read": {
1397
1492
  const payload = msg.payload;
1493
+ if (!payload?.path) {
1494
+ sendError(ws, "invalid_payload", "Missing path", msg.id);
1495
+ break;
1496
+ }
1398
1497
  const result = readFileSafe(payload.path, roots);
1399
1498
  if ("error" in result) {
1400
1499
  sendError(ws, "fs_error", result.error, msg.id);
@@ -1506,15 +1605,19 @@ function serveToolbarBundle(res) {
1506
1605
  join3(__dirname, "..", "dist", "toolbar", "index.global.js")
1507
1606
  ];
1508
1607
  for (const bundlePath of bundlePaths) {
1509
- if (existsSync3(bundlePath)) {
1510
- const content = readFileSync3(bundlePath, "utf-8");
1511
- res.writeHead(200, {
1512
- "Content-Type": "application/javascript",
1513
- "Access-Control-Allow-Origin": "*",
1514
- "Cache-Control": "no-cache"
1515
- });
1516
- res.end(content);
1517
- return;
1608
+ try {
1609
+ if (existsSync3(bundlePath)) {
1610
+ const content = readFileSync3(bundlePath, "utf-8");
1611
+ res.writeHead(200, {
1612
+ "Content-Type": "application/javascript",
1613
+ "Access-Control-Allow-Origin": "*",
1614
+ "Cache-Control": "no-cache"
1615
+ });
1616
+ res.end(content);
1617
+ return;
1618
+ }
1619
+ } catch {
1620
+ continue;
1518
1621
  }
1519
1622
  }
1520
1623
  res.writeHead(200, {
@@ -1670,9 +1773,47 @@ function getProjectName(cwd = process.cwd()) {
1670
1773
  return "this project";
1671
1774
  }
1672
1775
  }
1776
+ var LOCK_FILES = [
1777
+ { file: "pnpm-lock.yaml", pm: "pnpm" },
1778
+ { file: "yarn.lock", pm: "yarn" },
1779
+ { file: "bun.lockb", pm: "bun" },
1780
+ { file: "bun.lock", pm: "bun" },
1781
+ { file: "package-lock.json", pm: "npm" }
1782
+ ];
1783
+ var INSTALL_COMMANDS = {
1784
+ npm: "npm install",
1785
+ yarn: "yarn install",
1786
+ pnpm: "pnpm install",
1787
+ bun: "bun install"
1788
+ };
1789
+ function checkDependenciesInstalled(cwd = process.cwd()) {
1790
+ const hasNodeModules = existsSync4(join4(cwd, "node_modules"));
1791
+ let pm = "npm";
1792
+ for (const { file, pm: detectedPm } of LOCK_FILES) {
1793
+ if (existsSync4(join4(cwd, file))) {
1794
+ pm = detectedPm;
1795
+ break;
1796
+ }
1797
+ }
1798
+ return {
1799
+ installed: hasNodeModules,
1800
+ packageManager: pm,
1801
+ installCommand: INSTALL_COMMANDS[pm]
1802
+ };
1803
+ }
1673
1804
 
1674
1805
  // src/cli.ts
1675
- var VERSION = "0.3.0";
1806
+ process.on("unhandledRejection", (err) => {
1807
+ console.error(chalk.red("\n [OpenMagic] Unhandled error:"), err?.message || err);
1808
+ console.error(chalk.dim(" Please report this at https://github.com/Kalmuraee/OpenMagic/issues"));
1809
+ });
1810
+ process.on("uncaughtException", (err) => {
1811
+ console.error(chalk.red("\n [OpenMagic] Fatal error:"), err.message);
1812
+ console.error(chalk.dim(" Please report this at https://github.com/Kalmuraee/OpenMagic/issues"));
1813
+ process.exit(1);
1814
+ });
1815
+ var childProcesses = [];
1816
+ var VERSION = "0.4.0";
1676
1817
  function ask(question) {
1677
1818
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1678
1819
  return new Promise((resolve3) => {
@@ -1682,10 +1823,14 @@ function ask(question) {
1682
1823
  });
1683
1824
  });
1684
1825
  }
1685
- function waitForPort(port, timeoutMs = 3e4) {
1826
+ function waitForPort(port, timeoutMs = 3e4, shouldAbort) {
1686
1827
  const start = Date.now();
1687
1828
  return new Promise((resolve3) => {
1688
1829
  const check = async () => {
1830
+ if (shouldAbort?.()) {
1831
+ resolve3(false);
1832
+ return;
1833
+ }
1689
1834
  if (await isPortOpen(port)) {
1690
1835
  resolve3(true);
1691
1836
  return;
@@ -1699,6 +1844,35 @@ function waitForPort(port, timeoutMs = 3e4) {
1699
1844
  check();
1700
1845
  });
1701
1846
  }
1847
+ function runCommand(cmd, args, cwd = process.cwd()) {
1848
+ return new Promise((resolve3) => {
1849
+ try {
1850
+ const child = spawn(cmd, args, {
1851
+ cwd,
1852
+ stdio: ["ignore", "pipe", "pipe"],
1853
+ shell: true
1854
+ });
1855
+ child.stdout?.on("data", (data) => {
1856
+ const lines = data.toString().trim().split("\n");
1857
+ for (const line of lines) {
1858
+ if (line.trim()) process.stdout.write(chalk.dim(` \u2502 ${line}
1859
+ `));
1860
+ }
1861
+ });
1862
+ child.stderr?.on("data", (data) => {
1863
+ const lines = data.toString().trim().split("\n");
1864
+ for (const line of lines) {
1865
+ if (line.trim()) process.stdout.write(chalk.dim(` \u2502 ${line}
1866
+ `));
1867
+ }
1868
+ });
1869
+ child.on("error", () => resolve3(false));
1870
+ child.on("close", (code) => resolve3(code === 0));
1871
+ } catch {
1872
+ resolve3(false);
1873
+ }
1874
+ });
1875
+ }
1702
1876
  var program = new Command();
1703
1877
  program.name("openmagic").description("AI-powered coding toolbar for any web application").version(VERSION).option("-p, --port <port>", "Dev server port to proxy", "").option(
1704
1878
  "-l, --listen <port>",
@@ -1811,6 +1985,34 @@ async function offerToStartDevServer(expectedPort) {
1811
1985
  console.log("");
1812
1986
  return false;
1813
1987
  }
1988
+ const deps = checkDependenciesInstalled();
1989
+ if (!deps.installed) {
1990
+ console.log(
1991
+ chalk.yellow(" \u26A0 node_modules/ not found. Dependencies need to be installed.")
1992
+ );
1993
+ console.log("");
1994
+ const answer = await ask(
1995
+ chalk.white(` Run `) + chalk.cyan(deps.installCommand) + chalk.white("? ") + chalk.dim("(Y/n) ")
1996
+ );
1997
+ if (answer.toLowerCase() === "n" || answer.toLowerCase() === "no") {
1998
+ console.log("");
1999
+ console.log(chalk.dim(` Run ${deps.installCommand} manually, then try again.`));
2000
+ console.log("");
2001
+ return false;
2002
+ }
2003
+ console.log("");
2004
+ console.log(chalk.dim(` Installing dependencies with ${deps.packageManager}...`));
2005
+ const [installCmd, ...installArgs] = deps.installCommand.split(" ");
2006
+ const installed = await runCommand(installCmd, installArgs);
2007
+ if (!installed) {
2008
+ console.log(chalk.red(" \u2717 Dependency installation failed."));
2009
+ console.log(chalk.dim(` Try running ${deps.installCommand} manually.`));
2010
+ console.log("");
2011
+ return false;
2012
+ }
2013
+ console.log(chalk.green(" \u2713 Dependencies installed."));
2014
+ console.log("");
2015
+ }
1814
2016
  let chosen = scripts[0];
1815
2017
  if (scripts.length === 1) {
1816
2018
  console.log(
@@ -1868,52 +2070,66 @@ async function offerToStartDevServer(expectedPort) {
1868
2070
  console.log(
1869
2071
  chalk.dim(` Starting `) + chalk.cyan(`npm run ${chosen.name}`) + chalk.dim("...")
1870
2072
  );
1871
- const child = spawn("npm", ["run", chosen.name], {
1872
- cwd: process.cwd(),
1873
- stdio: ["ignore", "pipe", "pipe"],
1874
- detached: false,
1875
- shell: true,
1876
- env: {
1877
- ...process.env,
1878
- PORT: String(port),
1879
- // CRA, Express
1880
- BROWSER: "none",
1881
- // Prevent CRA from opening browser
1882
- BROWSER_NONE: "true"
1883
- // Some frameworks
1884
- }
1885
- });
2073
+ const depsInfo = checkDependenciesInstalled();
2074
+ const runCmd = depsInfo.packageManager === "yarn" ? "yarn" : depsInfo.packageManager === "pnpm" ? "pnpm" : depsInfo.packageManager === "bun" ? "bun" : "npm";
2075
+ const runArgs = runCmd === "npm" ? ["run", chosen.name] : [chosen.name];
2076
+ let child;
2077
+ try {
2078
+ child = spawn(runCmd, runArgs, {
2079
+ cwd: process.cwd(),
2080
+ stdio: ["ignore", "pipe", "pipe"],
2081
+ detached: false,
2082
+ shell: true,
2083
+ env: {
2084
+ ...process.env,
2085
+ PORT: String(port),
2086
+ BROWSER: "none",
2087
+ BROWSER_NONE: "true"
2088
+ }
2089
+ });
2090
+ } catch (e) {
2091
+ console.log(chalk.red(` \u2717 Failed to start: ${e.message}`));
2092
+ return false;
2093
+ }
2094
+ childProcesses.push(child);
2095
+ let childExited = false;
1886
2096
  child.stdout?.on("data", (data) => {
1887
- const lines = data.toString().trim().split("\n");
1888
- for (const line of lines) {
1889
- if (line.trim()) {
1890
- process.stdout.write(chalk.dim(` \u2502 ${line}
2097
+ for (const line of data.toString().trim().split("\n")) {
2098
+ if (line.trim()) process.stdout.write(chalk.dim(` \u2502 ${line}
1891
2099
  `));
1892
- }
1893
2100
  }
1894
2101
  });
1895
2102
  child.stderr?.on("data", (data) => {
1896
- const lines = data.toString().trim().split("\n");
1897
- for (const line of lines) {
1898
- if (line.trim()) {
1899
- process.stdout.write(chalk.dim(` \u2502 ${line}
2103
+ for (const line of data.toString().trim().split("\n")) {
2104
+ if (line.trim()) process.stdout.write(chalk.dim(` \u2502 ${line}
1900
2105
  `));
1901
- }
1902
2106
  }
1903
2107
  });
1904
2108
  child.on("error", (err) => {
2109
+ childExited = true;
1905
2110
  console.log(chalk.red(` \u2717 Failed to start: ${err.message}`));
1906
2111
  });
1907
2112
  child.on("exit", (code) => {
2113
+ childExited = true;
1908
2114
  if (code !== null && code !== 0) {
1909
2115
  console.log(chalk.red(` \u2717 Dev server exited with code ${code}`));
1910
2116
  }
1911
2117
  });
1912
2118
  const cleanup = () => {
1913
- try {
1914
- child.kill("SIGTERM");
1915
- } catch {
2119
+ for (const cp of childProcesses) {
2120
+ try {
2121
+ cp.kill("SIGTERM");
2122
+ } catch {
2123
+ }
1916
2124
  }
2125
+ setTimeout(() => {
2126
+ for (const cp of childProcesses) {
2127
+ try {
2128
+ cp.kill("SIGKILL");
2129
+ } catch {
2130
+ }
2131
+ }
2132
+ }, 3e3);
1917
2133
  };
1918
2134
  process.on("exit", cleanup);
1919
2135
  process.on("SIGINT", cleanup);
@@ -1921,7 +2137,17 @@ async function offerToStartDevServer(expectedPort) {
1921
2137
  console.log(
1922
2138
  chalk.dim(` Waiting for port ${port}...`)
1923
2139
  );
1924
- const isUp = await waitForPort(port, 3e4);
2140
+ const isUp = await waitForPort(port, 3e4, () => childExited);
2141
+ if (childExited && !isUp) {
2142
+ console.log(
2143
+ chalk.red(` \u2717 Dev server exited before it was ready.`)
2144
+ );
2145
+ console.log(
2146
+ chalk.dim(` Check the error output above and fix the issue.`)
2147
+ );
2148
+ console.log("");
2149
+ return false;
2150
+ }
1925
2151
  if (!isUp) {
1926
2152
  console.log(
1927
2153
  chalk.yellow(` \u26A0 Port ${port} didn't open after 30s.`)