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 +369 -81
- package/dist/cli.js.map +1 -1
- package/dist/toolbar/index.global.js +1 -1
- package/dist/toolbar/index.global.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
74
|
+
const toolbarScript = buildInjectionScript(serverPort, token);
|
|
75
|
+
res.writeHead(502, { "Content-Type": "text/html" });
|
|
71
76
|
res.end(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
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
|
|
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
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
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
|
|
1897
|
-
|
|
1898
|
-
if (
|
|
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
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
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.`)
|