openmagic 0.3.0 → 0.5.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,29 @@ 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
+ const toolbarScript = buildInjectionScript(serverPort, token);
75
+ res.writeHead(502, { "Content-Type": "text/html" });
71
76
  res.end(
72
- `OpenMagic proxy error: Could not connect to dev server at ${targetHost}:${targetPort}
73
-
74
- Make sure your dev server is running.`
77
+ `<html><body style="font-family:system-ui;padding:40px;background:#1a1a2e;color:#e0e0e0;">
78
+ <h2 style="color:#e94560;">OpenMagic \u2014 Cannot connect to dev server</h2>
79
+ <p>Could not reach <code>${targetHost}:${targetPort}</code></p>
80
+ <p style="color:#888;">Make sure your dev server is running, then refresh this page.</p>
81
+ <p style="color:#666;font-size:13px;">${err.message}</p>
82
+ ${toolbarScript}
83
+ </body></html>`
75
84
  );
76
85
  }
77
86
  });
@@ -90,6 +99,58 @@ Make sure your dev server is running.`
90
99
  });
91
100
  return server;
92
101
  }
102
+ function collectBody(stream) {
103
+ return new Promise((resolve3, reject) => {
104
+ const encoding = (stream.headers["content-encoding"] || "").toLowerCase();
105
+ const chunks = [];
106
+ let source = stream;
107
+ if (encoding === "gzip" || encoding === "x-gzip") {
108
+ const gunzip = createGunzip();
109
+ stream.pipe(gunzip);
110
+ source = gunzip;
111
+ gunzip.on("error", () => {
112
+ collectRaw(stream).then(resolve3).catch(reject);
113
+ });
114
+ } else if (encoding === "deflate") {
115
+ const inflate = createInflate();
116
+ stream.pipe(inflate);
117
+ source = inflate;
118
+ inflate.on("error", () => {
119
+ collectRaw(stream).then(resolve3).catch(reject);
120
+ });
121
+ } else if (encoding === "br") {
122
+ const brotli = createBrotliDecompress();
123
+ stream.pipe(brotli);
124
+ source = brotli;
125
+ brotli.on("error", () => {
126
+ collectRaw(stream).then(resolve3).catch(reject);
127
+ });
128
+ }
129
+ source.on("data", (chunk) => chunks.push(chunk));
130
+ source.on("end", () => {
131
+ try {
132
+ resolve3(Buffer.concat(chunks).toString("utf-8"));
133
+ } catch {
134
+ reject(new Error("Failed to decode response body"));
135
+ }
136
+ });
137
+ source.on("error", (err) => reject(err));
138
+ });
139
+ }
140
+ function collectRaw(stream) {
141
+ return new Promise((resolve3, reject) => {
142
+ const chunks = [];
143
+ stream.on("data", (chunk) => chunks.push(chunk));
144
+ stream.on("end", () => {
145
+ try {
146
+ resolve3(Buffer.concat(chunks).toString("utf-8"));
147
+ } catch {
148
+ reject(new Error("Failed to decode raw body"));
149
+ }
150
+ });
151
+ stream.on("error", reject);
152
+ });
153
+ }
93
154
  function handleToolbarAsset(_req, res, _serverPort) {
94
155
  res.writeHead(404, { "Content-Type": "text/plain" });
95
156
  res.end("Not found");
@@ -143,10 +204,14 @@ function loadConfig() {
143
204
  }
144
205
  }
145
206
  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");
207
+ try {
208
+ ensureConfigDir();
209
+ const existing = loadConfig();
210
+ const merged = { ...existing, ...updates };
211
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
212
+ } catch (e) {
213
+ console.warn(`[OpenMagic] Warning: Could not save config to ${CONFIG_FILE}: ${e.message}`);
214
+ }
150
215
  }
151
216
 
152
217
  // src/filesystem.ts
@@ -1007,8 +1072,14 @@ async function chatOpenAICompatible(provider, model, apiKey, messages, context,
1007
1072
  body: JSON.stringify(body)
1008
1073
  });
1009
1074
  if (!response.ok) {
1010
- const errorText = await response.text();
1011
- onError(`API error ${response.status}: ${errorText}`);
1075
+ const errorText = await response.text().catch(() => "Unknown error");
1076
+ if (response.status === 401 || response.status === 403) {
1077
+ onError(`Invalid API key for ${providerConfig.name}. Check your key in Settings.`);
1078
+ } else if (response.status === 429) {
1079
+ onError(`Rate limit exceeded for ${providerConfig.name}. Wait a moment and try again.`);
1080
+ } else {
1081
+ onError(`${providerConfig.name} API error ${response.status}: ${errorText.slice(0, 200)}`);
1082
+ }
1012
1083
  return;
1013
1084
  }
1014
1085
  if (!response.body) {
@@ -1127,8 +1198,14 @@ async function chatAnthropic(model, apiKey, messages, context, onChunk, onDone,
1127
1198
  body: JSON.stringify(body)
1128
1199
  });
1129
1200
  if (!response.ok) {
1130
- const errorText = await response.text();
1131
- onError(`Anthropic API error ${response.status}: ${errorText}`);
1201
+ const errorText = await response.text().catch(() => "Unknown error");
1202
+ if (response.status === 401 || response.status === 403) {
1203
+ onError("Invalid Anthropic API key. Check your key in Settings.");
1204
+ } else if (response.status === 429) {
1205
+ onError("Anthropic rate limit exceeded. Wait a moment and try again.");
1206
+ } else {
1207
+ onError(`Anthropic API error ${response.status}: ${errorText.slice(0, 200)}`);
1208
+ }
1132
1209
  return;
1133
1210
  }
1134
1211
  if (!response.body) {
@@ -1233,8 +1310,14 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
1233
1310
  body: JSON.stringify(body)
1234
1311
  });
1235
1312
  if (!response.ok) {
1236
- const errorText = await response.text();
1237
- onError(`Google API error ${response.status}: ${errorText}`);
1313
+ const errorText = await response.text().catch(() => "Unknown error");
1314
+ if (response.status === 401 || response.status === 403) {
1315
+ onError("Invalid Google API key. Check your key in Settings.");
1316
+ } else if (response.status === 429) {
1317
+ onError("Google API rate limit exceeded. Wait a moment and try again.");
1318
+ } else {
1319
+ onError(`Google API error ${response.status}: ${errorText.slice(0, 200)}`);
1320
+ }
1238
1321
  return;
1239
1322
  }
1240
1323
  if (!response.body) {
@@ -1296,23 +1379,32 @@ async function handleLlmChat(params, onChunk, onDone, onError) {
1296
1379
  }
1297
1380
  onDone({ content: result.content, modifications });
1298
1381
  };
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}`);
1382
+ try {
1383
+ if (provider === "anthropic") {
1384
+ await chatAnthropic(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
1385
+ } else if (provider === "google") {
1386
+ await chatGoogle(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
1387
+ } else if (OPENAI_COMPATIBLE_PROVIDERS.has(provider)) {
1388
+ await chatOpenAICompatible(
1389
+ provider,
1390
+ model,
1391
+ apiKey,
1392
+ messages,
1393
+ context,
1394
+ onChunk,
1395
+ wrappedOnDone,
1396
+ onError
1397
+ );
1398
+ } else {
1399
+ onError(`Unsupported provider: ${provider}. Check your Settings.`);
1400
+ }
1401
+ } catch (e) {
1402
+ const msg = e.message || "Unknown error";
1403
+ if (msg.includes("fetch") || msg.includes("ECONNREFUSED") || msg.includes("network")) {
1404
+ onError(`Network error: Could not reach the ${provider} API. Check your internet connection.`);
1405
+ } else {
1406
+ onError(`Unexpected error with ${provider}: ${msg}`);
1407
+ }
1316
1408
  }
1317
1409
  }
1318
1410
 
@@ -1329,7 +1421,7 @@ function createOpenMagicServer(proxyPort, roots) {
1329
1421
  "Content-Type": "application/json",
1330
1422
  "Access-Control-Allow-Origin": "*"
1331
1423
  });
