openmagic 0.36.4 → 0.38.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 +453 -185
- package/dist/cli.js.map +1 -1
- package/dist/toolbar/index.global.js +57 -53
- package/dist/toolbar/index.global.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import open from "open";
|
|
7
|
-
import { resolve as resolve3, join as
|
|
8
|
-
import { existsSync as
|
|
9
|
-
import { spawn as
|
|
7
|
+
import { resolve as resolve3, join as join6 } from "path";
|
|
8
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
9
|
+
import { spawn as spawn5, execSync as execSync2 } from "child_process";
|
|
10
|
+
import http2 from "http";
|
|
10
11
|
import { createInterface } from "readline";
|
|
11
12
|
|
|
12
13
|
// src/proxy.ts
|
|
@@ -31,8 +32,8 @@ function validateToken(token) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
// src/server.ts
|
|
34
|
-
import { readFileSync as
|
|
35
|
-
import { join as
|
|
35
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
36
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
36
37
|
import { fileURLToPath } from "url";
|
|
37
38
|
import { WebSocketServer, WebSocket } from "ws";
|
|
38
39
|
|
|
@@ -73,19 +74,145 @@ function saveConfig(updates) {
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// src/llm/cli-detect.ts
|
|
78
|
+
import { spawn } from "child_process";
|
|
79
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
80
|
+
import { join as join2 } from "path";
|
|
81
|
+
import { homedir as homedir2 } from "os";
|
|
82
|
+
var CLI_AGENTS = [
|
|
83
|
+
{ id: "claude-code", name: "Claude Code", command: "claude" },
|
|
84
|
+
{ id: "codex-cli", name: "Codex CLI", command: "codex" },
|
|
85
|
+
{ id: "gemini-cli", name: "Gemini CLI", command: "gemini" }
|
|
86
|
+
];
|
|
87
|
+
var cachedResults = null;
|
|
88
|
+
var cacheTimestamp = 0;
|
|
89
|
+
var CACHE_TTL = 6e4;
|
|
90
|
+
function invalidateCliCache() {
|
|
91
|
+
cachedResults = null;
|
|
92
|
+
cacheTimestamp = 0;
|
|
93
|
+
}
|
|
94
|
+
async function detectAvailableClis() {
|
|
95
|
+
if (cachedResults && Date.now() - cacheTimestamp < CACHE_TTL) {
|
|
96
|
+
return cachedResults;
|
|
97
|
+
}
|
|
98
|
+
const results = await Promise.all(
|
|
99
|
+
CLI_AGENTS.map(async (agent) => {
|
|
100
|
+
const { installed, version } = await checkInstalled(agent.command);
|
|
101
|
+
const authenticated = installed ? checkAuthenticated(agent.id) : false;
|
|
102
|
+
return {
|
|
103
|
+
id: agent.id,
|
|
104
|
+
name: agent.name,
|
|
105
|
+
command: agent.command,
|
|
106
|
+
installed,
|
|
107
|
+
authenticated,
|
|
108
|
+
version
|
|
109
|
+
};
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
cachedResults = results;
|
|
113
|
+
cacheTimestamp = Date.now();
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
function checkInstalled(command) {
|
|
117
|
+
return new Promise((resolve4) => {
|
|
118
|
+
try {
|
|
119
|
+
const proc = spawn(command, ["--version"], {
|
|
120
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
121
|
+
timeout: 5e3
|
|
122
|
+
});
|
|
123
|
+
let stdout = "";
|
|
124
|
+
proc.stdout?.on("data", (d) => {
|
|
125
|
+
stdout += d.toString();
|
|
126
|
+
});
|
|
127
|
+
proc.on("error", () => resolve4({ installed: false }));
|
|
128
|
+
proc.on("close", (code) => {
|
|
129
|
+
if (code === 0) {
|
|
130
|
+
const match = stdout.match(/(\d+\.\d+\.\d+)/);
|
|
131
|
+
resolve4({ installed: true, version: match?.[1] });
|
|
132
|
+
} else {
|
|
133
|
+
resolve4({ installed: false });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
137
|
+
resolve4({ installed: false });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function checkAuthenticated(cliId) {
|
|
142
|
+
switch (cliId) {
|
|
143
|
+
case "claude-code":
|
|
144
|
+
return isClaudeAuthenticated();
|
|
145
|
+
case "codex-cli":
|
|
146
|
+
return isCodexAuthenticated();
|
|
147
|
+
case "gemini-cli":
|
|
148
|
+
return isGeminiAuthenticated();
|
|
149
|
+
default:
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function isClaudeAuthenticated() {
|
|
154
|
+
if (process.env.ANTHROPIC_API_KEY) return true;
|
|
155
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) return true;
|
|
156
|
+
try {
|
|
157
|
+
const configPath = join2(
|
|
158
|
+
process.env.CLAUDE_CONFIG_DIR || join2(homedir2(), ".claude"),
|
|
159
|
+
".claude.json"
|
|
160
|
+
);
|
|
161
|
+
if (existsSync2(configPath)) {
|
|
162
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
163
|
+
if (config.oauthAccount) return true;
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
function isCodexAuthenticated() {
|
|
170
|
+
if (process.env.OPENAI_API_KEY) return true;
|
|
171
|
+
try {
|
|
172
|
+
const authPath = join2(
|
|
173
|
+
process.env.CODEX_HOME || join2(homedir2(), ".codex"),
|
|
174
|
+
"auth.json"
|
|
175
|
+
);
|
|
176
|
+
if (existsSync2(authPath)) {
|
|
177
|
+
const auth = JSON.parse(readFileSync2(authPath, "utf-8"));
|
|
178
|
+
if (auth.auth_mode && (auth.OPENAI_API_KEY || auth.tokens)) return true;
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
function isGeminiAuthenticated() {
|
|
185
|
+
if (process.env.GEMINI_API_KEY) return true;
|
|
186
|
+
if (process.env.GOOGLE_API_KEY) return true;
|
|
187
|
+
try {
|
|
188
|
+
const credsPath = join2(homedir2(), ".gemini", "oauth_creds.json");
|
|
189
|
+
if (existsSync2(credsPath)) {
|
|
190
|
+
const creds = JSON.parse(readFileSync2(credsPath, "utf-8"));
|
|
191
|
+
if (creds.refresh_token) return true;
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
76
198
|
// src/filesystem.ts
|
|
77
199
|
import {
|
|
78
|
-
readFileSync as
|
|
200
|
+
readFileSync as readFileSync3,
|
|
79
201
|
writeFileSync as writeFileSync2,
|
|
80
|
-
existsSync as
|
|
202
|
+
existsSync as existsSync3,
|
|
81
203
|
lstatSync,
|
|
82
204
|
readdirSync,
|
|
83
205
|
copyFileSync,
|
|
84
206
|
mkdirSync as mkdirSync2,
|
|
85
207
|
realpathSync,
|
|
86
|
-
|
|
208
|
+
unlinkSync,
|
|
209
|
+
rmSync,
|
|
210
|
+
openSync,
|
|
211
|
+
fsyncSync,
|
|
212
|
+
closeSync,
|
|
213
|
+
renameSync as renameSync2
|
|
87
214
|
} from "fs";
|
|
88
|
-
import { join as
|
|
215
|
+
import { join as join3, resolve, relative, dirname, extname } from "path";
|
|
89
216
|
import { tmpdir } from "os";
|
|
90
217
|
import { createHash } from "crypto";
|
|
91
218
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -134,33 +261,39 @@ function isPathSafe(filePath, roots) {
|
|
|
134
261
|
return !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\") && (!realRel.startsWith("..") && !realRel.startsWith("/") && !realRel.startsWith("\\"));
|
|
135
262
|
});
|
|
136
263
|
}
|
|
264
|
+
var fileMetadata = /* @__PURE__ */ new Map();
|
|
137
265
|
function readFileSafe(filePath, roots) {
|
|
138
266
|
if (!isPathSafe(filePath, roots)) {
|
|
139
267
|
return { error: "Path is outside allowed roots" };
|
|
140
268
|
}
|
|
141
|
-
if (!
|
|
269
|
+
if (!existsSync3(filePath)) {
|
|
142
270
|
return { error: "File not found" };
|
|
143
271
|
}
|
|
144
272
|
try {
|
|
145
|
-
const
|
|
273
|
+
const raw = readFileSync3(filePath);
|
|
274
|
+
const hasBOM = raw[0] === 239 && raw[1] === 187 && raw[2] === 191;
|
|
275
|
+
let content = hasBOM ? raw.subarray(3).toString("utf-8") : raw.toString("utf-8");
|
|
276
|
+
const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
|
|
277
|
+
if (lineEnding === "\r\n") content = content.replace(/\r\n/g, "\n");
|
|
278
|
+
fileMetadata.set(resolve(filePath), { hasBOM, lineEnding });
|
|
146
279
|
return { content };
|
|
147
280
|
} catch (e) {
|
|
148
281
|
return { error: `Failed to read file: ${e.message}` };
|
|
149
282
|
}
|
|
150
283
|
}
|
|
151
|
-
var BACKUP_DIR =
|
|
284
|
+
var BACKUP_DIR = join3(tmpdir(), "openmagic-backups");
|
|
152
285
|
var backupMap = /* @__PURE__ */ new Map();
|
|
153
286
|
function getBackupPath(filePath) {
|
|
154
287
|
const hash = createHash("md5").update(resolve(filePath)).digest("hex").slice(0, 12);
|
|
155
288
|
const name = filePath.split(/[/\\]/).pop() || "file";
|
|
156
|
-
return
|
|
289
|
+
return join3(BACKUP_DIR, `${hash}_${name}`);
|
|
157
290
|
}
|
|
158
291
|
function getBackupForFile(filePath) {
|
|
159
292
|
return backupMap.get(resolve(filePath));
|
|
160
293
|
}
|
|
161
294
|
function cleanupBackups() {
|
|
162
295
|
try {
|
|
163
|
-
if (
|
|
296
|
+
if (existsSync3(BACKUP_DIR)) {
|
|
164
297
|
rmSync(BACKUP_DIR, { recursive: true, force: true });
|
|
165
298
|
}
|
|
166
299
|
} catch {
|
|
@@ -173,17 +306,39 @@ function writeFileSafe(filePath, content, roots) {
|
|
|
173
306
|
}
|
|
174
307
|
try {
|
|
175
308
|
let backupPath;
|
|
176
|
-
if (
|
|
177
|
-
if (!
|
|
309
|
+
if (existsSync3(filePath)) {
|
|
310
|
+
if (!existsSync3(BACKUP_DIR)) mkdirSync2(BACKUP_DIR, { recursive: true });
|
|
178
311
|
backupPath = getBackupPath(filePath);
|
|
179
312
|
copyFileSync(filePath, backupPath);
|
|
180
313
|
backupMap.set(resolve(filePath), backupPath);
|
|
181
314
|
}
|
|
182
315
|
const dir = dirname(filePath);
|
|
183
|
-
if (!
|
|
316
|
+
if (!existsSync3(dir)) {
|
|
184
317
|
mkdirSync2(dir, { recursive: true });
|
|
185
318
|
}
|
|
186
|
-
|
|
319
|
+
let output = content;
|
|
320
|
+
const meta = fileMetadata.get(resolve(filePath));
|
|
321
|
+
if (meta) {
|
|
322
|
+
if (meta.lineEnding === "\r\n") output = output.replace(/\n/g, "\r\n");
|
|
323
|
+
if (meta.hasBOM) output = "\uFEFF" + output;
|
|
324
|
+
}
|
|
325
|
+
const tmpPath = filePath + ".openmagic-tmp-" + Date.now();
|
|
326
|
+
writeFileSync2(tmpPath, output, "utf-8");
|
|
327
|
+
try {
|
|
328
|
+
const fd = openSync(tmpPath, "r");
|
|
329
|
+
fsyncSync(fd);
|
|
330
|
+
closeSync(fd);
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
renameSync2(tmpPath, filePath);
|
|
335
|
+
} catch {
|
|
336
|
+
writeFileSync2(filePath, output, "utf-8");
|
|
337
|
+
try {
|
|
338
|
+
unlinkSync(tmpPath);
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
}
|
|
187
342
|
return { ok: true, backupPath };
|
|
188
343
|
} catch (e) {
|
|
189
344
|
return { ok: false, error: `Failed to write file: ${e.message}` };
|
|
@@ -207,7 +362,7 @@ function listFiles(rootPath, roots, maxDepth = 4) {
|
|
|
207
362
|
if (entries.length >= MAX_LIST_ENTRIES) return;
|
|
208
363
|
if (IGNORED_DIRS.has(item)) continue;
|
|
209
364
|
if (item.startsWith(".") && item !== ".env.example") continue;
|
|
210
|
-
const fullPath =
|
|
365
|
+
const fullPath = join3(dir, item);
|
|
211
366
|
let stat;
|
|
212
367
|
try {
|
|
213
368
|
stat = lstatSync(fullPath);
|
|
@@ -271,7 +426,7 @@ function grepFiles(pattern, searchRoot, roots, maxResults = 30) {
|
|
|
271
426
|
for (const item of items) {
|
|
272
427
|
if (results.length >= maxResults || filesScanned >= MAX_GREP_FILES_SCANNED) return;
|
|
273
428
|
if (IGNORED_DIRS.has(item) || item.startsWith(".") && item !== ".env.example") continue;
|
|
274
|
-
const fullPath =
|
|
429
|
+
const fullPath = join3(dir, item);
|
|
275
430
|
let stat;
|
|
276
431
|
try {
|
|
277
432
|
stat = lstatSync(fullPath);
|
|
@@ -287,7 +442,7 @@ function grepFiles(pattern, searchRoot, roots, maxResults = 30) {
|
|
|
287
442
|
if (stat.size > MAX_GREP_FILE_SIZE) continue;
|
|
288
443
|
filesScanned++;
|
|
289
444
|
try {
|
|
290
|
-
const content =
|
|
445
|
+
const content = readFileSync3(fullPath, "utf-8");
|
|
291
446
|
const lines = content.split("\n");
|
|
292
447
|
let fileMatches = 0;
|
|
293
448
|
for (let i = 0; i < lines.length && fileMatches < 5; i++) {
|
|
@@ -1065,11 +1220,17 @@ You MUST respond with valid JSON in this exact format:
|
|
|
1065
1220
|
5. Keep modifications minimal \u2014 change only what's needed. Do NOT rewrite entire files.
|
|
1066
1221
|
6. If the grounded files don't contain the code you need to modify, return: {"modifications":[],"explanation":"NEED_FILE: exact/relative/path/to/file.ext"}
|
|
1067
1222
|
7. To search for a pattern across the codebase, return: {"modifications":[],"explanation":"SEARCH_FILES: \\"pattern\\" in optional/path/"}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1223
|
+
8. For style changes: check the dependencies (package.json) to know if the project uses Tailwind, MUI, etc. Use the project's styling approach, not raw CSS
|
|
1224
|
+
9. Use the selected element's cssSelector, className, parentContainerStyles, and siblings to understand the full layout context
|
|
1225
|
+
10. Use the page URL route and componentHint to identify the correct source file to modify
|
|
1226
|
+
11. Use childrenLayout pixel measurements for spacing. gapToNext.vertical=0 means elements are touching.
|
|
1227
|
+
12. Use resolvedClasses to see actual CSS values behind utility classes (space-y-6 = margin-top: 1.5rem).
|
|
1228
|
+
13. Check themeState.darkMode \u2014 if true, use dark-mode-aware colors and respect the project's dark mode classes.
|
|
1229
|
+
14. Use cssVariables to leverage existing design tokens (var(--color-primary)) instead of hardcoding hex values.
|
|
1230
|
+
15. Check activeBreakpoints to know which responsive breakpoint is active. Suggest responsive-aware changes.
|
|
1231
|
+
16. Check visibilityState \u2014 the element may be scrolled out of view, hidden, or inside a scrollable container.
|
|
1232
|
+
17. Always preserve existing code style, conventions, and indentation
|
|
1233
|
+
18. ALWAYS respond with valid JSON only \u2014 no text before or after the JSON object`;
|
|
1073
1234
|
function buildContextParts(context) {
|
|
1074
1235
|
const parts = {};
|
|
1075
1236
|
if (context.selectedElement) {
|
|
@@ -1111,6 +1272,15 @@ function buildContextParts(context) {
|
|
|
1111
1272
|
if (el.resolvedClasses?.length) {
|
|
1112
1273
|
elementData.resolvedClasses = el.resolvedClasses;
|
|
1113
1274
|
}
|
|
1275
|
+
if (el.themeState) elementData.themeState = el.themeState;
|
|
1276
|
+
if (el.cssVariables && Object.keys(el.cssVariables).length) elementData.cssVariables = el.cssVariables;
|
|
1277
|
+
if (el.stackingContext) elementData.stackingContext = el.stackingContext;
|
|
1278
|
+
if (el.visibilityState) elementData.visibilityState = el.visibilityState;
|
|
1279
|
+
if (el.activeBreakpoints?.length) elementData.activeBreakpoints = el.activeBreakpoints;
|
|
1280
|
+
if (el.pseudoElements && (el.pseudoElements.before !== "none" || el.pseudoElements.after !== "none")) {
|
|
1281
|
+
elementData.pseudoElements = el.pseudoElements;
|
|
1282
|
+
}
|
|
1283
|
+
if (el.formState) elementData.formState = el.formState;
|
|
1114
1284
|
parts.selectedElement = JSON.stringify(elementData, null, 2);
|
|
1115
1285
|
}
|
|
1116
1286
|
if (context.files?.length) {
|
|
@@ -1519,13 +1689,13 @@ async function chatGoogle(model, apiKey, messages, context, onChunk, onDone, onE
|
|
|
1519
1689
|
}
|
|
1520
1690
|
|
|
1521
1691
|
// src/llm/claude-code.ts
|
|
1522
|
-
import { spawn } from "child_process";
|
|
1692
|
+
import { spawn as spawn2 } from "child_process";
|
|
1523
1693
|
async function chatClaudeCode(messages, context, onChunk, onDone, onError) {
|
|
1524
1694
|
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
1525
1695
|
const userPrompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "Help me with this element.";
|
|
1526
1696
|
const contextParts = buildContextParts(context);
|
|
1527
1697
|
const fullPrompt = buildUserMessage(userPrompt, contextParts);
|
|
1528
|
-
const proc =
|
|
1698
|
+
const proc = spawn2(
|
|
1529
1699
|
"claude",
|
|
1530
1700
|
[
|
|
1531
1701
|
"-p",
|
|
@@ -1639,7 +1809,7 @@ function extractText(event) {
|
|
|
1639
1809
|
}
|
|
1640
1810
|
|
|
1641
1811
|
// src/llm/codex-cli.ts
|
|
1642
|
-
import { spawn as
|
|
1812
|
+
import { spawn as spawn3 } from "child_process";
|
|
1643
1813
|
async function chatCodexCli(messages, context, onChunk, onDone, onError) {
|
|
1644
1814
|
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
1645
1815
|
const userPrompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "Help me with this element.";
|
|
@@ -1647,7 +1817,7 @@ async function chatCodexCli(messages, context, onChunk, onDone, onError) {
|
|
|
1647
1817
|
const fullPrompt = `${SYSTEM_PROMPT}
|
|
1648
1818
|
|
|
1649
1819
|
${buildUserMessage(userPrompt, contextParts)}`;
|
|
1650
|
-
const proc =
|
|
1820
|
+
const proc = spawn3(
|
|
1651
1821
|
"codex",
|
|
1652
1822
|
["exec", "--full-auto", "--json", "--skip-git-repo-check", "-"],
|
|
1653
1823
|
{
|
|
@@ -1709,20 +1879,17 @@ ${buildUserMessage(userPrompt, contextParts)}`;
|
|
|
1709
1879
|
});
|
|
1710
1880
|
}
|
|
1711
1881
|
function extractCodexText(event) {
|
|
1712
|
-
if (event.type === "item.completed"
|
|
1882
|
+
if (event.type === "item.completed") {
|
|
1713
1883
|
const item = event.item;
|
|
1714
1884
|
if (item?.type === "agent_message" && typeof item.text === "string") {
|
|
1715
1885
|
return item.text;
|
|
1716
1886
|
}
|
|
1717
1887
|
}
|
|
1718
|
-
if (event.type === "error" && typeof event.message === "string") {
|
|
1719
|
-
return void 0;
|
|
1720
|
-
}
|
|
1721
1888
|
return void 0;
|
|
1722
1889
|
}
|
|
1723
1890
|
|
|
1724
1891
|
// src/llm/gemini-cli.ts
|
|
1725
|
-
import { spawn as
|
|
1892
|
+
import { spawn as spawn4 } from "child_process";
|
|
1726
1893
|
async function chatGeminiCli(messages, context, onChunk, onDone, onError) {
|
|
1727
1894
|
const lastUserMsg = [...messages].reverse().find((m) => m.role === "user");
|
|
1728
1895
|
const userPrompt = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : "Help me with this element.";
|
|
@@ -1730,13 +1897,9 @@ async function chatGeminiCli(messages, context, onChunk, onDone, onError) {
|
|
|
1730
1897
|
const fullPrompt = `${SYSTEM_PROMPT}
|
|
1731
1898
|
|
|
1732
1899
|
${buildUserMessage(userPrompt, contextParts)}`;
|
|
1733
|
-
const proc =
|
|
1900
|
+
const proc = spawn4(
|
|
1734
1901
|
"gemini",
|
|
1735
1902
|
[
|
|
1736
|
-
"-p",
|
|
1737
|
-
userPrompt,
|
|
1738
|
-
"--output-format",
|
|
1739
|
-
"stream-json",
|
|
1740
1903
|
"--yolo"
|
|
1741
1904
|
],
|
|
1742
1905
|
{
|
|
@@ -1744,41 +1907,14 @@ ${buildUserMessage(userPrompt, contextParts)}`;
|
|
|
1744
1907
|
cwd: process.cwd()
|
|
1745
1908
|
}
|
|
1746
1909
|
);
|
|
1747
|
-
proc.stdin.write(
|
|
1748
|
-
|
|
1749
|
-
${buildUserMessage("", contextParts)}`);
|
|
1910
|
+
proc.stdin.write(fullPrompt);
|
|
1750
1911
|
proc.stdin.end();
|
|
1751
1912
|
let fullContent = "";
|
|
1752
|
-
let resultContent = "";
|
|
1753
|
-
let buffer = "";
|
|
1754
1913
|
let errOutput = "";
|
|
1755
1914
|
proc.stdout.on("data", (data) => {
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
for (const line of lines) {
|
|
1760
|
-
if (!line.trim()) continue;
|
|
1761
|
-
try {
|
|
1762
|
-
const event = JSON.parse(line);
|
|
1763
|
-
if (event.type === "result") {
|
|
1764
|
-
if (typeof event.result === "string") {
|
|
1765
|
-
resultContent = event.result;
|
|
1766
|
-
}
|
|
1767
|
-
continue;
|
|
1768
|
-
}
|
|
1769
|
-
const text = extractGeminiText(event);
|
|
1770
|
-
if (text) {
|
|
1771
|
-
fullContent += text;
|
|
1772
|
-
onChunk(text);
|
|
1773
|
-
}
|
|
1774
|
-
} catch {
|
|
1775
|
-
const trimmed = line.trim();
|
|
1776
|
-
if (trimmed) {
|
|
1777
|
-
fullContent += trimmed + "\n";
|
|
1778
|
-
onChunk(trimmed + "\n");
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1915
|
+
const text = data.toString();
|
|
1916
|
+
fullContent += text;
|
|
1917
|
+
onChunk(text);
|
|
1782
1918
|
});
|
|
1783
1919
|
proc.stderr.on("data", (data) => {
|
|
1784
1920
|
errOutput += data.toString();
|
|
@@ -1791,23 +1927,8 @@ ${buildUserMessage("", contextParts)}`);
|
|
|
1791
1927
|
}
|
|
1792
1928
|
});
|
|
1793
1929
|
proc.on("close", (code) => {
|
|
1794
|
-
if (
|
|
1795
|
-
|
|
1796
|
-
const event = JSON.parse(buffer);
|
|
1797
|
-
if (event.type === "result" && typeof event.result === "string") {
|
|
1798
|
-
resultContent = event.result;
|
|
1799
|
-
} else {
|
|
1800
|
-
const text = extractGeminiText(event);
|
|
1801
|
-
if (text) fullContent += text;
|
|
1802
|
-
}
|
|
1803
|
-
} catch {
|
|
1804
|
-
const trimmed = buffer.trim();
|
|
1805
|
-
if (trimmed) fullContent += trimmed;
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
const finalContent = resultContent || fullContent;
|
|
1809
|
-
if (code === 0 || finalContent.trim()) {
|
|
1810
|
-
onDone({ content: finalContent });
|
|
1930
|
+
if (code === 0 || fullContent.trim()) {
|
|
1931
|
+
onDone({ content: fullContent });
|
|
1811
1932
|
} else {
|
|
1812
1933
|
const err = errOutput.trim();
|
|
1813
1934
|
if (err.includes("auth") || err.includes("GEMINI_API_KEY") || err.includes("credentials") || err.includes("login")) {
|
|
@@ -1818,21 +1939,6 @@ ${buildUserMessage("", contextParts)}`);
|
|
|
1818
1939
|
}
|
|
1819
1940
|
});
|
|
1820
1941
|
}
|
|
1821
|
-
function extractGeminiText(event) {
|
|
1822
|
-
if (event.type === "assistant" || event.type === "message") {
|
|
1823
|
-
const content = event.content ?? event.message?.content;
|
|
1824
|
-
if (Array.isArray(content)) {
|
|
1825
|
-
return content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join("");
|
|
1826
|
-
}
|
|
1827
|
-
if (typeof content === "string") return content;
|
|
1828
|
-
if (typeof event.text === "string") return event.text;
|
|
1829
|
-
}
|
|
1830
|
-
if (event.type === "content_block_delta") {
|
|
1831
|
-
const delta = event.delta;
|
|
1832
|
-
if (typeof delta?.text === "string") return delta.text;
|
|
1833
|
-
}
|
|
1834
|
-
return void 0;
|
|
1835
|
-
}
|
|
1836
1942
|
|
|
1837
1943
|
// src/llm/proxy.ts
|
|
1838
1944
|
var OPENAI_COMPATIBLE_PROVIDERS = /* @__PURE__ */ new Set([
|
|
@@ -1850,6 +1956,11 @@ var OPENAI_COMPATIBLE_PROVIDERS = /* @__PURE__ */ new Set([
|
|
|
1850
1956
|
"doubao"
|
|
1851
1957
|
]);
|
|
1852
1958
|
function extractJsonFromResponse(content) {
|
|
1959
|
+
try {
|
|
1960
|
+
JSON.parse(content);
|
|
1961
|
+
return content;
|
|
1962
|
+
} catch {
|
|
1963
|
+
}
|
|
1853
1964
|
const mdMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
1854
1965
|
if (mdMatch?.[1]) {
|
|
1855
1966
|
const candidate = mdMatch[1].trim();
|
|
@@ -1888,11 +1999,28 @@ function extractJsonFromResponse(content) {
|
|
|
1888
1999
|
JSON.parse(candidate);
|
|
1889
2000
|
return candidate;
|
|
1890
2001
|
} catch {
|
|
1891
|
-
|
|
2002
|
+
break;
|
|
1892
2003
|
}
|
|
1893
2004
|
}
|
|
1894
2005
|
}
|
|
1895
2006
|
}
|
|
2007
|
+
if (depth > 0) {
|
|
2008
|
+
let repaired = content.substring(start);
|
|
2009
|
+
if (inString) repaired += '"';
|
|
2010
|
+
while (depth > 0) {
|
|
2011
|
+
repaired += "}";
|
|
2012
|
+
depth--;
|
|
2013
|
+
}
|
|
2014
|
+
try {
|
|
2015
|
+
JSON.parse(repaired);
|
|
2016
|
+
return repaired;
|
|
2017
|
+
} catch {
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
const explMatch = content.match(/"explanation"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
|
2021
|
+
if (explMatch) {
|
|
2022
|
+
return JSON.stringify({ modifications: [], explanation: JSON.parse('"' + explMatch[1] + '"') });
|
|
2023
|
+
}
|
|
1896
2024
|
return null;
|
|
1897
2025
|
}
|
|
1898
2026
|
async function handleLlmChat(params, onChunk, onDone, onError) {
|
|
@@ -1909,13 +2037,19 @@ async function handleLlmChat(params, onChunk, onDone, onError) {
|
|
|
1909
2037
|
}
|
|
1910
2038
|
onDone({ content: result.content, modifications });
|
|
1911
2039
|
};
|
|
2040
|
+
const cliOnError = (error) => {
|
|
2041
|
+
if (error.includes("not found") || error.includes("ENOENT") || error.includes("not authenticated") || error.includes("not logged in")) {
|
|
2042
|
+
invalidateCliCache();
|
|
2043
|
+
}
|
|
2044
|
+
onError(error);
|
|
2045
|
+
};
|
|
1912
2046
|
try {
|
|
1913
2047
|
if (provider === "claude-code") {
|
|
1914
|
-
await chatClaudeCode(messages, context, onChunk, wrappedOnDone,
|
|
2048
|
+
await chatClaudeCode(messages, context, onChunk, wrappedOnDone, cliOnError);
|
|
1915
2049
|
} else if (provider === "codex-cli") {
|
|
1916
|
-
await chatCodexCli(messages, context, onChunk, wrappedOnDone,
|
|
2050
|
+
await chatCodexCli(messages, context, onChunk, wrappedOnDone, cliOnError);
|
|
1917
2051
|
} else if (provider === "gemini-cli") {
|
|
1918
|
-
await chatGeminiCli(messages, context, onChunk, wrappedOnDone,
|
|
2052
|
+
await chatGeminiCli(messages, context, onChunk, wrappedOnDone, cliOnError);
|
|
1919
2053
|
} else if (provider === "anthropic") {
|
|
1920
2054
|
await chatAnthropic(model, apiKey, messages, context, onChunk, wrappedOnDone, onError);
|
|
1921
2055
|
} else if (provider === "google") {
|
|
@@ -2124,7 +2258,7 @@ async function handleMessage(ws, msg, state, roots) {
|
|
|
2124
2258
|
break;
|
|
2125
2259
|
}
|
|
2126
2260
|
try {
|
|
2127
|
-
const backupContent =
|
|
2261
|
+
const backupContent = readFileSync4(backupPath, "utf-8");
|
|
2128
2262
|
const writeResult = writeFileSafe(payload.path, backupContent, roots);
|
|
2129
2263
|
if (!writeResult.ok) {
|
|
2130
2264
|
sendError(ws, "fs_error", writeResult.error || "Undo failed", msg.id);
|
|
@@ -2179,17 +2313,35 @@ async function handleMessage(ws, msg, state, roots) {
|
|
|
2179
2313
|
}
|
|
2180
2314
|
case "config.get": {
|
|
2181
2315
|
const config = loadConfig();
|
|
2316
|
+
const detectedClis = await detectAvailableClis();
|
|
2317
|
+
const cliStatuses = detectedClis.map((c) => ({
|
|
2318
|
+
id: c.id,
|
|
2319
|
+
name: c.name,
|
|
2320
|
+
installed: c.installed,
|
|
2321
|
+
authenticated: c.authenticated,
|
|
2322
|
+
version: c.version
|
|
2323
|
+
}));
|
|
2324
|
+
let provider = config.provider || "";
|
|
2325
|
+
let model = config.model || "";
|
|
2326
|
+
if (!provider) {
|
|
2327
|
+
const bestCli = detectedClis.find((c) => c.installed && c.authenticated);
|
|
2328
|
+
if (bestCli) {
|
|
2329
|
+
provider = bestCli.id;
|
|
2330
|
+
model = bestCli.id;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2182
2333
|
send(ws, {
|
|
2183
2334
|
id: msg.id,
|
|
2184
2335
|
type: "config.value",
|
|
2185
2336
|
payload: {
|
|
2186
|
-
provider
|
|
2187
|
-
model
|
|
2188
|
-
hasApiKey: !!(config.apiKeys?.[
|
|
2337
|
+
provider,
|
|
2338
|
+
model,
|
|
2339
|
+
hasApiKey: !!(config.apiKeys?.[provider] || config.apiKey),
|
|
2189
2340
|
roots: config.roots || roots,
|
|
2190
2341
|
apiKeys: Object.fromEntries(
|
|
2191
2342
|
Object.entries(config.apiKeys || {}).map(([k]) => [k, true])
|
|
2192
|
-
)
|
|
2343
|
+
),
|
|
2344
|
+
detectedClis: cliStatuses
|
|
2193
2345
|
}
|
|
2194
2346
|
});
|
|
2195
2347
|
break;
|
|
@@ -2222,7 +2374,7 @@ async function handleMessage(ws, msg, state, roots) {
|
|
|
2222
2374
|
sendError(ws, "invalid_payload", "Missing pattern", msg.id);
|
|
2223
2375
|
break;
|
|
2224
2376
|
}
|
|
2225
|
-
const searchRoot = payload.path ?
|
|
2377
|
+
const searchRoot = payload.path ? join4(roots[0] || process.cwd(), payload.path) : roots[0] || process.cwd();
|
|
2226
2378
|
const results = grepFiles(payload.pattern, searchRoot, roots);
|
|
2227
2379
|
send(ws, { id: msg.id, type: "fs.grep.result", payload: { results } });
|
|
2228
2380
|
break;
|
|
@@ -2258,13 +2410,13 @@ function sendError(ws, code, message, id) {
|
|
|
2258
2410
|
}
|
|
2259
2411
|
function serveToolbarBundle(res) {
|
|
2260
2412
|
const bundlePaths = [
|
|
2261
|
-
|
|
2262
|
-
|
|
2413
|
+
join4(__dirname, "toolbar", "index.global.js"),
|
|
2414
|
+
join4(__dirname, "..", "dist", "toolbar", "index.global.js")
|
|
2263
2415
|
];
|
|
2264
2416
|
for (const bundlePath of bundlePaths) {
|
|
2265
2417
|
try {
|
|
2266
|
-
if (
|
|
2267
|
-
const content =
|
|
2418
|
+
if (existsSync4(bundlePath)) {
|
|
2419
|
+
const content = readFileSync4(bundlePath, "utf-8");
|
|
2268
2420
|
res.writeHead(200, {
|
|
2269
2421
|
"Content-Type": "application/javascript",
|
|
2270
2422
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -2407,8 +2559,8 @@ function buildInjectionScript(token) {
|
|
|
2407
2559
|
|
|
2408
2560
|
// src/detect.ts
|
|
2409
2561
|
import { createConnection } from "net";
|
|
2410
|
-
import { readFileSync as
|
|
2411
|
-
import { join as
|
|
2562
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
2563
|
+
import { join as join5, resolve as resolve2 } from "path";
|
|
2412
2564
|
import { execSync } from "child_process";
|
|
2413
2565
|
var COMMON_DEV_PORTS = [
|
|
2414
2566
|
3e3,
|
|
@@ -2565,11 +2717,11 @@ var FRAMEWORK_PATTERNS = [
|
|
|
2565
2717
|
];
|
|
2566
2718
|
var DEV_SCRIPT_NAMES = ["dev", "start", "serve", "develop", "dev:start", "start:dev", "server", "dev:server", "web", "frontend"];
|
|
2567
2719
|
function detectDevScripts(cwd = process.cwd()) {
|
|
2568
|
-
const pkgPath =
|
|
2569
|
-
if (!
|
|
2720
|
+
const pkgPath = join5(cwd, "package.json");
|
|
2721
|
+
if (!existsSync5(pkgPath)) return [];
|
|
2570
2722
|
let pkg;
|
|
2571
2723
|
try {
|
|
2572
|
-
pkg = JSON.parse(
|
|
2724
|
+
pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
2573
2725
|
} catch {
|
|
2574
2726
|
return [];
|
|
2575
2727
|
}
|
|
@@ -2633,10 +2785,10 @@ function checkNodeCompatibility(framework) {
|
|
|
2633
2785
|
function checkEnvPort(cwd = process.cwd()) {
|
|
2634
2786
|
const envFiles = [".env.local", ".env.development.local", ".env.development", ".env"];
|
|
2635
2787
|
for (const envFile of envFiles) {
|
|
2636
|
-
const envPath =
|
|
2637
|
-
if (!
|
|
2788
|
+
const envPath = join5(cwd, envFile);
|
|
2789
|
+
if (!existsSync5(envPath)) continue;
|
|
2638
2790
|
try {
|
|
2639
|
-
const content =
|
|
2791
|
+
const content = readFileSync5(envPath, "utf-8");
|
|
2640
2792
|
const match = content.match(/^PORT\s*=\s*(\d+)/m);
|
|
2641
2793
|
if (match) return parseInt(match[1], 10);
|
|
2642
2794
|
} catch {
|
|
@@ -2653,8 +2805,8 @@ function scanParentLockfiles(projectDir) {
|
|
|
2653
2805
|
let dir = resolve2(project, "..");
|
|
2654
2806
|
while (dir.length > 1 && dir.length >= home.length) {
|
|
2655
2807
|
for (const lockfile of LOCKFILE_NAMES) {
|
|
2656
|
-
const p =
|
|
2657
|
-
if (
|
|
2808
|
+
const p = join5(dir, lockfile);
|
|
2809
|
+
if (existsSync5(p)) found.push(p);
|
|
2658
2810
|
}
|
|
2659
2811
|
const parent = resolve2(dir, "..");
|
|
2660
2812
|
if (parent === dir) break;
|
|
@@ -2663,10 +2815,10 @@ function scanParentLockfiles(projectDir) {
|
|
|
2663
2815
|
return found;
|
|
2664
2816
|
}
|
|
2665
2817
|
function getProjectName(cwd = process.cwd()) {
|
|
2666
|
-
const pkgPath =
|
|
2667
|
-
if (!
|
|
2818
|
+
const pkgPath = join5(cwd, "package.json");
|
|
2819
|
+
if (!existsSync5(pkgPath)) return "this project";
|
|
2668
2820
|
try {
|
|
2669
|
-
const pkg = JSON.parse(
|
|
2821
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
2670
2822
|
return pkg.name || "this project";
|
|
2671
2823
|
} catch {
|
|
2672
2824
|
return "this project";
|
|
@@ -2686,10 +2838,10 @@ var INSTALL_COMMANDS = {
|
|
|
2686
2838
|
bun: "bun install"
|
|
2687
2839
|
};
|
|
2688
2840
|
function checkDependenciesInstalled(cwd = process.cwd()) {
|
|
2689
|
-
const hasNodeModules =
|
|
2841
|
+
const hasNodeModules = existsSync5(join5(cwd, "node_modules"));
|
|
2690
2842
|
let pm = "npm";
|
|
2691
2843
|
for (const { file, pm: detectedPm } of LOCK_FILES) {
|
|
2692
|
-
if (
|
|
2844
|
+
if (existsSync5(join5(cwd, file))) {
|
|
2693
2845
|
pm = detectedPm;
|
|
2694
2846
|
break;
|
|
2695
2847
|
}
|
|
@@ -2730,6 +2882,59 @@ function ask(question) {
|
|
|
2730
2882
|
});
|
|
2731
2883
|
});
|
|
2732
2884
|
}
|
|
2885
|
+
function waitForPortClose(port, timeoutMs = 1e4) {
|
|
2886
|
+
const start = Date.now();
|
|
2887
|
+
return new Promise((resolve4) => {
|
|
2888
|
+
const check = async () => {
|
|
2889
|
+
if (!await isPortOpen(port)) {
|
|
2890
|
+
resolve4(true);
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
if (Date.now() - start > timeoutMs) {
|
|
2894
|
+
resolve4(false);
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
setTimeout(check, 300);
|
|
2898
|
+
};
|
|
2899
|
+
check();
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2902
|
+
function killPortProcess(port) {
|
|
2903
|
+
try {
|
|
2904
|
+
const pidOutput = execSync2(`lsof -i :${port} -sTCP:LISTEN -t 2>/dev/null`, {
|
|
2905
|
+
encoding: "utf-8",
|
|
2906
|
+
timeout: 3e3
|
|
2907
|
+
}).trim();
|
|
2908
|
+
if (!pidOutput) return false;
|
|
2909
|
+
const pids = pidOutput.split("\n").map((p) => p.trim()).filter(Boolean);
|
|
2910
|
+
for (const pid of pids) {
|
|
2911
|
+
try {
|
|
2912
|
+
process.kill(parseInt(pid, 10), "SIGTERM");
|
|
2913
|
+
} catch {
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
return pids.length > 0;
|
|
2917
|
+
} catch {
|
|
2918
|
+
return false;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
function isPortHealthy(host, port) {
|
|
2922
|
+
return new Promise((resolve4) => {
|
|
2923
|
+
const req = http2.get(
|
|
2924
|
+
`http://${host}:${port}/`,
|
|
2925
|
+
{ timeout: 3e3 },
|
|
2926
|
+
(res) => {
|
|
2927
|
+
resolve4(res.statusCode !== void 0 && res.statusCode < 400);
|
|
2928
|
+
res.resume();
|
|
2929
|
+
}
|
|
2930
|
+
);
|
|
2931
|
+
req.on("error", () => resolve4(false));
|
|
2932
|
+
req.on("timeout", () => {
|
|
2933
|
+
req.destroy();
|
|
2934
|
+
resolve4(false);
|
|
2935
|
+
});
|
|
2936
|
+
});
|
|
2937
|
+
}
|
|
2733
2938
|
function waitForPort(port, timeoutMs = 6e4, shouldAbort) {
|
|
2734
2939
|
const start = Date.now();
|
|
2735
2940
|
return new Promise((resolve4) => {
|
|
@@ -2756,7 +2961,7 @@ function waitForPort(port, timeoutMs = 6e4, shouldAbort) {
|
|
|
2756
2961
|
function runCommand(cmd, args, cwd = process.cwd()) {
|
|
2757
2962
|
return new Promise((resolve4) => {
|
|
2758
2963
|
try {
|
|
2759
|
-
const child =
|
|
2964
|
+
const child = spawn5(cmd, args, {
|
|
2760
2965
|
cwd,
|
|
2761
2966
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2762
2967
|
shell: true
|
|
@@ -2894,8 +3099,49 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
|
|
|
2894
3099
|
console.log(chalk.dim(" Scanning for dev server..."));
|
|
2895
3100
|
const detected = await detectDevServer();
|
|
2896
3101
|
if (detected && detected.fromScripts) {
|
|
2897
|
-
|
|
2898
|
-
|
|
3102
|
+
const healthy = await isPortHealthy(detected.host, detected.port);
|
|
3103
|
+
if (healthy) {
|
|
3104
|
+
targetPort = detected.port;
|
|
3105
|
+
targetHost = detected.host;
|
|
3106
|
+
} else {
|
|
3107
|
+
console.log(chalk.yellow(` \u26A0 Dev server on port ${detected.port} is not responding properly.`));
|
|
3108
|
+
console.log(chalk.dim(" Cleaning up orphaned process..."));
|
|
3109
|
+
killPortProcess(detected.port);
|
|
3110
|
+
const freed = await waitForPortClose(detected.port, 5e3);
|
|
3111
|
+
if (!freed) {
|
|
3112
|
+
try {
|
|
3113
|
+
const pidOutput = execSync2(`lsof -i :${detected.port} -sTCP:LISTEN -t 2>/dev/null`, {
|
|
3114
|
+
encoding: "utf-8",
|
|
3115
|
+
timeout: 3e3
|
|
3116
|
+
}).trim();
|
|
3117
|
+
for (const pid of pidOutput.split("\n").filter(Boolean)) {
|
|
3118
|
+
try {
|
|
3119
|
+
process.kill(parseInt(pid, 10), "SIGKILL");
|
|
3120
|
+
} catch {
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
await waitForPortClose(detected.port, 3e3);
|
|
3124
|
+
} catch {
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
const started = await offerToStartDevServer();
|
|
3128
|
+
if (!started) {
|
|
3129
|
+
process.exit(1);
|
|
3130
|
+
}
|
|
3131
|
+
if (lastDetectedPort) {
|
|
3132
|
+
targetPort = lastDetectedPort;
|
|
3133
|
+
} else {
|
|
3134
|
+
const redetected = await detectDevServer();
|
|
3135
|
+
if (redetected) {
|
|
3136
|
+
targetPort = redetected.port;
|
|
3137
|
+
targetHost = redetected.host;
|
|
3138
|
+
} else {
|
|
3139
|
+
console.log(chalk.red(" \u2717 Could not detect the dev server after starting."));
|
|
3140
|
+
console.log(chalk.dim(" Try specifying the port: npx openmagic --port 3000"));
|
|
3141
|
+
process.exit(1);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
2899
3145
|
} else if (detected && !detected.fromScripts) {
|
|
2900
3146
|
const answer = await ask(
|
|
2901
3147
|
chalk.yellow(` Found a server on port ${detected.port}. Is this your project's dev server? `) + chalk.dim("(y/n) ")
|
|
@@ -2987,58 +3233,89 @@ program.name("openmagic").description("AI-powered coding toolbar for any web app
|
|
|
2987
3233
|
}
|
|
2988
3234
|
});
|
|
2989
3235
|
let shuttingDown = false;
|
|
2990
|
-
const shutdown = () => {
|
|
3236
|
+
const shutdown = async () => {
|
|
2991
3237
|
if (shuttingDown) return;
|
|
2992
3238
|
shuttingDown = true;
|
|
2993
3239
|
console.log("");
|
|
2994
3240
|
console.log(chalk.dim(" Shutting down OpenMagic..."));
|
|
2995
3241
|
cleanupBackups();
|
|
2996
3242
|
proxyServer.close();
|
|
2997
|
-
|
|
3243
|
+
const alive = childProcesses.filter((cp) => cp.exitCode === null);
|
|
3244
|
+
for (const cp of alive) {
|
|
2998
3245
|
try {
|
|
2999
3246
|
cp.kill("SIGTERM");
|
|
3000
3247
|
} catch {
|
|
3001
3248
|
}
|
|
3002
3249
|
}
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3250
|
+
if (targetPort) {
|
|
3251
|
+
killPortProcess(targetPort);
|
|
3252
|
+
}
|
|
3253
|
+
if (targetPort && await isPortOpen(targetPort)) {
|
|
3254
|
+
const freed = await waitForPortClose(targetPort, 4e3);
|
|
3255
|
+
if (!freed) {
|
|
3256
|
+
console.log(chalk.dim(" Force-killing remaining processes..."));
|
|
3257
|
+
if (targetPort) {
|
|
3258
|
+
try {
|
|
3259
|
+
const pids = execSync2(`lsof -i :${targetPort} -sTCP:LISTEN -t 2>/dev/null`, {
|
|
3260
|
+
encoding: "utf-8",
|
|
3261
|
+
timeout: 2e3
|
|
3262
|
+
}).trim().split("\n").filter(Boolean);
|
|
3263
|
+
for (const pid of pids) {
|
|
3264
|
+
try {
|
|
3265
|
+
process.kill(parseInt(pid, 10), "SIGKILL");
|
|
3266
|
+
} catch {
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
} catch {
|
|
3270
|
+
}
|
|
3008
3271
|
}
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
process.exit(0);
|
|
3017
|
-
}
|
|
3018
|
-
let remaining = childProcesses.filter((cp) => !cp.killed && cp.exitCode === null).length;
|
|
3019
|
-
if (remaining === 0) {
|
|
3020
|
-
clearTimeout(forceExit);
|
|
3021
|
-
process.exit(0);
|
|
3022
|
-
}
|
|
3023
|
-
for (const cp of childProcesses) {
|
|
3024
|
-
cp.once("exit", () => {
|
|
3025
|
-
remaining--;
|
|
3026
|
-
if (remaining <= 0) {
|
|
3027
|
-
clearTimeout(forceExit);
|
|
3028
|
-
process.exit(0);
|
|
3272
|
+
for (const cp of childProcesses) {
|
|
3273
|
+
if (cp.exitCode === null) {
|
|
3274
|
+
try {
|
|
3275
|
+
cp.kill("SIGKILL");
|
|
3276
|
+
} catch {
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3029
3279
|
}
|
|
3030
|
-
|
|
3280
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
3281
|
+
}
|
|
3282
|
+
} else {
|
|
3283
|
+
if (alive.length > 0) {
|
|
3284
|
+
await Promise.race([
|
|
3285
|
+
Promise.all(alive.map((cp) => new Promise((r) => {
|
|
3286
|
+
if (cp.exitCode !== null) {
|
|
3287
|
+
r();
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
cp.once("exit", () => r());
|
|
3291
|
+
}))),
|
|
3292
|
+
new Promise((r) => setTimeout(() => {
|
|
3293
|
+
for (const cp of childProcesses) {
|
|
3294
|
+
if (cp.exitCode === null) try {
|
|
3295
|
+
cp.kill("SIGKILL");
|
|
3296
|
+
} catch {
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
setTimeout(r, 200);
|
|
3300
|
+
}, 3e3))
|
|
3301
|
+
]);
|
|
3302
|
+
}
|
|
3031
3303
|
}
|
|
3304
|
+
process.exit(0);
|
|
3032
3305
|
};
|
|
3033
|
-
process.on("SIGINT",
|
|
3034
|
-
|
|
3306
|
+
process.on("SIGINT", () => {
|
|
3307
|
+
shutdown();
|
|
3308
|
+
});
|
|
3309
|
+
process.on("SIGTERM", () => {
|
|
3310
|
+
shutdown();
|
|
3311
|
+
});
|
|
3035
3312
|
});
|
|
3036
3313
|
async function offerToStartDevServer(expectedPort) {
|
|
3037
3314
|
const projectName = getProjectName();
|
|
3038
3315
|
const scripts = detectDevScripts();
|
|
3039
3316
|
if (scripts.length === 0) {
|
|
3040
|
-
const htmlPath =
|
|
3041
|
-
if (
|
|
3317
|
+
const htmlPath = join6(process.cwd(), "index.html");
|
|
3318
|
+
if (existsSync6(htmlPath)) {
|
|
3042
3319
|
console.log(
|
|
3043
3320
|
chalk.dim(" No dev scripts found, but index.html detected.")
|
|
3044
3321
|
);
|
|
@@ -3051,7 +3328,7 @@ async function offerToStartDevServer(expectedPort) {
|
|
|
3051
3328
|
}
|
|
3052
3329
|
const staticPort = expectedPort || 8080;
|
|
3053
3330
|
console.log(chalk.dim(` Starting static server on port ${staticPort}...`));
|
|
3054
|
-
const staticChild =
|
|
3331
|
+
const staticChild = spawn5("node", ["-e", `
|
|
3055
3332
|
const http = require("http");
|
|
3056
3333
|
const fs = require("fs");
|
|
3057
3334
|
const path = require("path");
|
|
@@ -3225,7 +3502,7 @@ async function offerToStartDevServer(expectedPort) {
|
|
|
3225
3502
|
}
|
|
3226
3503
|
let child;
|
|
3227
3504
|
try {
|
|
3228
|
-
child =
|
|
3505
|
+
child = spawn5(runCmd, runArgs, {
|
|
3229
3506
|
cwd: process.cwd(),
|
|
3230
3507
|
stdio: "inherit",
|
|
3231
3508
|
env: {
|
|
@@ -3253,25 +3530,16 @@ async function offerToStartDevServer(expectedPort) {
|
|
|
3253
3530
|
\u2717 Dev server exited with code ${code}`));
|
|
3254
3531
|
}
|
|
3255
3532
|
});
|
|
3256
|
-
|
|
3533
|
+
process.once("exit", () => {
|
|
3257
3534
|
for (const cp of childProcesses) {
|
|
3258
|
-
|
|
3259
|
-
cp.kill("SIGTERM");
|
|
3260
|
-
} catch {
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
setTimeout(() => {
|
|
3264
|
-
for (const cp of childProcesses) {
|
|
3535
|
+
if (cp.exitCode === null) {
|
|
3265
3536
|
try {
|
|
3266
3537
|
cp.kill("SIGKILL");
|
|
3267
3538
|
} catch {
|
|
3268
3539
|
}
|
|
3269
3540
|
}
|
|
3270
|
-
}
|
|
3271
|
-
};
|
|
3272
|
-
process.once("exit", cleanup);
|
|
3273
|
-
process.once("SIGINT", cleanup);
|
|
3274
|
-
process.once("SIGTERM", cleanup);
|
|
3541
|
+
}
|
|
3542
|
+
});
|
|
3275
3543
|
console.log(
|
|
3276
3544
|
chalk.dim(` Waiting for dev server on port ${port}...`)
|
|
3277
3545
|
);
|
|
@@ -3302,9 +3570,9 @@ async function offerToStartDevServer(expectedPort) {
|
|
|
3302
3570
|
);
|
|
3303
3571
|
console.log("");
|
|
3304
3572
|
try {
|
|
3305
|
-
const pkgPath =
|
|
3306
|
-
if (
|
|
3307
|
-
const pkg = JSON.parse(
|
|
3573
|
+
const pkgPath = join6(process.cwd(), "package.json");
|
|
3574
|
+
if (existsSync6(pkgPath)) {
|
|
3575
|
+
const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
3308
3576
|
if (pkg.engines?.node) {
|
|
3309
3577
|
console.log(chalk.yellow(` This project requires Node.js ${pkg.engines.node}`));
|
|
3310
3578
|
console.log(chalk.dim(` You are running Node.js ${process.version}`));
|