1332
- res.end(JSON.stringify({ status: "ok", version: "0.3.0" }));
1424
+ res.end(JSON.stringify({ status: "ok", version: "0.5.0" }));
1333
1425
  return;
1334
1426
  }
1335
1427
  res.writeHead(404);
@@ -1371,6 +1463,11 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1371
1463
  switch (msg.type) {
1372
1464
  case "handshake": {
1373
1465
  const payload = msg.payload;
1466
+ if (!payload?.token) {
1467
+ sendError(ws, "invalid_payload", "Missing token in handshake", msg.id);
1468
+ ws.close();
1469
+ return;
1470
+ }
1374
1471
  if (!validateToken(payload.token)) {
1375
1472
  sendError(ws, "auth_failed", "Invalid token", msg.id);
1376
1473
  ws.close();
@@ -1382,7 +1479,7 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1382
1479
  id: msg.id,
1383
1480
  type: "handshake.ok",
1384
1481
  payload: {
1385
- version: "0.3.0",
1482
+ version: "0.5.0",
1386
1483
  roots,
1387
1484
  config: {
1388
1485
  provider: config.provider,
@@ -1395,6 +1492,10 @@ async function handleMessage(ws, msg, state, roots, _proxyPort) {
1395
1492
  }
1396
1493
  case "fs.read": {
1397
1494
  const payload = msg.payload;
1495
+ if (!payload?.path) {
1496
+ sendError(ws, "invalid_payload", "Missing path", msg.id);
1497
+ break;
1498
+ }
1398
1499
  const result = readFileSafe(payload.path, roots);
1399
1500
  if ("error" in result) {
1400
1501
  sendError(ws, "fs_error", result.error, msg.id);
@@ -1506,15 +1607,19 @@ function serveToolbarBundle(res) {
1506
1607
  join3(__dirname, "..", "dist", "toolbar", "index.global.js")
1507
1608
  ];
1508
1609
  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;
1610
+ try {
1611
+ if (existsSync3(bundlePath)) {
1612
+ const content = readFileSync3(bundlePath, "utf-8");
1613
+ res.writeHead(200, {
1614
+ "Content-Type": "application/javascript",
1615
+ "Access-Control-Allow-Origin": "*",
1616
+ "Cache-Control": "no-cache"
1617
+ });
1618
+ res.end(content);
1619
+ return;
1620
+ }
1621
+ } catch {
1622
+ continue;
1518
1623
  }
1519
1624
  }
1520
1625
  res.writeHead(200, {
@@ -1670,9 +1775,52 @@ function getProjectName(cwd = process.cwd()) {
1670
1775
  return "this project";
1671
1776
  }
1672
1777
  }
1778
+ var LOCK_FILES = [
1779
+ { file: "pnpm-lock.yaml", pm: "pnpm" },
1780
+ { file: "yarn.lock", pm: "yarn" },
1781
+ { file: "bun.lockb", pm: "bun" },
1782
+ { file: "bun.lock", pm: "bun" },
1783
+ { file: "package-lock.json", pm: "npm" }
1784
+ ];
1785
+ var INSTALL_COMMANDS = {
1786
+ npm: "npm install",
1787
+ yarn: "yarn install",
1788
+ pnpm: "pnpm install",
1789
+ bun: "bun install"
1790
+ };
1791
+ function checkDependenciesInstalled(cwd = process.cwd()) {
1792
+ const hasNodeModules = existsSync4(join4(cwd, "node_modules"));
1793
+ let pm = "npm";
1794
+ for (const { file, pm: detectedPm } of LOCK_FILES) {
1795
+ if (existsSync4(join4(cwd, file))) {
1796
+ pm = detectedPm;
1797
+ break;
1798
+ }
1799
+ }
1800
+ return {
1801
+ installed: hasNodeModules,
1802
+ packageManager: pm,
1803
+ installCommand: INSTALL_COMMANDS[pm]
1804
+ };
1805
+ }
1673
1806
 
1674
1807
  // src/cli.ts
1675
- var VERSION = "0.3.0";
1808
+ var origEmitWarning = process.emitWarning;
1809
+ process.emitWarning = function(warning, ...args) {
1810
+ if (typeof warning === "string" && warning.includes("util._extend")) return;
1811
+ return origEmitWarning.call(process, warning, ...args);
1812
+ };
1813
+ process.on("unhandledRejection", (err) => {
1814
+ console.error(chalk.red("\n [OpenMagic] Unhandled error:"), err?.message || err);
1815
+ console.error(chalk.dim(" Please report this at https://github.com/Kalmuraee/OpenMagic/issues"));
1816
+ });
1817
+ process.on("uncaughtException", (err) => {
1818
+ console.error(chalk.red("\n [OpenMagic] Fatal error:"), err.message);
1819
+ console.error(chalk.dim(" Please report this at https://github.com/Kalmuraee/OpenMagic/issues"));
1820
+ process.exit(1);
1821
+ });
1822
+ var childProcesses = [];
1823
+ var VERSION = "0.5.0";
1676
1824
  function ask(question) {
1677
1825
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1678
1826
  return new Promise((resolve3) => {
@@ -1682,10 +1830,14 @@ function ask(question) {
1682
1830
  });
1683
1831
  });
1684
1832
  }
1685
- function waitForPort(port, timeoutMs = 3e4) {
1833
+ function waitForPort(port, timeoutMs = 3e4, shouldAbort) {
1686
1834
  const start = Date.now();
1687
1835
  return new Promise((resolve3) => {
1688
1836
  const check = async () => {
1837
+ if (shouldAbort?.()) {
1838
+ resolve3(false);
1839
+ return;
1840
+ }
1689
1841
  if (await isPortOpen(port)) {
1690
1842
  resolve3(true);
1691
1843
  return;
@@ -1699,6 +1851,86 @@ function waitForPort(port, timeoutMs = 3e4) {
1699
1851
  check();
1700
1852
  });
1701
1853
  }
1854
+ function runCommand(cmd, args, cwd = process.cwd()) {
1855
+ return new Promise((resolve3) => {
1856
+ try {
1857
+ const child = spawn(cmd, args, {
1858
+ cwd,
1859
+ stdio: ["ignore", "pipe", "pipe"],
1860
+ shell: true
1861
+ });
1862
+ child.stdout?.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.stderr?.on("data", (data) => {
1870
+ const lines = data.toString().trim().split("\n");
1871
+ for (const line of lines) {
1872
+ if (line.trim()) process.stdout.write(chalk.dim(` \u2502 ${line}
1873
+ `));
1874
+ }
1875
+ });
1876
+ child.on("error", () => resolve3(false));
1877
+ child.on("close", (code) => resolve3(code === 0));
1878
+ } catch {
1879
+ resolve3(false);
1880
+ }
1881
+ });
1882
+ }
1883
+ async function healthCheck(proxyPort, targetPort) {
1884
+ try {
1885
+ const controller = new AbortController();
1886
+ const timeout = setTimeout(() => controller.abort(), 5e3);
1887
+ const res = await fetch(`http://127.0.0.1:${proxyPort}/`, {
1888
+ signal: controller.signal,
1889
+ headers: { Accept: "text/html" }
1890
+ });
1891
+ clearTimeout(timeout);
1892
+ if (res.ok) {
1893
+ const text = await res.text();
1894
+ if (text.includes("__OPENMAGIC_LOADED__")) {
1895
+ console.log(chalk.green(" \u2713 Toolbar injection verified."));
1896
+ } else {
1897
+ console.log(chalk.yellow(" \u26A0 Page loaded but toolbar may not have injected (non-HTML response or CSP)."));
1898
+ }
1899
+ } else {
1900
+ console.log(
1901
+ chalk.yellow(` \u26A0 Dev server returned ${res.status}. Pages may have errors.`)
1902
+ );
1903
+ console.log(
1904
+ chalk.dim(" The toolbar will still appear on pages that load successfully.")
1905
+ );
1906
+ }
1907
+ } catch {
1908
+ console.log(
1909
+ chalk.yellow(" \u26A0 Could not verify proxy. The dev server may still be starting.")
1910
+ );
1911
+ console.log(
1912
+ chalk.dim(" Try refreshing the page in a few seconds.")
1913
+ );
1914
+ }
1915
+ console.log("");
1916
+ }
1917
+ function formatDevServerLine(line) {
1918
+ const trimmed = line.trim();
1919
+ if (!trimmed) return "";
1920
+ if (trimmed.startsWith("Error:") || trimmed.includes("ModuleNotFoundError") || trimmed.includes("Can't resolve")) {
1921
+ return chalk.red(` \u2502 ${trimmed}`);
1922
+ }
1923
+ if (trimmed.includes("EADDRINUSE") || trimmed.includes("address already in use")) {
1924
+ return chalk.red(` \u2502 ${trimmed}`) + "\n" + chalk.yellow(" \u2502 \u2192 Port is already in use. Stop the other process or use --port <different-port>");
1925
+ }
1926
+ if (trimmed.includes("EACCES") || trimmed.includes("permission denied")) {
1927
+ return chalk.red(` \u2502 ${trimmed}`) + "\n" + chalk.yellow(" \u2502 \u2192 Permission denied. Try a different port or check file permissions.");
1928
+ }
1929
+ if (trimmed.includes("Cannot find module") || trimmed.includes("MODULE_NOT_FOUND")) {
1930
+ return chalk.red(` \u2502 ${trimmed}`) + "\n" + chalk.yellow(" \u2502 \u2192 Missing dependency. Try running npm install.");
1931
+ }
1932
+ return chalk.dim(` \u2502 ${trimmed}`);
1933
+ }
1702
1934
  var program = new Command();
1703
1935
  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
1936
  "-l, --listen <port>",
@@ -1767,16 +1999,20 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
1767
1999
  targetPort,
1768
2000
  proxyPort + 1
1769
2001
  );
1770
- proxyServer.listen(proxyPort, "127.0.0.1", () => {
2002
+ proxyServer.listen(proxyPort, "127.0.0.1", async () => {
1771
2003
  console.log("");
1772
2004
  console.log(
1773
2005
  chalk.bold.green(` \u{1F680} Proxy running at \u2192 `) + chalk.bold.underline.cyan(`http://localhost:${proxyPort}`)
1774
2006
  );
1775
2007
  console.log("");
2008
+ await healthCheck(proxyPort, targetPort);
1776
2009
  console.log(
1777
2010
  chalk.dim(" Open the URL above in your browser to start.")
1778
2011
  );
1779
2012
  console.log(chalk.dim(" Press Ctrl+C to stop."));
2013
+ console.log(
2014
+ chalk.dim(" Errors below are from your dev server, not OpenMagic.")
2015
+ );
1780
2016
  console.log("");
1781
2017
  if (opts.open !== false) {
1782
2018
  open(`http://localhost:${proxyPort}`).catch(() => {
@@ -1811,6 +2047,34 @@ async function offerToStartDevServer(expectedPort) {
1811
2047
  console.log("");
1812
2048
  return false;
1813
2049
  }
2050
+ const deps = checkDependenciesInstalled();
2051
+ if (!deps.installed) {
2052
+ console.log(
2053
+ chalk.yellow(" \u26A0 node_modules/ not found. Dependencies need to be installed.")
2054
+ );
2055
+ console.log("");
2056
+ const answer = await ask(
2057
+ chalk.white(` Run `) + chalk.cyan(deps.installCommand) + chalk.white("? ") + chalk.dim("(Y/n) ")
2058
+ );
2059
+ if (answer.toLowerCase() === "n" || answer.toLowerCase() === "no") {
2060
+ console.log("");
2061
+ console.log(chalk.dim(` Run ${deps.installCommand} manually, then try again.`));
2062
+ console.log("");
2063
+ return false;
2064
+ }
2065
+ console.log("");
2066
+ console.log(chalk.dim(` Installing dependencies with ${deps.packageManager}...`));
2067
+ const [installCmd, ...installArgs] = deps.installCommand.split(" ");
2068
+ const installed = await runCommand(installCmd, installArgs);
2069
+ if (!installed) {
2070
+ console.log(chalk.red(" \u2717 Dependency installation failed."));
2071
+ console.log(chalk.dim(` Try running ${deps.installCommand} manually.`));
2072
+ console.log("");
2073
+ return false;
2074
+ }
2075
+ console.log(chalk.green(" \u2713 Dependencies installed."));
2076
+ console.log("");
2077
+ }
1814
2078
  let chosen = scripts[0];
1815
2079
  if (scripts.length === 1) {
1816
2080
  console.log(
@@ -1868,52 +2132,66 @@ async function offerToStartDevServer(expectedPort) {
1868
2132
  console.log(
1869
2133
  chalk.dim(` Starting `) + chalk.cyan(`npm run ${chosen.name}`) + chalk.dim("...")
1870
2134
  );
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
- });
1886
- 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}
1891
- `));
2135
+ const depsInfo = checkDependenciesInstalled();
2136
+ const runCmd = depsInfo.packageManager === "yarn" ? "yarn" : depsInfo.packageManager === "pnpm" ? "pnpm" : depsInfo.packageManager === "bun" ? "bun" : "npm";
2137
+ const runArgs = runCmd === "npm" ? ["run", chosen.name] : [chosen.name];
2138
+ let child;
2139
+ try {
2140
+ child = spawn(runCmd, runArgs, {
2141
+ cwd: process.cwd(),
2142
+ stdio: ["ignore", "pipe", "pipe"],
2143
+ detached: false,
2144
+ shell: true,
2145
+ env: {
2146
+ ...process.env,
2147
+ PORT: String(port),
2148
+ BROWSER: "none",
2149
+ BROWSER_NONE: "true"
1892
2150
  }
2151
+ });
2152
+ } catch (e) {
2153
+ console.log(chalk.red(` \u2717 Failed to start: ${e.message}`));
2154
+ return false;
2155
+ }
2156
+ childProcesses.push(child);
2157
+ let childExited = false;
2158
+ child.stdout?.on("data", (data) => {
2159
+ for (const line of data.toString().trim().split("\n")) {
2160
+ const formatted = formatDevServerLine(line);
2161
+ if (formatted) process.stdout.write(formatted + "\n");
1893
2162
  }
1894
2163
  });
1895
2164
  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}
1900
- `));
1901
- }
2165
+ for (const line of data.toString().trim().split("\n")) {
2166
+ const formatted = formatDevServerLine(line);
2167
+ if (formatted) process.stdout.write(formatted + "\n");
1902
2168
  }
1903
2169
  });
1904
2170
  child.on("error", (err) => {
2171
+ childExited = true;
1905
2172
  console.log(chalk.red(` \u2717 Failed to start: ${err.message}`));
1906
2173
  });
1907
2174
  child.on("exit", (code) => {
2175
+ childExited = true;
1908
2176
  if (code !== null && code !== 0) {
1909
2177
  console.log(chalk.red(` \u2717 Dev server exited with code ${code}`));
1910
2178
  }
1911
2179
  });
1912
2180
  const cleanup = () => {
1913
- try {
1914
- child.kill("SIGTERM");
1915
- } catch {
2181
+ for (const cp of childProcesses) {
2182
+ try {
2183
+ cp.kill("SIGTERM");
2184
+ } catch {
2185
+ }
1916
2186
  }
2187
+ setTimeout(() => {
2188
+ for (const cp of childProcesses) {
2189
+ try {
2190
+ cp.kill("SIGKILL");
2191
+ } catch {
2192
+ }
2193
+ }
2194
+ }, 3e3);
1917
2195
  };
1918
2196
  process.on("exit", cleanup);
1919
2197
  process.on("SIGINT", cleanup);
@@ -1921,7 +2199,17 @@ async function offerToStartDevServer(expectedPort) {
1921
2199
  console.log(
1922
2200
  chalk.dim(` Waiting for port ${port}...`)
1923
2201
  );
1924
- const isUp = await waitForPort(port, 3e4);
2202
+ const isUp = await waitForPort(port, 3e4, () => childExited);
2203
+ if (childExited && !isUp) {
2204
+ console.log(
2205
+ chalk.red(` \u2717 Dev server exited before it was ready.`)
2206
+ );
2207
+ console.log(
2208
+ chalk.dim(` Check the error output above and fix the issue.`)
2209
+ );
2210
+ console.log("");
2211
+ return false;
2212
+ }
1925
2213
  if (!isUp) {
1926
2214
  console.log(
1927
2215
  chalk.yellow(` \u26A0 Port ${port} didn't open after 30s.`)