@gettrace/cli 1.4.23 → 1.4.24
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/index.js +2443 -2036
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,2123 +1,2530 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
MAX_LINES = 500;
|
|
28
|
-
push(chunk) {
|
|
29
|
-
// Strip ANSI escape codes so agents get clean text
|
|
30
|
-
const clean = chunk.replace(/\x1B\[[0-9;]*[mGKHFABCDEJsu]/g, '');
|
|
31
|
-
const newLines = clean.split('\n');
|
|
32
|
-
for (const line of newLines) {
|
|
33
|
-
if (line.trim() === '')
|
|
34
|
-
continue;
|
|
35
|
-
this.lines.push(line);
|
|
36
|
-
if (this.lines.length > this.MAX_LINES) {
|
|
37
|
-
this.lines.shift();
|
|
38
|
-
}
|
|
39
|
-
}
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
7
|
+
import * as fs4 from "fs";
|
|
8
|
+
import * as path4 from "path";
|
|
9
|
+
import { exec as exec4, spawn } from "child_process";
|
|
10
|
+
import { promisify as promisify3 } from "util";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
|
|
13
|
+
// src/file-lock.ts
|
|
14
|
+
var _locks = /* @__PURE__ */ new Map();
|
|
15
|
+
async function withFileLock(filePath, fn) {
|
|
16
|
+
const prior = _locks.get(filePath) ?? Promise.resolve();
|
|
17
|
+
let release;
|
|
18
|
+
const hold = new Promise((resolve3) => release = resolve3);
|
|
19
|
+
_locks.set(filePath, hold);
|
|
20
|
+
try {
|
|
21
|
+
await prior;
|
|
22
|
+
return await fn();
|
|
23
|
+
} finally {
|
|
24
|
+
release();
|
|
25
|
+
if (_locks.get(filePath) === hold) {
|
|
26
|
+
_locks.delete(filePath);
|
|
40
27
|
}
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/format.ts
|
|
32
|
+
import * as fs from "fs";
|
|
33
|
+
import * as path from "path";
|
|
34
|
+
import { exec } from "child_process";
|
|
35
|
+
import { promisify } from "util";
|
|
36
|
+
var execAsync = promisify(exec);
|
|
37
|
+
var JS_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
38
|
+
var CSS_EXTS = /* @__PURE__ */ new Set([".css", ".scss", ".less"]);
|
|
39
|
+
var HTML_EXTS = /* @__PURE__ */ new Set([".html", ".htm", ".vue", ".svelte"]);
|
|
40
|
+
var ALL_FORMATTABLE = /* @__PURE__ */ new Set([
|
|
41
|
+
...JS_EXTS,
|
|
42
|
+
...CSS_EXTS,
|
|
43
|
+
...HTML_EXTS,
|
|
44
|
+
".json",
|
|
45
|
+
".md",
|
|
46
|
+
".yaml",
|
|
47
|
+
".yml"
|
|
48
|
+
]);
|
|
49
|
+
async function autoFormat(filePath, projectPath) {
|
|
50
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
51
|
+
if (!ALL_FORMATTABLE.has(ext))
|
|
52
|
+
return null;
|
|
53
|
+
const prettierBin = path.join(projectPath, "node_modules", ".bin", "prettier");
|
|
54
|
+
if (fs.existsSync(prettierBin)) {
|
|
55
|
+
try {
|
|
56
|
+
await execAsync(`"${prettierBin}" --write "${filePath}"`, { cwd: projectPath });
|
|
57
|
+
return "prettier";
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (JS_EXTS.has(ext)) {
|
|
62
|
+
const eslintBin = path.join(projectPath, "node_modules", ".bin", "eslint");
|
|
63
|
+
if (fs.existsSync(eslintBin)) {
|
|
64
|
+
try {
|
|
65
|
+
await execAsync(`"${eslintBin}" --fix "${filePath}"`, { cwd: projectPath });
|
|
66
|
+
return "eslint";
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/lsp.ts
|
|
75
|
+
import * as fs2 from "fs";
|
|
76
|
+
import * as path2 from "path";
|
|
77
|
+
import { exec as exec2 } from "child_process";
|
|
78
|
+
var TS_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
79
|
+
var JS_EXTS2 = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs"]);
|
|
80
|
+
var CSS_EXTS2 = /* @__PURE__ */ new Set([".css", ".scss", ".less", ".sass"]);
|
|
81
|
+
var CHECKER_TIMEOUT_MS = 6e3;
|
|
82
|
+
var MAX_DIAGNOSTICS = 20;
|
|
83
|
+
async function checkDiagnostics(filePath, projectPath) {
|
|
84
|
+
const SKIP = { diagnostics: [], summary: null, checkers: [], ran: false };
|
|
85
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
86
|
+
const all = [];
|
|
87
|
+
const checkers = [];
|
|
88
|
+
const pending = [];
|
|
89
|
+
if (TS_EXTS.has(ext) || JS_EXTS2.has(ext)) {
|
|
90
|
+
const tsconfig = path2.join(projectPath, "tsconfig.json");
|
|
91
|
+
if (fs2.existsSync(tsconfig)) {
|
|
92
|
+
const shouldRunForJs = JS_EXTS2.has(ext) && tsconfigAllowsJs(tsconfig);
|
|
93
|
+
if (TS_EXTS.has(ext) || shouldRunForJs) {
|
|
94
|
+
checkers.push("tsc");
|
|
95
|
+
pending.push(runTsc(projectPath));
|
|
96
|
+
}
|
|
43
97
|
}
|
|
44
|
-
|
|
45
|
-
|
|
98
|
+
}
|
|
99
|
+
if (TS_EXTS.has(ext) || JS_EXTS2.has(ext)) {
|
|
100
|
+
const eslintBin = path2.join(projectPath, "node_modules", ".bin", "eslint");
|
|
101
|
+
if (fs2.existsSync(eslintBin)) {
|
|
102
|
+
checkers.push("eslint");
|
|
103
|
+
pending.push(runEslint(eslintBin, filePath, projectPath));
|
|
46
104
|
}
|
|
47
|
-
|
|
48
|
-
|
|
105
|
+
}
|
|
106
|
+
if (CSS_EXTS2.has(ext)) {
|
|
107
|
+
const stylelintBin = path2.join(projectPath, "node_modules", ".bin", "stylelint");
|
|
108
|
+
if (fs2.existsSync(stylelintBin)) {
|
|
109
|
+
checkers.push("stylelint");
|
|
110
|
+
pending.push(runStylelint(stylelintBin, filePath, projectPath));
|
|
49
111
|
}
|
|
112
|
+
}
|
|
113
|
+
if (pending.length === 0)
|
|
114
|
+
return SKIP;
|
|
115
|
+
const results = await Promise.allSettled(pending);
|
|
116
|
+
for (const r of results) {
|
|
117
|
+
if (r.status === "fulfilled")
|
|
118
|
+
all.push(...r.value);
|
|
119
|
+
}
|
|
120
|
+
const seen = /* @__PURE__ */ new Set();
|
|
121
|
+
const unique = all.filter((d) => {
|
|
122
|
+
const key = `${d.file}:${d.line}:${d.col}:${d.message}`;
|
|
123
|
+
if (seen.has(key))
|
|
124
|
+
return false;
|
|
125
|
+
seen.add(key);
|
|
126
|
+
return true;
|
|
127
|
+
});
|
|
128
|
+
const rel = path2.relative(projectPath, filePath);
|
|
129
|
+
const sorted = [
|
|
130
|
+
...unique.filter((d) => d.file === rel && d.severity === "error"),
|
|
131
|
+
...unique.filter((d) => d.file !== rel && d.severity === "error"),
|
|
132
|
+
...unique.filter((d) => d.severity === "warning")
|
|
133
|
+
].slice(0, MAX_DIAGNOSTICS);
|
|
134
|
+
const errCount = sorted.filter((d) => d.severity === "error").length;
|
|
135
|
+
const warnCount = sorted.filter((d) => d.severity === "warning").length;
|
|
136
|
+
let summary = null;
|
|
137
|
+
if (errCount > 0) {
|
|
138
|
+
summary = `\u26A0 ${errCount} error${errCount > 1 ? "s" : ""}` + (warnCount > 0 ? ` + ${warnCount} warning${warnCount > 1 ? "s" : ""}` : "") + ` detected after write [${checkers.join("+")}]. Fix these before making more edits.`;
|
|
139
|
+
} else if (warnCount > 0) {
|
|
140
|
+
summary = `\u2139 ${warnCount} warning${warnCount > 1 ? "s" : ""} detected [${checkers.join("+")}]. No errors.`;
|
|
141
|
+
} else if (unique.length === 0 && checkers.length > 0) {
|
|
142
|
+
summary = `\u2713 No errors detected [${checkers.join("+")}].`;
|
|
143
|
+
}
|
|
144
|
+
return { diagnostics: sorted, summary, checkers, ran: true };
|
|
50
145
|
}
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
146
|
+
async function runTsc(projectPath) {
|
|
147
|
+
const localTsc = path2.join(projectPath, "node_modules", ".bin", "tsc");
|
|
148
|
+
const bin = fs2.existsSync(localTsc) ? `"${localTsc}"` : "tsc";
|
|
149
|
+
const cmd = `${bin} --noEmit --pretty false`;
|
|
150
|
+
try {
|
|
151
|
+
const raw = await spawnWithTimeout(cmd, projectPath);
|
|
152
|
+
return parseTscOutput(raw, projectPath);
|
|
153
|
+
} catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function parseTscOutput(output, projectPath) {
|
|
158
|
+
const diagnostics = [];
|
|
159
|
+
const pattern = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/gm;
|
|
160
|
+
let m;
|
|
161
|
+
while ((m = pattern.exec(output)) !== null) {
|
|
162
|
+
const [, rawFile, lineStr, colStr, severity, code, message] = m;
|
|
163
|
+
const absFile = path2.isAbsolute(rawFile) ? rawFile : path2.resolve(projectPath, rawFile);
|
|
164
|
+
diagnostics.push({
|
|
165
|
+
file: path2.relative(projectPath, absFile),
|
|
166
|
+
line: parseInt(lineStr, 10),
|
|
167
|
+
col: parseInt(colStr, 10),
|
|
168
|
+
severity,
|
|
169
|
+
code,
|
|
170
|
+
message: message.trim(),
|
|
171
|
+
source: "tsc"
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return diagnostics;
|
|
175
|
+
}
|
|
176
|
+
async function runEslint(eslintBin, filePath, projectPath) {
|
|
177
|
+
const cmd = `"${eslintBin}" --format json "${filePath}"`;
|
|
178
|
+
try {
|
|
179
|
+
const raw = await spawnWithTimeout(cmd, projectPath);
|
|
180
|
+
return parseEslintOutput(raw, projectPath);
|
|
181
|
+
} catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function parseEslintOutput(raw, projectPath) {
|
|
186
|
+
const diagnostics = [];
|
|
187
|
+
const jsonStart = raw.indexOf("[");
|
|
188
|
+
if (jsonStart === -1)
|
|
189
|
+
return diagnostics;
|
|
190
|
+
try {
|
|
191
|
+
const results = JSON.parse(raw.slice(jsonStart));
|
|
192
|
+
for (const fileResult of results) {
|
|
193
|
+
const relFile = path2.relative(projectPath, fileResult.filePath);
|
|
194
|
+
for (const msg of fileResult.messages) {
|
|
195
|
+
if (msg.severity === 0)
|
|
196
|
+
continue;
|
|
197
|
+
diagnostics.push({
|
|
198
|
+
file: relFile,
|
|
199
|
+
line: msg.line ?? 1,
|
|
200
|
+
col: msg.column ?? 1,
|
|
201
|
+
severity: msg.severity >= 2 ? "error" : "warning",
|
|
202
|
+
code: msg.ruleId ?? "eslint",
|
|
203
|
+
message: msg.message,
|
|
204
|
+
source: "eslint"
|
|
205
|
+
});
|
|
206
|
+
}
|
|
63
207
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
return diagnostics;
|
|
211
|
+
}
|
|
212
|
+
async function runStylelint(stylelintBin, filePath, projectPath) {
|
|
213
|
+
const cmd = `"${stylelintBin}" --formatter json "${filePath}"`;
|
|
214
|
+
try {
|
|
215
|
+
const raw = await spawnWithTimeout(cmd, projectPath);
|
|
216
|
+
return parseStylelintOutput(raw, projectPath);
|
|
217
|
+
} catch {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function parseStylelintOutput(raw, projectPath) {
|
|
222
|
+
const diagnostics = [];
|
|
223
|
+
const jsonStart = raw.indexOf("[");
|
|
224
|
+
if (jsonStart === -1)
|
|
225
|
+
return diagnostics;
|
|
226
|
+
try {
|
|
227
|
+
const results = JSON.parse(raw.slice(jsonStart));
|
|
228
|
+
for (const fileResult of results) {
|
|
229
|
+
const relFile = path2.relative(projectPath, fileResult.source);
|
|
230
|
+
for (const w of fileResult.warnings) {
|
|
231
|
+
diagnostics.push({
|
|
232
|
+
file: relFile,
|
|
233
|
+
line: w.line ?? 1,
|
|
234
|
+
col: w.column ?? 1,
|
|
235
|
+
severity: w.severity === "error" ? "error" : "warning",
|
|
236
|
+
code: w.rule ?? "stylelint",
|
|
237
|
+
message: w.text,
|
|
238
|
+
source: "stylelint"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
81
241
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// 5. Build the final command
|
|
86
|
-
const runCmd = {
|
|
87
|
-
npm: `npm run ${script}`,
|
|
88
|
-
pnpm: `pnpm run ${script}`,
|
|
89
|
-
yarn: `yarn ${script}`,
|
|
90
|
-
bun: `bun run ${script}`,
|
|
91
|
-
};
|
|
92
|
-
return { command: runCmd[pm] ?? `npm run ${script}`, pm, script };
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
return diagnostics;
|
|
93
245
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
246
|
+
function spawnWithTimeout(cmd, cwd) {
|
|
247
|
+
return new Promise((resolve3, reject) => {
|
|
248
|
+
let settled = false;
|
|
249
|
+
const timer = setTimeout(() => {
|
|
250
|
+
if (settled)
|
|
251
|
+
return;
|
|
252
|
+
settled = true;
|
|
253
|
+
proc.kill("SIGTERM");
|
|
254
|
+
reject(new Error(`Checker timed out: ${cmd.slice(0, 60)}`));
|
|
255
|
+
}, CHECKER_TIMEOUT_MS);
|
|
256
|
+
const proc = exec2(cmd, { cwd }, (_err, stdout, stderr) => {
|
|
257
|
+
if (settled)
|
|
258
|
+
return;
|
|
259
|
+
settled = true;
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
resolve3(stdout + stderr);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function tsconfigAllowsJs(tsconfigPath) {
|
|
266
|
+
try {
|
|
267
|
+
const raw = fs2.readFileSync(tsconfigPath, "utf-8");
|
|
268
|
+
return /"allowJs"\s*:\s*true/.test(raw);
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/search.ts
|
|
275
|
+
import * as fs3 from "fs";
|
|
276
|
+
import * as path3 from "path";
|
|
277
|
+
import { exec as exec3 } from "child_process";
|
|
278
|
+
import { promisify as promisify2 } from "util";
|
|
279
|
+
var execAsync2 = promisify2(exec3);
|
|
280
|
+
async function searchWithRipgrep(projectPath, query, opts) {
|
|
281
|
+
const flags = [
|
|
282
|
+
"--json",
|
|
283
|
+
"--line-number",
|
|
284
|
+
opts.isRegex ? "" : "--fixed-strings",
|
|
285
|
+
opts.caseSensitive ? "--case-sensitive" : "--ignore-case",
|
|
286
|
+
`--max-count=${opts.maxResults}`,
|
|
287
|
+
// Exclude directories that are never source code
|
|
288
|
+
"--glob",
|
|
289
|
+
"!node_modules/**",
|
|
290
|
+
"--glob",
|
|
291
|
+
"!.git/**",
|
|
292
|
+
"--glob",
|
|
293
|
+
"!dist/**",
|
|
294
|
+
"--glob",
|
|
295
|
+
"!build/**",
|
|
296
|
+
"--glob",
|
|
297
|
+
"!.next/**",
|
|
298
|
+
"--glob",
|
|
299
|
+
"!coverage/**",
|
|
300
|
+
"--glob",
|
|
301
|
+
"!.cache/**",
|
|
302
|
+
"--glob",
|
|
303
|
+
"!.turbo/**"
|
|
304
|
+
].filter(Boolean);
|
|
305
|
+
const escapedQuery = query.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
306
|
+
const cmd = `rg ${flags.join(" ")} "${escapedQuery}"`;
|
|
307
|
+
const { stdout } = await execAsync2(cmd, {
|
|
308
|
+
cwd: projectPath,
|
|
309
|
+
maxBuffer: 10 * 1024 * 1024
|
|
310
|
+
// 10 MB — generous for large codebases
|
|
311
|
+
});
|
|
312
|
+
const matches = [];
|
|
313
|
+
for (const line of stdout.split("\n")) {
|
|
314
|
+
const trimmed = line.trim();
|
|
315
|
+
if (!trimmed || !trimmed.startsWith("{"))
|
|
316
|
+
continue;
|
|
317
|
+
try {
|
|
318
|
+
const obj = JSON.parse(trimmed);
|
|
319
|
+
if (obj.type !== "match")
|
|
320
|
+
continue;
|
|
321
|
+
matches.push({
|
|
322
|
+
file: path3.relative(projectPath, obj.data.path.text),
|
|
323
|
+
line: obj.data.line_number,
|
|
324
|
+
content: obj.data.lines.text.trimEnd().substring(0, 200)
|
|
325
|
+
});
|
|
326
|
+
if (matches.length >= opts.maxResults)
|
|
327
|
+
break;
|
|
328
|
+
} catch {
|
|
108
329
|
}
|
|
109
|
-
|
|
330
|
+
}
|
|
331
|
+
return matches;
|
|
110
332
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
333
|
+
var FS_SEARCH_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
334
|
+
".js",
|
|
335
|
+
".ts",
|
|
336
|
+
".jsx",
|
|
337
|
+
".tsx",
|
|
338
|
+
".vue",
|
|
339
|
+
".svelte",
|
|
340
|
+
".css",
|
|
341
|
+
".scss",
|
|
342
|
+
".sass",
|
|
343
|
+
".less",
|
|
344
|
+
".html",
|
|
345
|
+
".htm",
|
|
346
|
+
".json",
|
|
347
|
+
".md"
|
|
348
|
+
]);
|
|
349
|
+
var FS_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
350
|
+
"node_modules",
|
|
351
|
+
".git",
|
|
352
|
+
"dist",
|
|
353
|
+
"build",
|
|
354
|
+
".next",
|
|
355
|
+
".nuxt",
|
|
356
|
+
".svelte-kit",
|
|
357
|
+
".cache",
|
|
358
|
+
"coverage",
|
|
359
|
+
".turbo",
|
|
360
|
+
".vercel",
|
|
361
|
+
".output"
|
|
362
|
+
]);
|
|
363
|
+
function buildMatcher(query, opts) {
|
|
364
|
+
if (opts.isRegex) {
|
|
365
|
+
try {
|
|
366
|
+
const re = new RegExp(query, opts.caseSensitive ? "" : "i");
|
|
367
|
+
return (line) => re.test(line);
|
|
368
|
+
} catch {
|
|
141
369
|
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
370
|
+
}
|
|
371
|
+
if (opts.caseSensitive) {
|
|
372
|
+
return (line) => line.includes(query);
|
|
373
|
+
}
|
|
374
|
+
const lowerQuery = query.toLowerCase();
|
|
375
|
+
return (line) => line.toLowerCase().includes(lowerQuery);
|
|
376
|
+
}
|
|
377
|
+
function searchWithFs(projectPath, query, opts) {
|
|
378
|
+
const results = [];
|
|
379
|
+
const matcher = buildMatcher(query, opts);
|
|
380
|
+
function walkDir(dir) {
|
|
381
|
+
if (results.length >= opts.maxResults)
|
|
382
|
+
return;
|
|
383
|
+
let entries;
|
|
384
|
+
try {
|
|
385
|
+
entries = fs3.readdirSync(dir);
|
|
386
|
+
} catch {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
for (const entry of entries) {
|
|
390
|
+
if (results.length >= opts.maxResults)
|
|
148
391
|
return;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
392
|
+
if (FS_IGNORE_DIRS.has(entry) || entry.startsWith("."))
|
|
393
|
+
continue;
|
|
394
|
+
const fullPath = path3.join(dir, entry);
|
|
395
|
+
let stat;
|
|
396
|
+
try {
|
|
397
|
+
stat = fs3.statSync(fullPath);
|
|
398
|
+
} catch {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (stat.isDirectory()) {
|
|
402
|
+
walkDir(fullPath);
|
|
403
|
+
} else if (FS_SEARCH_EXTENSIONS.has(path3.extname(entry).toLowerCase())) {
|
|
404
|
+
let content;
|
|
405
|
+
try {
|
|
406
|
+
content = fs3.readFileSync(fullPath, "utf-8");
|
|
407
|
+
} catch {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const lines = content.split("\n");
|
|
411
|
+
for (let i = 0; i < lines.length && results.length < opts.maxResults; i++) {
|
|
412
|
+
if (matcher(lines[i])) {
|
|
413
|
+
results.push({
|
|
414
|
+
file: path3.relative(projectPath, fullPath),
|
|
415
|
+
line: i + 1,
|
|
416
|
+
content: lines[i].trim().substring(0, 200)
|
|
417
|
+
});
|
|
418
|
+
}
|
|
163
419
|
}
|
|
420
|
+
}
|
|
164
421
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
422
|
+
}
|
|
423
|
+
walkDir(projectPath);
|
|
424
|
+
return results;
|
|
425
|
+
}
|
|
426
|
+
async function searchCode(projectPath, query, opts = {}) {
|
|
427
|
+
const resolved = {
|
|
428
|
+
isRegex: opts.isRegex ?? false,
|
|
429
|
+
caseSensitive: opts.caseSensitive ?? true,
|
|
430
|
+
maxResults: opts.maxResults ?? 20
|
|
431
|
+
};
|
|
432
|
+
try {
|
|
433
|
+
const matches = await searchWithRipgrep(projectPath, query, resolved);
|
|
434
|
+
return { matches, engine: "ripgrep" };
|
|
435
|
+
} catch {
|
|
436
|
+
const matches = searchWithFs(projectPath, query, resolved);
|
|
437
|
+
return { matches, engine: "fs" };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/ast.ts
|
|
442
|
+
import { parse } from "@babel/parser";
|
|
443
|
+
import { createRequire } from "module";
|
|
444
|
+
import * as t from "@babel/types";
|
|
445
|
+
var _require = createRequire(import.meta.url);
|
|
446
|
+
var _traverse = _require("@babel/traverse");
|
|
447
|
+
var _generate = _require("@babel/generator");
|
|
448
|
+
var traverse = _traverse.default ?? _traverse;
|
|
449
|
+
var generate = _generate.default ?? _generate;
|
|
450
|
+
function editJSXClassName(source, edit) {
|
|
451
|
+
let ast;
|
|
452
|
+
try {
|
|
453
|
+
ast = parse(source, {
|
|
454
|
+
sourceType: "module",
|
|
455
|
+
plugins: ["jsx", "typescript"],
|
|
456
|
+
errorRecovery: true
|
|
457
|
+
// tolerate minor syntax errors in source file
|
|
458
|
+
});
|
|
459
|
+
} catch (err) {
|
|
460
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
461
|
+
return { success: false, error: `Parse error: ${msg}` };
|
|
462
|
+
}
|
|
463
|
+
const candidates = [];
|
|
464
|
+
traverse(ast, {
|
|
465
|
+
JSXOpeningElement(nodePath) {
|
|
466
|
+
for (const attr of nodePath.node.attributes) {
|
|
467
|
+
if (!t.isJSXAttribute(attr))
|
|
468
|
+
continue;
|
|
469
|
+
if (!t.isJSXIdentifier(attr.name, { name: "className" }))
|
|
470
|
+
continue;
|
|
471
|
+
const line = attr.loc?.start.line ?? 0;
|
|
472
|
+
let score = 0;
|
|
473
|
+
if (edit.lineHint && line > 0) {
|
|
474
|
+
const dist = Math.abs(line - edit.lineHint);
|
|
475
|
+
if (dist <= 1)
|
|
476
|
+
score += 100;
|
|
477
|
+
else if (dist <= 5)
|
|
478
|
+
score += 50;
|
|
479
|
+
else if (dist <= 15)
|
|
480
|
+
score += 15;
|
|
184
481
|
}
|
|
185
|
-
|
|
186
|
-
|
|
482
|
+
const currentValue = _extractClassNameString(attr.value);
|
|
483
|
+
if (edit.oldValue) {
|
|
484
|
+
if (currentValue === edit.oldValue)
|
|
485
|
+
score += 80;
|
|
486
|
+
else if (currentValue?.includes(edit.oldValue))
|
|
487
|
+
score += 40;
|
|
488
|
+
else if (currentValue === null && edit.lineHint)
|
|
489
|
+
score += 20;
|
|
490
|
+
} else {
|
|
491
|
+
score += 20;
|
|
187
492
|
}
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
bestMatch = cand;
|
|
493
|
+
if (score > 0) {
|
|
494
|
+
candidates.push({ nodePath, attrNode: attr, score, line });
|
|
191
495
|
}
|
|
496
|
+
}
|
|
192
497
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
498
|
+
});
|
|
499
|
+
if (candidates.length === 0) {
|
|
500
|
+
return { success: false, error: "No className attribute found matching the given hints." };
|
|
501
|
+
}
|
|
502
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
503
|
+
const best = candidates[0];
|
|
504
|
+
let strategy;
|
|
505
|
+
if (best.score >= 180)
|
|
506
|
+
strategy = "line_exact";
|
|
507
|
+
else if (best.score >= 100)
|
|
508
|
+
strategy = "line_proximity";
|
|
509
|
+
else if (best.score >= 80)
|
|
510
|
+
strategy = "value_exact";
|
|
511
|
+
else
|
|
512
|
+
strategy = "value_partial";
|
|
513
|
+
best.attrNode.value = t.stringLiteral(edit.newValue);
|
|
514
|
+
const output = generate(
|
|
515
|
+
ast,
|
|
516
|
+
{ retainLines: true, jsescOption: { minimal: true } },
|
|
517
|
+
source
|
|
518
|
+
);
|
|
519
|
+
return {
|
|
520
|
+
success: true,
|
|
521
|
+
code: output.code,
|
|
522
|
+
strategy,
|
|
523
|
+
matchedLine: best.line
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function _extractClassNameString(value) {
|
|
527
|
+
if (!value)
|
|
528
|
+
return "";
|
|
529
|
+
if (t.isStringLiteral(value))
|
|
530
|
+
return value.value;
|
|
531
|
+
if (t.isJSXExpressionContainer(value)) {
|
|
532
|
+
const expr = value.expression;
|
|
533
|
+
if (t.isStringLiteral(expr))
|
|
534
|
+
return expr.value;
|
|
535
|
+
if (t.isTemplateLiteral(expr) && expr.expressions.length === 0) {
|
|
536
|
+
return expr.quasis[0]?.value.cooked ?? null;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/index.ts
|
|
543
|
+
import { createRequire as _createRequire } from "module";
|
|
544
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
545
|
+
var __dirname = path4.dirname(__filename);
|
|
546
|
+
var execAsync3 = promisify3(exec4);
|
|
547
|
+
var TerminalBuffer = class {
|
|
548
|
+
lines = [];
|
|
549
|
+
MAX_LINES = 500;
|
|
550
|
+
push(chunk) {
|
|
551
|
+
const clean = chunk.replace(/\x1B\[[0-9;]*[mGKHFABCDEJsu]/g, "");
|
|
552
|
+
const newLines = clean.split("\n");
|
|
553
|
+
for (const line of newLines) {
|
|
554
|
+
if (line.trim() === "")
|
|
555
|
+
continue;
|
|
556
|
+
this.lines.push(line);
|
|
557
|
+
if (this.lines.length > this.MAX_LINES) {
|
|
558
|
+
this.lines.shift();
|
|
559
|
+
}
|
|
204
560
|
}
|
|
561
|
+
}
|
|
562
|
+
getLast(n = 100) {
|
|
563
|
+
return this.lines.slice(-n);
|
|
564
|
+
}
|
|
565
|
+
clear() {
|
|
566
|
+
this.lines = [];
|
|
567
|
+
}
|
|
568
|
+
get length() {
|
|
569
|
+
return this.lines.length;
|
|
570
|
+
}
|
|
205
571
|
};
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
572
|
+
var globalTerminalBuffer = new TerminalBuffer();
|
|
573
|
+
var devProcess = null;
|
|
574
|
+
function detectDevCommand(projectPath, override) {
|
|
575
|
+
if (override) {
|
|
576
|
+
return { command: override, pm: "custom", script: override };
|
|
577
|
+
}
|
|
578
|
+
let pm = "npm";
|
|
579
|
+
if (fs4.existsSync(path4.join(projectPath, "bun.lockb")))
|
|
580
|
+
pm = "bun";
|
|
581
|
+
else if (fs4.existsSync(path4.join(projectPath, "pnpm-lock.yaml")))
|
|
582
|
+
pm = "pnpm";
|
|
583
|
+
else if (fs4.existsSync(path4.join(projectPath, "yarn.lock")))
|
|
584
|
+
pm = "yarn";
|
|
585
|
+
const pkgPath = path4.join(projectPath, "package.json");
|
|
586
|
+
let scripts = {};
|
|
587
|
+
if (fs4.existsSync(pkgPath)) {
|
|
588
|
+
try {
|
|
589
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
590
|
+
scripts = pkg.scripts || {};
|
|
591
|
+
} catch (e) {
|
|
215
592
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
593
|
+
}
|
|
594
|
+
const candidates = ["dev", "start", "serve", "preview"];
|
|
595
|
+
const script = candidates.find((c) => !!scripts[c]) || "dev";
|
|
596
|
+
const runCmd = {
|
|
597
|
+
npm: `npm run ${script}`,
|
|
598
|
+
pnpm: `pnpm run ${script}`,
|
|
599
|
+
yarn: `yarn ${script}`,
|
|
600
|
+
bun: `bun run ${script}`
|
|
601
|
+
};
|
|
602
|
+
return { command: runCmd[pm] ?? `npm run ${script}`, pm, script };
|
|
603
|
+
}
|
|
604
|
+
var _pkgRequire = _createRequire(import.meta.url);
|
|
605
|
+
var VERSION = _pkgRequire("../package.json").version;
|
|
606
|
+
function levenshtein(a, b) {
|
|
607
|
+
if (a === "" || b === "")
|
|
608
|
+
return Math.max(a.length, b.length);
|
|
609
|
+
const matrix = Array.from(
|
|
610
|
+
{ length: a.length + 1 },
|
|
611
|
+
(_, i) => Array.from({ length: b.length + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
|
|
612
|
+
);
|
|
613
|
+
for (let i = 1; i <= a.length; i++) {
|
|
614
|
+
for (let j = 1; j <= b.length; j++) {
|
|
615
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
616
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
224
617
|
}
|
|
618
|
+
}
|
|
619
|
+
return matrix[a.length][b.length];
|
|
620
|
+
}
|
|
621
|
+
var SimpleReplacer = function* (_content, find) {
|
|
622
|
+
yield find;
|
|
225
623
|
};
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const findLines = find.split('\n');
|
|
239
|
-
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
|
|
240
|
-
const block = contentLines.slice(i, i + findLines.length).join('\n');
|
|
241
|
-
if (removeIndent(block) === normalizedFind) {
|
|
242
|
-
yield block;
|
|
243
|
-
}
|
|
624
|
+
var LineTrimmedReplacer = function* (content, find) {
|
|
625
|
+
const originalLines = content.split("\n");
|
|
626
|
+
const searchLines = find.split("\n");
|
|
627
|
+
if (searchLines[searchLines.length - 1] === "")
|
|
628
|
+
searchLines.pop();
|
|
629
|
+
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
|
630
|
+
let matches = true;
|
|
631
|
+
for (let j = 0; j < searchLines.length; j++) {
|
|
632
|
+
if (originalLines[i + j].trim() !== searchLines[j].trim()) {
|
|
633
|
+
matches = false;
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
244
636
|
}
|
|
637
|
+
if (matches) {
|
|
638
|
+
let start = 0;
|
|
639
|
+
for (let k = 0; k < i; k++)
|
|
640
|
+
start += originalLines[k].length + 1;
|
|
641
|
+
let end = start;
|
|
642
|
+
for (let k = 0; k < searchLines.length; k++) {
|
|
643
|
+
end += originalLines[i + k].length;
|
|
644
|
+
if (k < searchLines.length - 1)
|
|
645
|
+
end += 1;
|
|
646
|
+
}
|
|
647
|
+
yield content.substring(start, end);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
245
650
|
};
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
651
|
+
var BlockAnchorReplacer = function* (content, find) {
|
|
652
|
+
const originalLines = content.split("\n");
|
|
653
|
+
const searchLines = find.split("\n");
|
|
654
|
+
if (searchLines.length < 3)
|
|
655
|
+
return;
|
|
656
|
+
if (searchLines[searchLines.length - 1] === "")
|
|
657
|
+
searchLines.pop();
|
|
658
|
+
const firstSearch = searchLines[0].trim();
|
|
659
|
+
const lastSearch = searchLines[searchLines.length - 1].trim();
|
|
660
|
+
const searchBlockSize = searchLines.length;
|
|
661
|
+
const candidates = [];
|
|
662
|
+
for (let i = 0; i < originalLines.length; i++) {
|
|
663
|
+
if (originalLines[i].trim() !== firstSearch)
|
|
664
|
+
continue;
|
|
665
|
+
for (let j = i + 2; j < originalLines.length; j++) {
|
|
666
|
+
if (originalLines[j].trim() === lastSearch) {
|
|
667
|
+
candidates.push({ startLine: i, endLine: j });
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (candidates.length === 0)
|
|
673
|
+
return;
|
|
674
|
+
let bestMatch = null;
|
|
675
|
+
let maxSimilarity = -1;
|
|
676
|
+
const threshold = candidates.length === 1 ? 0 : 0.3;
|
|
677
|
+
for (const cand of candidates) {
|
|
678
|
+
const actualSize = cand.endLine - cand.startLine + 1;
|
|
679
|
+
let similarity = 0;
|
|
680
|
+
const linesToCheck = Math.min(searchBlockSize - 2, actualSize - 2);
|
|
681
|
+
if (linesToCheck > 0) {
|
|
682
|
+
for (let j = 1; j < searchBlockSize - 1 && j < actualSize - 1; j++) {
|
|
683
|
+
const orig = originalLines[cand.startLine + j].trim();
|
|
684
|
+
const srch = searchLines[j].trim();
|
|
685
|
+
const maxLen = Math.max(orig.length, srch.length);
|
|
686
|
+
if (maxLen === 0)
|
|
687
|
+
continue;
|
|
688
|
+
similarity += (1 - levenshtein(orig, srch) / maxLen) / linesToCheck;
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
similarity = 1;
|
|
692
|
+
}
|
|
693
|
+
if (similarity > maxSimilarity) {
|
|
694
|
+
maxSimilarity = similarity;
|
|
695
|
+
bestMatch = cand;
|
|
271
696
|
}
|
|
697
|
+
}
|
|
698
|
+
if (maxSimilarity >= threshold && bestMatch) {
|
|
699
|
+
let start = 0;
|
|
700
|
+
for (let k = 0; k < bestMatch.startLine; k++)
|
|
701
|
+
start += originalLines[k].length + 1;
|
|
702
|
+
let end = start;
|
|
703
|
+
for (let k = bestMatch.startLine; k <= bestMatch.endLine; k++) {
|
|
704
|
+
end += originalLines[k].length;
|
|
705
|
+
if (k < bestMatch.endLine)
|
|
706
|
+
end += 1;
|
|
707
|
+
}
|
|
708
|
+
yield content.substring(start, end);
|
|
709
|
+
}
|
|
272
710
|
};
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
711
|
+
var WhitespaceNormalizedReplacer = function* (content, find) {
|
|
712
|
+
const normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
713
|
+
const normalizedFind = normalize(find);
|
|
714
|
+
const lines = content.split("\n");
|
|
715
|
+
for (const line of lines) {
|
|
716
|
+
if (normalize(line) === normalizedFind) {
|
|
717
|
+
yield line;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const findLines = find.split("\n");
|
|
721
|
+
if (findLines.length > 1) {
|
|
282
722
|
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
723
|
+
const block = lines.slice(i, i + findLines.length);
|
|
724
|
+
if (normalize(block.join("\n")) === normalizedFind) {
|
|
725
|
+
yield block.join("\n");
|
|
726
|
+
}
|
|
286
727
|
}
|
|
728
|
+
}
|
|
287
729
|
};
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const bl = blockLines[k].trim(), fl = findLines[k].trim();
|
|
308
|
-
if (bl.length > 0 || fl.length > 0) {
|
|
309
|
-
total++;
|
|
310
|
-
if (bl === fl)
|
|
311
|
-
matching++;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (total === 0 || matching / total >= 0.5) {
|
|
315
|
-
yield blockLines.join('\n');
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
730
|
+
var IndentationFlexibleReplacer = function* (content, find) {
|
|
731
|
+
const removeIndent = (text) => {
|
|
732
|
+
const lines = text.split("\n");
|
|
733
|
+
const nonEmpty = lines.filter((l) => l.trim().length > 0);
|
|
734
|
+
if (nonEmpty.length === 0)
|
|
735
|
+
return text;
|
|
736
|
+
const minIndent = Math.min(...nonEmpty.map((l) => {
|
|
737
|
+
const m = l.match(/^(\s*)/);
|
|
738
|
+
return m ? m[1].length : 0;
|
|
739
|
+
}));
|
|
740
|
+
return lines.map((l) => l.trim().length === 0 ? l : l.slice(minIndent)).join("\n");
|
|
741
|
+
};
|
|
742
|
+
const normalizedFind = removeIndent(find);
|
|
743
|
+
const contentLines = content.split("\n");
|
|
744
|
+
const findLines = find.split("\n");
|
|
745
|
+
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
|
|
746
|
+
const block = contentLines.slice(i, i + findLines.length).join("\n");
|
|
747
|
+
if (removeIndent(block) === normalizedFind) {
|
|
748
|
+
yield block;
|
|
322
749
|
}
|
|
750
|
+
}
|
|
323
751
|
};
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
752
|
+
var EscapeNormalizedReplacer = function* (content, find) {
|
|
753
|
+
const unescape = (str) => str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, c) => {
|
|
754
|
+
switch (c) {
|
|
755
|
+
case "n":
|
|
756
|
+
return "\n";
|
|
757
|
+
case "t":
|
|
758
|
+
return " ";
|
|
759
|
+
case "r":
|
|
760
|
+
return "\r";
|
|
761
|
+
case "'":
|
|
762
|
+
return "'";
|
|
763
|
+
case '"':
|
|
764
|
+
return '"';
|
|
765
|
+
case "`":
|
|
766
|
+
return "`";
|
|
767
|
+
case "\\":
|
|
768
|
+
return "\\";
|
|
769
|
+
case "\n":
|
|
770
|
+
return "\n";
|
|
771
|
+
case "$":
|
|
772
|
+
return "$";
|
|
773
|
+
default:
|
|
774
|
+
return match;
|
|
333
775
|
}
|
|
776
|
+
});
|
|
777
|
+
const unescapedFind = unescape(find);
|
|
778
|
+
if (content.includes(unescapedFind))
|
|
779
|
+
yield unescapedFind;
|
|
780
|
+
const lines = content.split("\n");
|
|
781
|
+
const findLines = unescapedFind.split("\n");
|
|
782
|
+
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
783
|
+
const block = lines.slice(i, i + findLines.length).join("\n");
|
|
784
|
+
if (unescape(block) === unescapedFind)
|
|
785
|
+
yield block;
|
|
786
|
+
}
|
|
334
787
|
};
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (foundEnd >= 0) {
|
|
373
|
-
// Verify the non-empty lines match
|
|
374
|
-
const candidateLines = contentLines.slice(i, foundEnd + 1);
|
|
375
|
-
const candidateNonEmpty = candidateLines.filter(l => l.trim().length > 0);
|
|
376
|
-
let allMatch = candidateNonEmpty.length === findLines.length;
|
|
377
|
-
if (allMatch) {
|
|
378
|
-
for (let m = 0; m < findLines.length; m++) {
|
|
379
|
-
if (candidateNonEmpty[m].trim() !== findLines[m].trim()) {
|
|
380
|
-
allMatch = false;
|
|
381
|
-
break;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
if (allMatch) {
|
|
386
|
-
yield candidateLines.join('\n');
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
788
|
+
var TrimmedBoundaryReplacer = function* (content, find) {
|
|
789
|
+
const trimmed = find.trim();
|
|
790
|
+
if (trimmed === find)
|
|
791
|
+
return;
|
|
792
|
+
if (content.includes(trimmed))
|
|
793
|
+
yield trimmed;
|
|
794
|
+
const lines = content.split("\n");
|
|
795
|
+
const findLines = find.split("\n");
|
|
796
|
+
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
797
|
+
const block = lines.slice(i, i + findLines.length).join("\n");
|
|
798
|
+
if (block.trim() === trimmed)
|
|
799
|
+
yield block;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
var ContextAwareReplacer = function* (content, find) {
|
|
803
|
+
const findLines = find.split("\n");
|
|
804
|
+
if (findLines.length < 3)
|
|
805
|
+
return;
|
|
806
|
+
if (findLines[findLines.length - 1] === "")
|
|
807
|
+
findLines.pop();
|
|
808
|
+
const contentLines = content.split("\n");
|
|
809
|
+
const firstLine = findLines[0].trim();
|
|
810
|
+
const lastLine = findLines[findLines.length - 1].trim();
|
|
811
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
812
|
+
if (contentLines[i].trim() !== firstLine)
|
|
813
|
+
continue;
|
|
814
|
+
for (let j = i + 2; j < contentLines.length; j++) {
|
|
815
|
+
if (contentLines[j].trim() === lastLine) {
|
|
816
|
+
const blockLines = contentLines.slice(i, j + 1);
|
|
817
|
+
if (blockLines.length === findLines.length) {
|
|
818
|
+
let matching = 0, total = 0;
|
|
819
|
+
for (let k = 1; k < blockLines.length - 1; k++) {
|
|
820
|
+
const bl = blockLines[k].trim(), fl = findLines[k].trim();
|
|
821
|
+
if (bl.length > 0 || fl.length > 0) {
|
|
822
|
+
total++;
|
|
823
|
+
if (bl === fl)
|
|
824
|
+
matching++;
|
|
389
825
|
}
|
|
826
|
+
}
|
|
827
|
+
if (total === 0 || matching / total >= 0.5) {
|
|
828
|
+
yield blockLines.join("\n");
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
390
831
|
}
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
391
834
|
}
|
|
835
|
+
}
|
|
392
836
|
};
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
837
|
+
var MultiOccurrenceReplacer = function* (content, find) {
|
|
838
|
+
let start = 0;
|
|
839
|
+
while (true) {
|
|
840
|
+
const idx = content.indexOf(find, start);
|
|
841
|
+
if (idx === -1)
|
|
842
|
+
break;
|
|
843
|
+
yield find;
|
|
844
|
+
start = idx + find.length;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
var BlankLineTolerantReplacer = function* (content, find) {
|
|
848
|
+
const collapseBlankLines = (text) => text.replace(/\n\s*\n/g, "\n").trim();
|
|
849
|
+
const collapsedFind = collapseBlankLines(find);
|
|
850
|
+
if (!collapsedFind)
|
|
851
|
+
return;
|
|
852
|
+
const collapsedContent = collapseBlankLines(content);
|
|
853
|
+
if (collapsedContent.includes(collapsedFind)) {
|
|
854
|
+
const contentLines = content.split("\n");
|
|
855
|
+
const findLines = find.split("\n").filter((l) => l.trim().length > 0);
|
|
856
|
+
if (findLines.length === 0)
|
|
857
|
+
return;
|
|
858
|
+
const firstNonEmpty = findLines[0].trim();
|
|
859
|
+
const lastNonEmpty = findLines[findLines.length - 1].trim();
|
|
860
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
861
|
+
if (contentLines[i].trim() !== firstNonEmpty)
|
|
862
|
+
continue;
|
|
863
|
+
let foundEnd = -1;
|
|
864
|
+
let nonEmptyCount = 0;
|
|
865
|
+
for (let j = i; j < contentLines.length; j++) {
|
|
866
|
+
if (contentLines[j].trim().length > 0) {
|
|
867
|
+
nonEmptyCount++;
|
|
868
|
+
}
|
|
869
|
+
if (contentLines[j].trim() === lastNonEmpty && nonEmptyCount >= findLines.length) {
|
|
870
|
+
foundEnd = j;
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
if (j - i > findLines.length * 2 + 5)
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
if (foundEnd >= 0) {
|
|
877
|
+
const candidateLines = contentLines.slice(i, foundEnd + 1);
|
|
878
|
+
const candidateNonEmpty = candidateLines.filter((l) => l.trim().length > 0);
|
|
879
|
+
let allMatch = candidateNonEmpty.length === findLines.length;
|
|
880
|
+
if (allMatch) {
|
|
881
|
+
for (let m = 0; m < findLines.length; m++) {
|
|
882
|
+
if (candidateNonEmpty[m].trim() !== findLines[m].trim()) {
|
|
883
|
+
allMatch = false;
|
|
884
|
+
break;
|
|
419
885
|
}
|
|
420
|
-
|
|
421
|
-
if (index !== lastIndex)
|
|
422
|
-
continue; // Multiple matches, try next replacer
|
|
423
|
-
return {
|
|
424
|
-
result: content.substring(0, index) + newString + content.substring(index + search.length),
|
|
425
|
-
strategy: name,
|
|
426
|
-
};
|
|
886
|
+
}
|
|
427
887
|
}
|
|
888
|
+
if (allMatch) {
|
|
889
|
+
yield candidateLines.join("\n");
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
428
893
|
}
|
|
429
|
-
|
|
430
|
-
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
function fuzzyReplace(content, oldString, newString, replaceAll = false) {
|
|
897
|
+
if (oldString === newString) {
|
|
898
|
+
return { error: "No changes: oldString and newString are identical." };
|
|
899
|
+
}
|
|
900
|
+
const replacers = [
|
|
901
|
+
["exact", SimpleReplacer],
|
|
902
|
+
["line-trimmed", LineTrimmedReplacer],
|
|
903
|
+
["block-anchor", BlockAnchorReplacer],
|
|
904
|
+
["whitespace-normalized", WhitespaceNormalizedReplacer],
|
|
905
|
+
["indentation-flexible", IndentationFlexibleReplacer],
|
|
906
|
+
["escape-normalized", EscapeNormalizedReplacer],
|
|
907
|
+
["trimmed-boundary", TrimmedBoundaryReplacer],
|
|
908
|
+
["context-aware", ContextAwareReplacer],
|
|
909
|
+
["multi-occurrence", MultiOccurrenceReplacer],
|
|
910
|
+
["blank-line-tolerant", BlankLineTolerantReplacer]
|
|
911
|
+
];
|
|
912
|
+
let notFound = true;
|
|
913
|
+
for (const [name, replacer] of replacers) {
|
|
914
|
+
for (const search of replacer(content, oldString)) {
|
|
915
|
+
const index = content.indexOf(search);
|
|
916
|
+
if (index === -1)
|
|
917
|
+
continue;
|
|
918
|
+
notFound = false;
|
|
919
|
+
if (replaceAll) {
|
|
920
|
+
return { result: content.replaceAll(search, newString), strategy: name };
|
|
921
|
+
}
|
|
922
|
+
const lastIndex = content.lastIndexOf(search);
|
|
923
|
+
if (index !== lastIndex)
|
|
924
|
+
continue;
|
|
925
|
+
return {
|
|
926
|
+
result: content.substring(0, index) + newString + content.substring(index + search.length),
|
|
927
|
+
strategy: name
|
|
928
|
+
};
|
|
431
929
|
}
|
|
432
|
-
|
|
930
|
+
}
|
|
931
|
+
if (notFound) {
|
|
932
|
+
return { error: "Could not find oldString in the file. Tried 10 matching strategies including fuzzy whitespace, indentation, blank-line-tolerant, and block-anchor matching." };
|
|
933
|
+
}
|
|
934
|
+
return { error: "Found multiple matches for oldString. Provide more surrounding context to make the match unique." };
|
|
433
935
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
936
|
+
program.name("trace").description("Trace IDE Bridge \u2014 connect your codebase and dev server to the Trace extension").version(VERSION);
|
|
937
|
+
program.command("dev").description("Start dev server + IDE bridge together (recommended)").argument("[command]", 'Override the dev command (e.g. "npm run start:staging")').option("-p, --port <port>", "WebSocket port for IDE bridge", "8765").action(async (commandOverride, options) => {
|
|
938
|
+
const port = parseInt(options.port);
|
|
939
|
+
const projectPath = process.cwd();
|
|
940
|
+
const { command, pm, script } = detectDevCommand(projectPath, commandOverride);
|
|
941
|
+
console.log();
|
|
942
|
+
console.log(chalk.bold.cyan("\u26A1 Trace Dev"));
|
|
943
|
+
console.log(chalk.gray("\u2500".repeat(55)));
|
|
944
|
+
console.log();
|
|
945
|
+
console.log(`\u{1F4C1} Project: ${chalk.green(projectPath)}`);
|
|
946
|
+
console.log(`\u{1F4E6} Package Mgr: ${chalk.yellow(pm)}`);
|
|
947
|
+
console.log(`\u{1F680} Dev Command: ${chalk.cyan(command)}`);
|
|
948
|
+
console.log(`\u{1F310} Bridge Port: ${chalk.cyan(port)}`);
|
|
949
|
+
console.log();
|
|
950
|
+
console.log(chalk.gray("\u2500".repeat(55)));
|
|
951
|
+
console.log();
|
|
952
|
+
const connectedClients = /* @__PURE__ */ new Set();
|
|
953
|
+
const broadcast = (payload) => {
|
|
954
|
+
const msg = JSON.stringify(payload);
|
|
955
|
+
for (const client of connectedClients) {
|
|
956
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
957
|
+
client.send(msg);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
console.log(chalk.dim("Starting dev server..."));
|
|
962
|
+
console.log();
|
|
963
|
+
const childEnv = { ...process.env, FORCE_COLOR: "1" };
|
|
964
|
+
delete childEnv.MallocNanoZone;
|
|
965
|
+
delete childEnv.MallocStackLogging;
|
|
966
|
+
delete childEnv.MallocScribble;
|
|
967
|
+
delete childEnv.MallocGuardEdges;
|
|
968
|
+
delete childEnv.MallocErrorAbort;
|
|
969
|
+
devProcess = spawn(command, [], {
|
|
970
|
+
cwd: projectPath,
|
|
971
|
+
shell: true,
|
|
972
|
+
env: childEnv
|
|
973
|
+
});
|
|
974
|
+
devProcess.stdout?.on("data", (chunk) => {
|
|
975
|
+
const text = chunk.toString();
|
|
976
|
+
process.stdout.write(text);
|
|
977
|
+
globalTerminalBuffer.push(text);
|
|
978
|
+
broadcast({ type: "STREAM_CHUNK", stream: "stdout", chunk: text });
|
|
979
|
+
});
|
|
980
|
+
devProcess.stderr?.on("data", (chunk) => {
|
|
981
|
+
const text = chunk.toString();
|
|
982
|
+
process.stderr.write(text);
|
|
983
|
+
globalTerminalBuffer.push(text);
|
|
984
|
+
broadcast({ type: "STREAM_CHUNK", stream: "stderr", chunk: text });
|
|
985
|
+
});
|
|
986
|
+
devProcess.on("close", (code) => {
|
|
987
|
+
const msg = `
|
|
988
|
+
[Trace] Dev server exited with code ${code}
|
|
989
|
+
`;
|
|
990
|
+
process.stdout.write(chalk.yellow(msg));
|
|
991
|
+
globalTerminalBuffer.push(msg);
|
|
992
|
+
broadcast({ type: "STREAM_END", exitCode: code });
|
|
993
|
+
devProcess = null;
|
|
994
|
+
});
|
|
995
|
+
devProcess.on("error", (err) => {
|
|
996
|
+
const msg = `[Trace] Failed to start dev server: ${err.message}
|
|
997
|
+
`;
|
|
998
|
+
process.stderr.write(chalk.red(msg));
|
|
999
|
+
console.error(chalk.red("\n\u2717 Could not start dev server."));
|
|
1000
|
+
console.error(chalk.dim(` Command: ${command}`));
|
|
1001
|
+
console.error(chalk.dim(` Make sure the script exists in your package.json`));
|
|
1002
|
+
});
|
|
1003
|
+
const wss = new WebSocketServer({ port });
|
|
1004
|
+
let clientCount = 0;
|
|
1005
|
+
wss.on("listening", () => {
|
|
466
1006
|
console.log();
|
|
467
|
-
console.log(chalk.gray(
|
|
1007
|
+
console.log(chalk.gray("\u2500".repeat(55)));
|
|
1008
|
+
console.log(chalk.green("\u2713") + " IDE Bridge listening on port " + chalk.cyan(port));
|
|
1009
|
+
console.log(chalk.dim("Waiting for Trace extension to connect..."));
|
|
1010
|
+
console.log(chalk.dim("Press Ctrl+C to stop both"));
|
|
1011
|
+
console.log(chalk.gray("\u2500".repeat(55)));
|
|
468
1012
|
console.log();
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
cwd: projectPath,
|
|
484
|
-
shell: true,
|
|
485
|
-
env: { ...process.env, FORCE_COLOR: '1' },
|
|
486
|
-
});
|
|
487
|
-
devProcess.stdout?.on('data', (chunk) => {
|
|
488
|
-
const text = chunk.toString();
|
|
489
|
-
// Print to terminal so the developer still sees their logs
|
|
490
|
-
process.stdout.write(text);
|
|
491
|
-
// Store in rolling buffer (ANSI stripped)
|
|
492
|
-
globalTerminalBuffer.push(text);
|
|
493
|
-
// Push raw chunk to extension for live terminal panel
|
|
494
|
-
broadcast({ type: 'STREAM_CHUNK', stream: 'stdout', chunk: text });
|
|
495
|
-
});
|
|
496
|
-
devProcess.stderr?.on('data', (chunk) => {
|
|
497
|
-
const text = chunk.toString();
|
|
498
|
-
process.stderr.write(text);
|
|
499
|
-
globalTerminalBuffer.push(text);
|
|
500
|
-
broadcast({ type: 'STREAM_CHUNK', stream: 'stderr', chunk: text });
|
|
501
|
-
});
|
|
502
|
-
devProcess.on('close', (code) => {
|
|
503
|
-
const msg = `\n[Trace] Dev server exited with code ${code}\n`;
|
|
504
|
-
process.stdout.write(chalk.yellow(msg));
|
|
505
|
-
globalTerminalBuffer.push(msg);
|
|
506
|
-
broadcast({ type: 'STREAM_END', exitCode: code });
|
|
507
|
-
devProcess = null;
|
|
508
|
-
});
|
|
509
|
-
devProcess.on('error', (err) => {
|
|
510
|
-
const msg = `[Trace] Failed to start dev server: ${err.message}\n`;
|
|
511
|
-
process.stderr.write(chalk.red(msg));
|
|
512
|
-
console.error(chalk.red('\n✗ Could not start dev server.'));
|
|
513
|
-
console.error(chalk.dim(` Command: ${command}`));
|
|
514
|
-
console.error(chalk.dim(` Make sure the script exists in your package.json`));
|
|
1013
|
+
});
|
|
1014
|
+
wss.on("connection", (ws) => {
|
|
1015
|
+
clientCount++;
|
|
1016
|
+
connectedClients.add(ws);
|
|
1017
|
+
console.log(chalk.green("\u25CF") + ` Extension connected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
1018
|
+
attachMessageHandler(ws, projectPath);
|
|
1019
|
+
const catchup = globalTerminalBuffer.getLast(100);
|
|
1020
|
+
if (catchup.length > 0) {
|
|
1021
|
+
ws.send(JSON.stringify({ type: "TERMINAL_CATCHUP", lines: catchup }));
|
|
1022
|
+
}
|
|
1023
|
+
ws.on("close", () => {
|
|
1024
|
+
clientCount--;
|
|
1025
|
+
connectedClients.delete(ws);
|
|
1026
|
+
console.log(chalk.yellow("\u25CF") + ` Extension disconnected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
515
1027
|
});
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
wss.on('listening', () => {
|
|
520
|
-
console.log();
|
|
521
|
-
console.log(chalk.gray('─'.repeat(55)));
|
|
522
|
-
console.log(chalk.green('✓') + ' IDE Bridge listening on port ' + chalk.cyan(port));
|
|
523
|
-
console.log(chalk.dim('Waiting for Trace extension to connect...'));
|
|
524
|
-
console.log(chalk.dim('Press Ctrl+C to stop both'));
|
|
525
|
-
console.log(chalk.gray('─'.repeat(55)));
|
|
526
|
-
console.log();
|
|
1028
|
+
ws.on("error", (err) => {
|
|
1029
|
+
console.error(chalk.red("WebSocket error:"), err.message);
|
|
1030
|
+
connectedClients.delete(ws);
|
|
527
1031
|
});
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1032
|
+
});
|
|
1033
|
+
wss.on("error", (error) => {
|
|
1034
|
+
if (error.code === "EADDRINUSE") {
|
|
1035
|
+
console.error(chalk.red(`\u2717 Port ${port} is already in use. Try: trace dev --port 8766`));
|
|
1036
|
+
} else {
|
|
1037
|
+
console.error(chalk.red("Bridge error:"), error.message);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
const http = await import("http");
|
|
1041
|
+
const browserHttpServer = http.createServer(async (req, res) => {
|
|
1042
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1043
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1044
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1045
|
+
if (req.method === "OPTIONS") {
|
|
1046
|
+
res.writeHead(204);
|
|
1047
|
+
res.end();
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const url = new URL(req.url, `http://localhost`);
|
|
1051
|
+
const agentRouteMatch = url.pathname.match(/^\/agent\/([a-z_-]+)$/);
|
|
1052
|
+
if (agentRouteMatch) {
|
|
1053
|
+
const agentName = agentRouteMatch[1];
|
|
1054
|
+
const client2 = [...connectedClients].find((c) => c.readyState === 1);
|
|
1055
|
+
if (!client2) {
|
|
1056
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1057
|
+
res.end(JSON.stringify({ error: "Trace extension not connected." }));
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
try {
|
|
1061
|
+
let body = {};
|
|
1062
|
+
if (req.method === "POST") {
|
|
1063
|
+
body = await new Promise((resolve3, reject) => {
|
|
1064
|
+
let raw = "";
|
|
1065
|
+
req.on("data", (chunk) => raw += chunk.toString());
|
|
1066
|
+
req.on("end", () => {
|
|
1067
|
+
try {
|
|
1068
|
+
resolve3(JSON.parse(raw || "{}"));
|
|
1069
|
+
} catch {
|
|
1070
|
+
resolve3({});
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
req.on("error", reject);
|
|
1074
|
+
});
|
|
538
1075
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1076
|
+
const reqId = ++globalBrowserRequestId;
|
|
1077
|
+
client2.send(JSON.stringify({
|
|
1078
|
+
id: reqId,
|
|
1079
|
+
type: "AGENT_INVOKE",
|
|
1080
|
+
agent: agentName,
|
|
1081
|
+
query: body.query || ""
|
|
1082
|
+
}));
|
|
1083
|
+
const result = await new Promise((resolve3, reject) => {
|
|
1084
|
+
const timer = setTimeout(() => {
|
|
1085
|
+
globalBrowserPending.delete(reqId);
|
|
1086
|
+
reject(new Error("Agent timeout (60s). Extension debug agent did not respond."));
|
|
1087
|
+
}, 6e4);
|
|
1088
|
+
globalBrowserPending.set(reqId, { resolve: resolve3, reject, timer });
|
|
543
1089
|
});
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
1090
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1091
|
+
res.end(JSON.stringify(result, null, 2));
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1094
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
1095
|
+
}
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const browserQueryRoutes = {
|
|
1099
|
+
"/browser/console": "BROWSER_GET_CONSOLE",
|
|
1100
|
+
"/browser/network": "BROWSER_GET_NETWORK",
|
|
1101
|
+
"/browser/dom": "BROWSER_GET_DOM",
|
|
1102
|
+
"/browser/screenshot": "BROWSER_SCREENSHOT"
|
|
1103
|
+
};
|
|
1104
|
+
const isBrowserRoute = url.pathname in browserQueryRoutes || url.pathname === "/browser/eval";
|
|
1105
|
+
if (!isBrowserRoute) {
|
|
1106
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1107
|
+
res.end(JSON.stringify({ error: "Not found. Available: /browser/{console,network,dom,eval,screenshot}" }));
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const client = [...connectedClients].find(
|
|
1111
|
+
(c) => c.readyState === 1
|
|
1112
|
+
/* OPEN */
|
|
1113
|
+
);
|
|
1114
|
+
if (!client) {
|
|
1115
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1116
|
+
res.end(JSON.stringify({ error: "Trace extension not connected. Open the extension and attach to a tab." }));
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
let body = {};
|
|
1121
|
+
if (req.method === "POST") {
|
|
1122
|
+
body = await new Promise((resolve3, reject) => {
|
|
1123
|
+
let raw = "";
|
|
1124
|
+
req.on("data", (chunk) => raw += chunk.toString());
|
|
1125
|
+
req.on("end", () => {
|
|
1126
|
+
try {
|
|
1127
|
+
resolve3(JSON.parse(raw || "{}"));
|
|
1128
|
+
} catch {
|
|
1129
|
+
resolve3({});
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
req.on("error", reject);
|
|
547
1133
|
});
|
|
1134
|
+
}
|
|
1135
|
+
const reqId = ++globalBrowserRequestId;
|
|
1136
|
+
const msgType = browserQueryRoutes[url.pathname] || "BROWSER_EVAL";
|
|
1137
|
+
const wsMsg = { id: reqId, type: msgType };
|
|
1138
|
+
if (url.pathname === "/browser/eval")
|
|
1139
|
+
wsMsg.code = body.code || "";
|
|
1140
|
+
client.send(JSON.stringify(wsMsg));
|
|
1141
|
+
const result = await new Promise((resolve3, reject) => {
|
|
1142
|
+
const timer = setTimeout(() => {
|
|
1143
|
+
globalBrowserPending.delete(reqId);
|
|
1144
|
+
reject(new Error("Browser query timeout (5s). Extension may not be attached to a tab."));
|
|
1145
|
+
}, 5e3);
|
|
1146
|
+
globalBrowserPending.set(reqId, { resolve: resolve3, reject, timer });
|
|
1147
|
+
});
|
|
1148
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1149
|
+
res.end(JSON.stringify(result, null, 2));
|
|
1150
|
+
} catch (e) {
|
|
1151
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1152
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
const BROWSER_HTTP_PORT = 8767;
|
|
1156
|
+
browserHttpServer.listen(BROWSER_HTTP_PORT, "127.0.0.1", () => {
|
|
1157
|
+
console.log(chalk.green("\u2713") + " Browser Query HTTP server on port " + chalk.cyan(BROWSER_HTTP_PORT));
|
|
1158
|
+
console.log(chalk.dim(" Coding agents can query: curl http://localhost:8767/browser/console"));
|
|
1159
|
+
});
|
|
1160
|
+
browserHttpServer.on("error", (e) => {
|
|
1161
|
+
if (e.code !== "EADDRINUSE")
|
|
1162
|
+
console.warn(chalk.yellow("\u26A0 Browser HTTP server error:"), e.message);
|
|
1163
|
+
});
|
|
1164
|
+
let agentServer = null;
|
|
1165
|
+
try {
|
|
1166
|
+
let agentPath = "@gettrace/agent/dist/node/index.js";
|
|
1167
|
+
if (process.env.TRACE_DEV_MODE) {
|
|
1168
|
+
agentPath = path4.resolve(__dirname, "../../packages/trace-agent/dist/node/index.js");
|
|
1169
|
+
console.log(chalk.magenta("\u2699") + " Dev Mode: Using local trace agent at " + chalk.dim(agentPath));
|
|
1170
|
+
}
|
|
1171
|
+
const { Server } = await import(agentPath);
|
|
1172
|
+
process.env.OPENCODE_EXPERIMENTAL = "1";
|
|
1173
|
+
process.env.OPENCODE_ENABLE_EXA = "1";
|
|
1174
|
+
process.env.OPENCODE_EXPERIMENTAL_LSP_TOOL = "1";
|
|
1175
|
+
process.env.OPENCODE_ENABLE_QUESTION_TOOL = "1";
|
|
1176
|
+
process.env.OPENCODE_CLIENT = "app";
|
|
1177
|
+
process.env.OPENCODE_DISABLE_AUTOUPDATE = "1";
|
|
1178
|
+
process.env.OPENCODE_DISABLE_TERMINAL_TITLE = "1";
|
|
1179
|
+
agentServer = await Server.listen({
|
|
1180
|
+
port: 8766,
|
|
1181
|
+
hostname: "127.0.0.1",
|
|
1182
|
+
cors: ["*"]
|
|
548
1183
|
});
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
process.on('SIGTERM', shutdown);
|
|
1184
|
+
console.log(chalk.green("\u2713") + " Trace Agent Server listening on port " + chalk.cyan(8766));
|
|
1185
|
+
console.log(chalk.dim(" LSP \xB7 Exa search \xB7 Plan mode \xB7 All tools unlocked"));
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
console.error(chalk.yellow("\u26A0") + " Failed to start Trace Agent Server:", e.message);
|
|
1188
|
+
}
|
|
1189
|
+
const shutdown = async () => {
|
|
1190
|
+
console.log();
|
|
1191
|
+
console.log(chalk.yellow("\nShutting down..."));
|
|
1192
|
+
if (devProcess && !devProcess.killed) {
|
|
1193
|
+
devProcess.kill("SIGTERM");
|
|
1194
|
+
}
|
|
1195
|
+
if (agentServer) {
|
|
1196
|
+
await agentServer.stop();
|
|
1197
|
+
}
|
|
1198
|
+
wss.close();
|
|
1199
|
+
process.exit(0);
|
|
1200
|
+
};
|
|
1201
|
+
process.on("SIGINT", shutdown);
|
|
1202
|
+
process.on("SIGTERM", shutdown);
|
|
569
1203
|
});
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
// Called by both `trace dev` and `trace connect`.
|
|
573
|
-
// Attaches the full IDE bridge protocol to a WS.
|
|
574
|
-
// ============================================
|
|
1204
|
+
var globalBrowserPending = /* @__PURE__ */ new Map();
|
|
1205
|
+
var globalBrowserRequestId = 0;
|
|
575
1206
|
function attachMessageHandler(ws, projectPath) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
584
|
-
projectInfo = { ...projectInfo, name: pkg.name, version: pkg.version, description: pkg.description };
|
|
585
|
-
}
|
|
1207
|
+
const undoStack = [];
|
|
1208
|
+
let projectInfo = { projectPath };
|
|
1209
|
+
try {
|
|
1210
|
+
const pkgPath = path4.join(projectPath, "package.json");
|
|
1211
|
+
if (fs4.existsSync(pkgPath)) {
|
|
1212
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
1213
|
+
projectInfo = { ...projectInfo, name: pkg.name, version: pkg.version, description: pkg.description };
|
|
586
1214
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
break;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
const rawContent = fs.readFileSync(filePath, 'utf-8');
|
|
617
|
-
const allLines = rawContent.split('\n');
|
|
618
|
-
const offset = message.offset || 1;
|
|
619
|
-
const limit = message.limit || 2000;
|
|
620
|
-
const startIdx = Math.max(0, offset - 1);
|
|
621
|
-
const endIdx = Math.min(allLines.length, startIdx + limit);
|
|
622
|
-
const sliced = allLines.slice(startIdx, endIdx);
|
|
623
|
-
// Line-number prefixed output (like OpenCode)
|
|
624
|
-
const numbered = sliced.map((line, i) => `${startIdx + i + 1}: ${line}`).join('\n');
|
|
625
|
-
const truncated = endIdx < allLines.length;
|
|
626
|
-
// ── READ TOKEN ───────────────────────────────────────────────
|
|
627
|
-
// Agents pass this back in EDIT_FILE / REPLACE_LINES to prove
|
|
628
|
-
// the file hasn't changed since they last read it. If the token
|
|
629
|
-
// mismatches on a subsequent write, the write is rejected so
|
|
630
|
-
// stale line numbers cannot corrupt the file.
|
|
631
|
-
const _rtStat = fs.statSync(filePath);
|
|
632
|
-
const _readToken = `${Math.round(_rtStat.mtimeMs)}-${_rtStat.size}`;
|
|
633
|
-
// ─────────────────────────────────────────────────────────────
|
|
634
|
-
response.data = {
|
|
635
|
-
content: numbered,
|
|
636
|
-
// rawContent intentionally omitted — agents use line-numbered
|
|
637
|
-
// content for edits; sending raw doubles context size per read.
|
|
638
|
-
exists: true,
|
|
639
|
-
path: message.filePath,
|
|
640
|
-
totalLines: allLines.length,
|
|
641
|
-
showing: { from: offset, to: endIdx, truncated },
|
|
642
|
-
readToken: _readToken,
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
else {
|
|
646
|
-
// Suggest similar files if not found
|
|
647
|
-
const dir = path.dirname(filePath);
|
|
648
|
-
const base = path.basename(filePath);
|
|
649
|
-
let suggestions = [];
|
|
650
|
-
try {
|
|
651
|
-
suggestions = fs.readdirSync(dir)
|
|
652
|
-
.filter(e => e.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(e.toLowerCase()))
|
|
653
|
-
.slice(0, 3)
|
|
654
|
-
.map(e => path.relative(projectPath, path.join(dir, e)));
|
|
655
|
-
}
|
|
656
|
-
catch (_) { }
|
|
657
|
-
response.data = { exists: false, suggestions };
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
catch (e) {
|
|
661
|
-
response.error = e.message;
|
|
662
|
-
}
|
|
663
|
-
break;
|
|
664
|
-
case 'GET_SOURCE':
|
|
665
|
-
try {
|
|
666
|
-
const filePath = path.resolve(projectPath, message.filePath);
|
|
667
|
-
if (!filePath.startsWith(projectPath)) {
|
|
668
|
-
response.error = 'Access denied';
|
|
669
|
-
}
|
|
670
|
-
else if (fs.existsSync(filePath)) {
|
|
671
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
672
|
-
const lines = content.split('\n');
|
|
673
|
-
const start = Math.max(0, (message.lineStart || 1) - 1);
|
|
674
|
-
const end = message.lineEnd ? Math.min(lines.length, message.lineEnd) : lines.length;
|
|
675
|
-
response.data = {
|
|
676
|
-
lines: lines.slice(start, end),
|
|
677
|
-
startLine: start + 1,
|
|
678
|
-
endLine: end,
|
|
679
|
-
totalLines: lines.length
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
else {
|
|
683
|
-
response.error = 'File not found';
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
catch (e) {
|
|
687
|
-
response.error = e.message;
|
|
688
|
-
}
|
|
689
|
-
break;
|
|
690
|
-
case 'GET_ERROR_CONTEXT':
|
|
691
|
-
try {
|
|
692
|
-
const filePath = path.resolve(projectPath, message.filePath);
|
|
693
|
-
if (!filePath.startsWith(projectPath)) {
|
|
694
|
-
response.error = 'Access denied';
|
|
695
|
-
}
|
|
696
|
-
else if (fs.existsSync(filePath)) {
|
|
697
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
698
|
-
const lines = content.split('\n');
|
|
699
|
-
const targetLine = message.line || 1;
|
|
700
|
-
const contextLines = message.contextLines || 5;
|
|
701
|
-
const start = Math.max(0, targetLine - contextLines - 1);
|
|
702
|
-
const end = Math.min(lines.length, targetLine + contextLines);
|
|
703
|
-
response.data = {
|
|
704
|
-
lines: lines.slice(start, end).map((line, i) => ({
|
|
705
|
-
number: start + i + 1,
|
|
706
|
-
content: line,
|
|
707
|
-
isError: start + i + 1 === targetLine
|
|
708
|
-
})),
|
|
709
|
-
errorLine: targetLine,
|
|
710
|
-
filePath: message.filePath
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
else {
|
|
714
|
-
response.error = 'File not found';
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
catch (e) {
|
|
718
|
-
response.error = e.message;
|
|
719
|
-
}
|
|
720
|
-
break;
|
|
721
|
-
case 'GET_FILE_TREE':
|
|
722
|
-
try {
|
|
723
|
-
const depth = message.depth || 3;
|
|
724
|
-
const tree = getFileTree(projectPath, depth);
|
|
725
|
-
response.data = { tree };
|
|
726
|
-
}
|
|
727
|
-
catch (e) {
|
|
728
|
-
response.error = e.message;
|
|
729
|
-
}
|
|
730
|
-
break;
|
|
731
|
-
case 'SEARCH_CODE':
|
|
732
|
-
try {
|
|
733
|
-
const { matches, engine } = await searchCode(projectPath, message.query, {
|
|
734
|
-
isRegex: message.isRegex || false,
|
|
735
|
-
caseSensitive: message.caseSensitive !== false,
|
|
736
|
-
maxResults: message.maxResults || 20,
|
|
737
|
-
});
|
|
738
|
-
response.data = { matches, engine };
|
|
739
|
-
}
|
|
740
|
-
catch (e) {
|
|
741
|
-
response.error = e.message;
|
|
742
|
-
}
|
|
743
|
-
break;
|
|
744
|
-
case 'FIND_FILES':
|
|
745
|
-
try {
|
|
746
|
-
const pattern = message.pattern || '';
|
|
747
|
-
// Extract the filename from a glob like **/Services.css
|
|
748
|
-
const fileName = pattern.split('/').pop().replace(/\*\*/g, '').replace(/\*/g, '');
|
|
749
|
-
const maxResults = message.maxResults || 20;
|
|
750
|
-
const ignorePatterns = ['node_modules', '.git', 'dist', 'build', '.next', '.cache'];
|
|
751
|
-
const found = [];
|
|
752
|
-
function findFiles(dir) {
|
|
753
|
-
if (found.length >= maxResults)
|
|
754
|
-
return;
|
|
755
|
-
try {
|
|
756
|
-
const items = fs.readdirSync(dir);
|
|
757
|
-
for (const item of items) {
|
|
758
|
-
if (found.length >= maxResults)
|
|
759
|
-
break;
|
|
760
|
-
if (ignorePatterns.includes(item) || item.startsWith('.'))
|
|
761
|
-
continue;
|
|
762
|
-
const fullPath = path.join(dir, item);
|
|
763
|
-
try {
|
|
764
|
-
const stat = fs.statSync(fullPath);
|
|
765
|
-
if (stat.isDirectory()) {
|
|
766
|
-
findFiles(fullPath);
|
|
767
|
-
}
|
|
768
|
-
else {
|
|
769
|
-
// Match by filename (with optional wildcard)
|
|
770
|
-
const matchByName = !fileName || item === fileName ||
|
|
771
|
-
(pattern.includes('*') && item.endsWith(fileName));
|
|
772
|
-
if (matchByName) {
|
|
773
|
-
found.push(path.relative(projectPath, fullPath));
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
catch (e) { /* skip */ }
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
catch (e) { /* skip */ }
|
|
781
|
-
}
|
|
782
|
-
findFiles(projectPath);
|
|
783
|
-
response.data = found;
|
|
784
|
-
}
|
|
785
|
-
catch (e) {
|
|
786
|
-
response.error = e.message;
|
|
787
|
-
}
|
|
788
|
-
// ========== PROJECT DETECTION (Code Export Pipeline) ==========
|
|
789
|
-
case 'DETECT_PROJECT':
|
|
790
|
-
try {
|
|
791
|
-
const pkgPath = path.join(projectPath, 'package.json');
|
|
792
|
-
let deps = {};
|
|
793
|
-
let devDeps = {};
|
|
794
|
-
let pkgName = '';
|
|
795
|
-
if (fs.existsSync(pkgPath)) {
|
|
796
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
797
|
-
deps = pkg.dependencies || {};
|
|
798
|
-
devDeps = pkg.devDependencies || {};
|
|
799
|
-
pkgName = pkg.name || '';
|
|
800
|
-
}
|
|
801
|
-
const allDeps = { ...deps, ...devDeps };
|
|
802
|
-
// --- Framework detection ---
|
|
803
|
-
let framework = 'html';
|
|
804
|
-
if (allDeps['next'])
|
|
805
|
-
framework = 'next.js';
|
|
806
|
-
else if (allDeps['nuxt'] || allDeps['nuxt3'])
|
|
807
|
-
framework = 'nuxt';
|
|
808
|
-
else if (allDeps['gatsby'])
|
|
809
|
-
framework = 'gatsby';
|
|
810
|
-
else if (allDeps['@remix-run/react'])
|
|
811
|
-
framework = 'remix';
|
|
812
|
-
else if (allDeps['svelte'] || allDeps['@sveltejs/kit'])
|
|
813
|
-
framework = 'svelte';
|
|
814
|
-
else if (allDeps['vue'])
|
|
815
|
-
framework = 'vue';
|
|
816
|
-
else if (allDeps['@angular/core'])
|
|
817
|
-
framework = 'angular';
|
|
818
|
-
else if (allDeps['react'])
|
|
819
|
-
framework = 'react';
|
|
820
|
-
// --- TypeScript detection ---
|
|
821
|
-
const typescript = !!allDeps['typescript'] || fs.existsSync(path.join(projectPath, 'tsconfig.json'));
|
|
822
|
-
const ext = typescript ? '.tsx' : '.jsx';
|
|
823
|
-
// --- Styling detection ---
|
|
824
|
-
let styling = 'plain-css';
|
|
825
|
-
if (allDeps['tailwindcss'])
|
|
826
|
-
styling = 'tailwind';
|
|
827
|
-
else if (allDeps['styled-components'])
|
|
828
|
-
styling = 'styled-components';
|
|
829
|
-
else if (allDeps['@emotion/react'] || allDeps['@emotion/styled'])
|
|
830
|
-
styling = 'emotion';
|
|
831
|
-
else if (allDeps['sass'] || allDeps['node-sass'])
|
|
832
|
-
styling = 'sass';
|
|
833
|
-
// CSS Modules: detected by file presence later
|
|
834
|
-
// --- Router detection (Next.js specific) ---
|
|
835
|
-
let router = null;
|
|
836
|
-
if (framework === 'next.js') {
|
|
837
|
-
if (fs.existsSync(path.join(projectPath, 'src/app')) || fs.existsSync(path.join(projectPath, 'app'))) {
|
|
838
|
-
router = 'app';
|
|
839
|
-
}
|
|
840
|
-
else if (fs.existsSync(path.join(projectPath, 'src/pages')) || fs.existsSync(path.join(projectPath, 'pages'))) {
|
|
841
|
-
router = 'pages';
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
// --- Key file location finder ---
|
|
845
|
-
const findFirst = (candidates) => {
|
|
846
|
-
for (const c of candidates) {
|
|
847
|
-
if (fs.existsSync(path.join(projectPath, c)))
|
|
848
|
-
return c;
|
|
849
|
-
}
|
|
850
|
-
return null;
|
|
851
|
-
};
|
|
852
|
-
const layoutFile = findFirst([
|
|
853
|
-
'src/app/layout.tsx', 'src/app/layout.jsx', 'src/app/layout.js',
|
|
854
|
-
'app/layout.tsx', 'app/layout.jsx', 'app/layout.js',
|
|
855
|
-
'src/pages/_app.tsx', 'src/pages/_app.jsx', 'pages/_app.tsx', 'pages/_app.jsx',
|
|
856
|
-
'src/App.tsx', 'src/App.jsx', 'src/App.js',
|
|
857
|
-
'src/main.tsx', 'src/main.jsx', 'src/main.js',
|
|
858
|
-
'index.html'
|
|
859
|
-
]);
|
|
860
|
-
const globalStyleFile = findFirst([
|
|
861
|
-
'src/app/globals.css', 'src/app/global.css',
|
|
862
|
-
'app/globals.css', 'app/global.css',
|
|
863
|
-
'src/index.css', 'src/styles/globals.css', 'src/styles/global.css',
|
|
864
|
-
'src/App.css', 'src/styles.css',
|
|
865
|
-
'styles/globals.css', 'styles/global.css',
|
|
866
|
-
'css/style.css', 'css/main.css',
|
|
867
|
-
'style.css', 'styles.css'
|
|
868
|
-
]);
|
|
869
|
-
// Initial guess via disk — overridden after tree scan with real component count
|
|
870
|
-
let componentsDir = findFirst([
|
|
871
|
-
'src/components', 'src/app/components', 'components',
|
|
872
|
-
'src/ui', 'src/app/ui'
|
|
873
|
-
]);
|
|
874
|
-
// --- Tailwind config ---
|
|
875
|
-
let tailwindConfig = null;
|
|
876
|
-
let tailwindVersion = null;
|
|
877
|
-
if (styling === 'tailwind') {
|
|
878
|
-
tailwindConfig = findFirst([
|
|
879
|
-
'tailwind.config.ts', 'tailwind.config.js', 'tailwind.config.mjs', 'tailwind.config.cjs'
|
|
880
|
-
]);
|
|
881
|
-
tailwindVersion = allDeps['tailwindcss'] || null;
|
|
882
|
-
}
|
|
883
|
-
// --- Detect code conventions from existing components ---
|
|
884
|
-
let componentPattern = 'arrow-function';
|
|
885
|
-
let exportPattern = 'default';
|
|
886
|
-
let sampleComponentFile = null;
|
|
887
|
-
let sampleComponentCode = null;
|
|
888
|
-
if (componentsDir) {
|
|
889
|
-
const compDir = path.join(projectPath, componentsDir);
|
|
890
|
-
try {
|
|
891
|
-
const files = fs.readdirSync(compDir).filter(f => f.endsWith('.tsx') || f.endsWith('.jsx') || f.endsWith('.js') || f.endsWith('.vue') || f.endsWith('.svelte'));
|
|
892
|
-
if (files.length > 0) {
|
|
893
|
-
sampleComponentFile = path.join(componentsDir, files[0]);
|
|
894
|
-
const code = fs.readFileSync(path.join(compDir, files[0]), 'utf-8');
|
|
895
|
-
// Only include first 80 lines as a pattern reference
|
|
896
|
-
sampleComponentCode = code.split('\n').slice(0, 80).join('\n');
|
|
897
|
-
// Detect patterns
|
|
898
|
-
if (/^export default function /m.test(code)) {
|
|
899
|
-
componentPattern = 'function-declaration';
|
|
900
|
-
exportPattern = 'default';
|
|
901
|
-
}
|
|
902
|
-
else if (/^export function /m.test(code)) {
|
|
903
|
-
componentPattern = 'function-declaration';
|
|
904
|
-
exportPattern = 'named';
|
|
905
|
-
}
|
|
906
|
-
else if (/const \w+ = \(/.test(code) || /const \w+ = \(\) =>/.test(code)) {
|
|
907
|
-
componentPattern = 'arrow-function';
|
|
908
|
-
}
|
|
909
|
-
if (/^export default /m.test(code))
|
|
910
|
-
exportPattern = 'default';
|
|
911
|
-
else if (/^export \{/m.test(code) || /^export const/m.test(code))
|
|
912
|
-
exportPattern = 'named';
|
|
913
|
-
// CSS Modules detection from component imports
|
|
914
|
-
if (/import\s+\w+\s+from\s+['"][^'"]+\.module\.(css|scss|sass)['"]/.test(code)) {
|
|
915
|
-
styling = 'css-modules';
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
catch (e) { /* skip */ }
|
|
920
|
-
}
|
|
921
|
-
// --- Also check layout file for CSS Modules if not yet detected ---
|
|
922
|
-
if (styling === 'plain-css' && layoutFile) {
|
|
923
|
-
try {
|
|
924
|
-
const layoutCode = fs.readFileSync(path.join(projectPath, layoutFile), 'utf-8');
|
|
925
|
-
if (/import\s+\w+\s+from\s+['"][^'"]+\.module\.(css|scss|sass)['"]/.test(layoutCode)) {
|
|
926
|
-
styling = 'css-modules';
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
catch (e) { /* skip */ }
|
|
930
|
-
}
|
|
931
|
-
// --- Read a snippet of global CSS for context ---
|
|
932
|
-
let globalStylePreview = null;
|
|
933
|
-
if (globalStyleFile) {
|
|
934
|
-
try {
|
|
935
|
-
const css = fs.readFileSync(path.join(projectPath, globalStyleFile), 'utf-8');
|
|
936
|
-
globalStylePreview = css.split('\n').slice(0, 40).join('\n');
|
|
937
|
-
}
|
|
938
|
-
catch (e) { /* skip */ }
|
|
939
|
-
}
|
|
940
|
-
// --- Build filtered scaffolding tree (UI-relevant files only) ---
|
|
941
|
-
const SKIP_DIRS = new Set([
|
|
942
|
-
'node_modules', '.git', '.next', '.nuxt', '.svelte-kit',
|
|
943
|
-
'dist', 'build', '.cache', '.turbo', 'coverage',
|
|
944
|
-
'__pycache__', '.vercel', '.output', '.parcel-cache'
|
|
945
|
-
]);
|
|
946
|
-
const UI_EXTENSIONS = new Set([
|
|
947
|
-
'.tsx', '.jsx', '.js', '.ts', '.vue', '.svelte',
|
|
948
|
-
'.css', '.scss', '.sass', '.less', '.module.css', '.module.scss',
|
|
949
|
-
'.html', '.astro'
|
|
950
|
-
]);
|
|
951
|
-
const treeLines = [];
|
|
952
|
-
const MAX_TREE_LINES = 120;
|
|
953
|
-
// Track dir → count of Pascal-case component files for smart detection
|
|
954
|
-
const dirComponentCount = new Map();
|
|
955
|
-
const buildTree = (dir, prefix, depth) => {
|
|
956
|
-
if (depth > 4 || treeLines.length >= MAX_TREE_LINES)
|
|
957
|
-
return;
|
|
958
|
-
try {
|
|
959
|
-
const entries = fs.readdirSync(path.join(projectPath, dir), { withFileTypes: true });
|
|
960
|
-
// Sort: directories first, then files
|
|
961
|
-
const sorted = entries
|
|
962
|
-
.filter(e => !e.name.startsWith('.') || e.name === '.env')
|
|
963
|
-
.sort((a, b) => {
|
|
964
|
-
if (a.isDirectory() && !b.isDirectory())
|
|
965
|
-
return -1;
|
|
966
|
-
if (!a.isDirectory() && b.isDirectory())
|
|
967
|
-
return 1;
|
|
968
|
-
return a.name.localeCompare(b.name);
|
|
969
|
-
});
|
|
970
|
-
for (let i = 0; i < sorted.length && treeLines.length < MAX_TREE_LINES; i++) {
|
|
971
|
-
const entry = sorted[i];
|
|
972
|
-
const isLast = i === sorted.length - 1;
|
|
973
|
-
const connector = isLast ? '└── ' : '├── ';
|
|
974
|
-
const childPrefix = isLast ? ' ' : '│ ';
|
|
975
|
-
const childPath = dir ? `${dir}/${entry.name}` : entry.name;
|
|
976
|
-
if (entry.isDirectory()) {
|
|
977
|
-
if (SKIP_DIRS.has(entry.name))
|
|
978
|
-
continue;
|
|
979
|
-
treeLines.push(`${prefix}${connector}${entry.name}/`);
|
|
980
|
-
buildTree(childPath, prefix + childPrefix, depth + 1);
|
|
981
|
-
}
|
|
982
|
-
else {
|
|
983
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
984
|
-
// Count Pascal-case component files per dir (Footer.tsx, Button.tsx, etc.)
|
|
985
|
-
const COMPONENT_EXTS = new Set(['.tsx', '.jsx', '.vue', '.svelte']);
|
|
986
|
-
if (COMPONENT_EXTS.has(ext) && /^[A-Z]/.test(entry.name)) {
|
|
987
|
-
dirComponentCount.set(dir, (dirComponentCount.get(dir) ?? 0) + 1);
|
|
988
|
-
}
|
|
989
|
-
// Include UI-relevant files + config files at root
|
|
990
|
-
if (UI_EXTENSIONS.has(ext) || (depth <= 1 && (entry.name === 'package.json' || entry.name === 'tsconfig.json' ||
|
|
991
|
-
entry.name.startsWith('tailwind.config') || entry.name.startsWith('next.config') ||
|
|
992
|
-
entry.name.startsWith('vite.config')))) {
|
|
993
|
-
treeLines.push(`${prefix}${connector}${entry.name}`);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
catch (e) { /* permission error, skip */ }
|
|
999
|
-
};
|
|
1000
|
-
// Only scan UI-relevant root dirs (not the entire project)
|
|
1001
|
-
const scanRoots = ['src', 'app', 'pages', 'components', 'styles', 'public', 'lib', 'utils'];
|
|
1002
|
-
const existingRoots = [];
|
|
1003
|
-
for (const root of scanRoots) {
|
|
1004
|
-
if (fs.existsSync(path.join(projectPath, root))) {
|
|
1005
|
-
existingRoots.push(root);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
// Also add root-level config files
|
|
1009
|
-
try {
|
|
1010
|
-
const rootEntries = fs.readdirSync(projectPath, { withFileTypes: true });
|
|
1011
|
-
for (const entry of rootEntries) {
|
|
1012
|
-
if (!entry.isDirectory() && (entry.name === 'package.json' || entry.name === 'tsconfig.json' ||
|
|
1013
|
-
entry.name.startsWith('tailwind.config') || entry.name.startsWith('next.config') ||
|
|
1014
|
-
entry.name.startsWith('vite.config') || entry.name === 'index.html')) {
|
|
1015
|
-
treeLines.push(entry.name);
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
catch (e) { /* skip */ }
|
|
1020
|
-
// Build tree for each UI root
|
|
1021
|
-
for (const root of existingRoots) {
|
|
1022
|
-
treeLines.push(`${root}/`);
|
|
1023
|
-
buildTree(root, '', 1);
|
|
1024
|
-
}
|
|
1025
|
-
const projectTree = treeLines.length > 0 ? treeLines.join('\n') : null;
|
|
1026
|
-
// --- Override componentsDir with real scan result ---
|
|
1027
|
-
// Pick the dir with the most Pascal-case component files.
|
|
1028
|
-
// This beats any hardcoded list: works for src/features, src/modules,
|
|
1029
|
-
// src/views, src/app/components — whatever the project uses.
|
|
1030
|
-
if (dirComponentCount.size > 0) {
|
|
1031
|
-
let bestDir = '';
|
|
1032
|
-
let bestCount = 0;
|
|
1033
|
-
for (const [dir, count] of dirComponentCount.entries()) {
|
|
1034
|
-
if (count > bestCount) {
|
|
1035
|
-
bestCount = count;
|
|
1036
|
-
bestDir = dir;
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
if (bestCount > 0)
|
|
1040
|
-
componentsDir = bestDir;
|
|
1041
|
-
}
|
|
1042
|
-
response.data = {
|
|
1043
|
-
framework,
|
|
1044
|
-
typescript,
|
|
1045
|
-
styling,
|
|
1046
|
-
router,
|
|
1047
|
-
ext,
|
|
1048
|
-
pkgName,
|
|
1049
|
-
// Key file locations
|
|
1050
|
-
layoutFile,
|
|
1051
|
-
globalStyleFile,
|
|
1052
|
-
componentsDir,
|
|
1053
|
-
tailwindConfig,
|
|
1054
|
-
tailwindVersion,
|
|
1055
|
-
// Code conventions
|
|
1056
|
-
componentPattern,
|
|
1057
|
-
exportPattern,
|
|
1058
|
-
sampleComponentFile,
|
|
1059
|
-
sampleComponentCode,
|
|
1060
|
-
// CSS context
|
|
1061
|
-
globalStylePreview,
|
|
1062
|
-
// Scaffolding tree
|
|
1063
|
-
projectTree,
|
|
1064
|
-
};
|
|
1065
|
-
console.log(chalk.blue('ℹ') + ` Project detected: ${chalk.yellow(framework)} + ${chalk.cyan(styling)}${typescript ? chalk.dim(' (TS)') : ''}${router ? chalk.dim(` [${router} router]`) : ''}`);
|
|
1066
|
-
}
|
|
1067
|
-
catch (e) {
|
|
1068
|
-
response.error = e.message;
|
|
1069
|
-
}
|
|
1070
|
-
break;
|
|
1071
|
-
case 'BUILD_CLASS_INDEX':
|
|
1072
|
-
// ────────────────────────────────────────────────────────────────
|
|
1073
|
-
// Scans all component files (.tsx, .jsx, .vue, .svelte) for
|
|
1074
|
-
// className attributes and builds a lookup:
|
|
1075
|
-
// { classSignature → { file: string, line: number } }
|
|
1076
|
-
//
|
|
1077
|
-
// Used by FileResolver in the extension to find which source file
|
|
1078
|
-
// owns a given DOM element when React Fiber _debugSource is null.
|
|
1079
|
-
// ────────────────────────────────────────────────────────────────
|
|
1080
|
-
try {
|
|
1081
|
-
const COMPONENT_EXTS_IDX = new Set(['.tsx', '.jsx', '.vue', '.svelte', '.js', '.ts']);
|
|
1082
|
-
const SKIP_DIRS_IDX = new Set(['node_modules', '.git', '.next', '.nuxt', 'dist', 'build', '.cache']);
|
|
1083
|
-
const index = {};
|
|
1084
|
-
let filesScanned = 0;
|
|
1085
|
-
let classesIndexed = 0;
|
|
1086
|
-
const scanDir = (dir) => {
|
|
1087
|
-
try {
|
|
1088
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1089
|
-
for (const entry of entries) {
|
|
1090
|
-
if (entry.name.startsWith('.'))
|
|
1091
|
-
continue;
|
|
1092
|
-
const fullPath = path.join(dir, entry.name);
|
|
1093
|
-
const relPath = path.relative(projectPath, fullPath).replace(/\\/g, '/');
|
|
1094
|
-
if (entry.isDirectory()) {
|
|
1095
|
-
if (SKIP_DIRS_IDX.has(entry.name))
|
|
1096
|
-
continue;
|
|
1097
|
-
scanDir(fullPath);
|
|
1098
|
-
}
|
|
1099
|
-
else {
|
|
1100
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
1101
|
-
if (!COMPONENT_EXTS_IDX.has(ext))
|
|
1102
|
-
continue;
|
|
1103
|
-
try {
|
|
1104
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
1105
|
-
const lines = content.split('\n');
|
|
1106
|
-
filesScanned++;
|
|
1107
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1108
|
-
const line = lines[i];
|
|
1109
|
-
// Match className="..." and className={'...'} and class="..."
|
|
1110
|
-
const classMatches = line.matchAll(/(?:className|class)\s*=\s*(?:"([^"]+)"|'([^']+)'|\{(?:`([^`]+)`|"([^"]+)"|'([^']+)')\})/g);
|
|
1111
|
-
for (const match of classMatches) {
|
|
1112
|
-
const classValue = match[1] || match[2] || match[3] || match[4] || match[5];
|
|
1113
|
-
if (!classValue)
|
|
1114
|
-
continue;
|
|
1115
|
-
const classes = classValue
|
|
1116
|
-
.split(/\s+/)
|
|
1117
|
-
.filter((c) => c.length > 2 &&
|
|
1118
|
-
!c.startsWith('$') &&
|
|
1119
|
-
!c.includes('{') &&
|
|
1120
|
-
!c.includes('}'));
|
|
1121
|
-
if (classes.length === 0)
|
|
1122
|
-
continue;
|
|
1123
|
-
// Full 2-3 class signature as key
|
|
1124
|
-
const sig = classes.slice(0, 3).sort().join(' ');
|
|
1125
|
-
if (sig && !index[sig]) {
|
|
1126
|
-
index[sig] = { file: relPath, line: i + 1 };
|
|
1127
|
-
classesIndexed++;
|
|
1128
|
-
}
|
|
1129
|
-
// Individual distinctive classes
|
|
1130
|
-
for (const cls of classes) {
|
|
1131
|
-
// Skip very common Tailwind utilities that match too many elements
|
|
1132
|
-
if (/^(flex|grid|block|hidden|relative|absolute|fixed|sticky|inline|w-|h-|p-|m-|text-|bg-|border|rounded|flex-|grid-|col-|row-)/.test(cls))
|
|
1133
|
-
continue;
|
|
1134
|
-
if (cls.length > 5 && !index[cls]) {
|
|
1135
|
-
index[cls] = { file: relPath, line: i + 1 };
|
|
1136
|
-
classesIndexed++;
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
catch (e) { /* skip unreadable files */ }
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
catch (e) { /* skip unreadable dirs */ }
|
|
1147
|
-
};
|
|
1148
|
-
scanDir(projectPath);
|
|
1149
|
-
response.data = {
|
|
1150
|
-
index,
|
|
1151
|
-
filesScanned,
|
|
1152
|
-
classesIndexed,
|
|
1153
|
-
};
|
|
1154
|
-
console.log(chalk.blue('ℹ') + ` Class index built: ${chalk.yellow(classesIndexed)} classes across ${chalk.cyan(filesScanned)} files`);
|
|
1155
|
-
}
|
|
1156
|
-
catch (e) {
|
|
1157
|
-
response.error = e.message;
|
|
1158
|
-
}
|
|
1159
|
-
break;
|
|
1160
|
-
// ========== FILE WRITING (UI Design Export) ==========
|
|
1161
|
-
// --- Undo Stack ---
|
|
1162
|
-
// Snapshots file contents before every write/append/edit.
|
|
1163
|
-
// UNDO_LAST pops the most recent snapshot and restores it.
|
|
1164
|
-
// Max 20 entries to avoid unbounded memory growth.
|
|
1165
|
-
case 'WRITE_FILE':
|
|
1166
|
-
try {
|
|
1167
|
-
const filePath = path.resolve(projectPath, message.filePath);
|
|
1168
|
-
if (!filePath.startsWith(projectPath)) {
|
|
1169
|
-
response.error = 'Access denied: Path outside project';
|
|
1170
|
-
}
|
|
1171
|
-
else {
|
|
1172
|
-
// Snapshot before write
|
|
1173
|
-
const prevContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : null;
|
|
1174
|
-
undoStack.push({ filePath, prevContent, operation: 'WRITE_FILE', timestamp: Date.now(), relativePath: message.filePath });
|
|
1175
|
-
if (undoStack.length > 20)
|
|
1176
|
-
undoStack.shift();
|
|
1177
|
-
// --- CSS safety: validate balanced braces ---
|
|
1178
|
-
if (filePath.endsWith('.css')) {
|
|
1179
|
-
const writeContent = message.content || '';
|
|
1180
|
-
const opens = (writeContent.match(/\{/g) || []).length;
|
|
1181
|
-
const closes = (writeContent.match(/\}/g) || []).length;
|
|
1182
|
-
if (opens !== closes) {
|
|
1183
|
-
// Remove snapshot since we're rejecting the write
|
|
1184
|
-
undoStack.pop();
|
|
1185
|
-
response.error = `CSS validation failed: unbalanced braces (${opens} opening vs ${closes} closing). Fix the CSS before writing.`;
|
|
1186
|
-
break;
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
const dirPath = path.dirname(filePath);
|
|
1190
|
-
if (!fs.existsSync(dirPath)) {
|
|
1191
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
1192
|
-
}
|
|
1193
|
-
await withFileLock(filePath, async () => {
|
|
1194
|
-
fs.writeFileSync(filePath, message.content, 'utf-8');
|
|
1195
|
-
// Auto-format after write
|
|
1196
|
-
const formatter = await autoFormat(filePath, projectPath);
|
|
1197
|
-
response.data = { success: true, path: filePath, formatted: formatter, undoAvailable: true };
|
|
1198
|
-
console.log(chalk.blue('ℹ') + ` Wrote file: ${message.filePath}` + (formatter ? chalk.dim(` (formatted with ${formatter})`) : '') + chalk.dim(' [undo saved]'));
|
|
1199
|
-
});
|
|
1200
|
-
// ── LSP: Post-write TypeScript diagnostics ────────────────────
|
|
1201
|
-
// Runs tsc --noEmit (6s timeout). Never throws. Returns [] for
|
|
1202
|
-
// non-TS files or projects without tsconfig.json.
|
|
1203
|
-
const _wLsp = await checkDiagnostics(filePath, projectPath);
|
|
1204
|
-
if (_wLsp.ran && response.data) {
|
|
1205
|
-
response.data.lspDiagnostics = _wLsp.diagnostics;
|
|
1206
|
-
if (_wLsp.summary) {
|
|
1207
|
-
response.data.lspSummary = _wLsp.summary;
|
|
1208
|
-
if (_wLsp.diagnostics.some(d => d.severity === 'error')) {
|
|
1209
|
-
console.log(chalk.yellow('⚠') + ` ${_wLsp.summary}`);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
// ─────────────────────────────────────────────────────────────
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
catch (e) {
|
|
1217
|
-
response.error = e.message;
|
|
1218
|
-
}
|
|
1219
|
-
break;
|
|
1220
|
-
case 'APPEND_FILE':
|
|
1221
|
-
try {
|
|
1222
|
-
const filePath = path.resolve(projectPath, message.filePath);
|
|
1223
|
-
if (!filePath.startsWith(projectPath)) {
|
|
1224
|
-
response.error = 'Access denied: Path outside project';
|
|
1225
|
-
}
|
|
1226
|
-
else {
|
|
1227
|
-
const isCssFile = filePath.endsWith('.css');
|
|
1228
|
-
const newContent = message.content || '';
|
|
1229
|
-
// --- CSS safety: validate balanced braces ---
|
|
1230
|
-
if (isCssFile) {
|
|
1231
|
-
const opens = (newContent.match(/\{/g) || []).length;
|
|
1232
|
-
const closes = (newContent.match(/\}/g) || []).length;
|
|
1233
|
-
if (opens !== closes) {
|
|
1234
|
-
response.error = `CSS validation failed: unbalanced braces (${opens} opening vs ${closes} closing). Fix the CSS before appending.`;
|
|
1235
|
-
break;
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
// Snapshot before append
|
|
1239
|
-
const prevContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : null;
|
|
1240
|
-
undoStack.push({ filePath, prevContent, operation: 'APPEND_FILE', timestamp: Date.now(), relativePath: message.filePath });
|
|
1241
|
-
if (undoStack.length > 20)
|
|
1242
|
-
undoStack.shift();
|
|
1243
|
-
// --- CSS safety: hoist @import/@charset to file top ---
|
|
1244
|
-
if (isCssFile && prevContent !== null) {
|
|
1245
|
-
// Extract @import and @charset from the NEW content
|
|
1246
|
-
const importRegex = /^@(?:import|charset)\s+[^;]+;\s*$/gm;
|
|
1247
|
-
const newImports = [];
|
|
1248
|
-
const bodyContent = newContent.replace(importRegex, (match) => {
|
|
1249
|
-
newImports.push(match.trim());
|
|
1250
|
-
return '';
|
|
1251
|
-
}).trim();
|
|
1252
|
-
if (newImports.length > 0) {
|
|
1253
|
-
// Find where existing imports end in the original file
|
|
1254
|
-
const existingLines = prevContent.split('\n');
|
|
1255
|
-
let lastImportLineIdx = -1;
|
|
1256
|
-
for (let i = 0; i < existingLines.length; i++) {
|
|
1257
|
-
const trimmed = existingLines[i].trim();
|
|
1258
|
-
if (trimmed.startsWith('@import') || trimmed.startsWith('@charset')) {
|
|
1259
|
-
lastImportLineIdx = i;
|
|
1260
|
-
}
|
|
1261
|
-
// Stop scanning after first non-import, non-comment, non-empty line
|
|
1262
|
-
if (trimmed && !trimmed.startsWith('@import') && !trimmed.startsWith('@charset') && !trimmed.startsWith('/*') && !trimmed.startsWith('*') && !trimmed.startsWith('//')) {
|
|
1263
|
-
break;
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
// Deduplicate: only add imports that don't already exist
|
|
1267
|
-
const dedupedImports = newImports.filter(imp => !prevContent.includes(imp));
|
|
1268
|
-
// Build new file: existing imports + new imports + rest of existing + new body
|
|
1269
|
-
const insertIdx = lastImportLineIdx + 1;
|
|
1270
|
-
const topLines = existingLines.slice(0, insertIdx);
|
|
1271
|
-
const restLines = existingLines.slice(insertIdx);
|
|
1272
|
-
const merged = [
|
|
1273
|
-
...topLines,
|
|
1274
|
-
...dedupedImports,
|
|
1275
|
-
...restLines,
|
|
1276
|
-
'', // separator
|
|
1277
|
-
bodyContent
|
|
1278
|
-
].join('\n');
|
|
1279
|
-
fs.writeFileSync(filePath, merged, 'utf-8');
|
|
1280
|
-
console.log(chalk.blue('ℹ') + ` CSS-safe append: hoisted ${dedupedImports.length} @import(s) to top of ${message.filePath}`);
|
|
1281
|
-
}
|
|
1282
|
-
else {
|
|
1283
|
-
// No imports in new content — simple append
|
|
1284
|
-
fs.appendFileSync(filePath, '\n\n' + newContent, 'utf-8');
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
else {
|
|
1288
|
-
// Non-CSS file or new file — simple append
|
|
1289
|
-
fs.appendFileSync(filePath, '\n' + newContent, 'utf-8');
|
|
1290
|
-
}
|
|
1291
|
-
// Auto-format after append
|
|
1292
|
-
const formatter = await autoFormat(filePath, projectPath);
|
|
1293
|
-
response.data = { success: true, path: filePath, formatted: formatter, undoAvailable: true };
|
|
1294
|
-
console.log(chalk.blue('ℹ') + ` Appended file: ${message.filePath}` + (formatter ? chalk.dim(` (formatted with ${formatter})`) : '') + chalk.dim(' [undo saved]'));
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
catch (e) {
|
|
1298
|
-
response.error = e.message;
|
|
1299
|
-
}
|
|
1300
|
-
break;
|
|
1301
|
-
// ========== AST-BASED EDITING ==========
|
|
1302
|
-
case 'EDIT_CLASSNAME': {
|
|
1303
|
-
// ────────────────────────────────────────────────────────
|
|
1304
|
-
// AST-precise className replacement for JSX/TSX files.
|
|
1305
|
-
// Survives Prettier reformatting and finds the right element
|
|
1306
|
-
// even when fuzzy string matching would pick the wrong line.
|
|
1307
|
-
//
|
|
1308
|
-
// Required: filePath, newValue
|
|
1309
|
-
// Optional: oldValue (current className to replace)
|
|
1310
|
-
// lineHint (from resolvedLine — strongly preferred)
|
|
1311
|
-
// readToken (drift guard, same as EDIT_FILE)
|
|
1312
|
-
// ────────────────────────────────────────────────────────
|
|
1313
|
-
try {
|
|
1314
|
-
const filePath = path.resolve(projectPath, message.filePath);
|
|
1315
|
-
if (!filePath.startsWith(projectPath)) {
|
|
1316
|
-
response.error = 'Access denied: Path outside project';
|
|
1317
|
-
break;
|
|
1318
|
-
}
|
|
1319
|
-
if (!fs.existsSync(filePath)) {
|
|
1320
|
-
response.error = 'File not found: ' + message.filePath;
|
|
1321
|
-
break;
|
|
1322
|
-
}
|
|
1323
|
-
// Read-token drift guard (same as EDIT_FILE)
|
|
1324
|
-
if (message.readToken) {
|
|
1325
|
-
const stat = fs.statSync(filePath);
|
|
1326
|
-
const token = `${Math.round(stat.mtimeMs)}-${stat.size}`;
|
|
1327
|
-
if (token !== message.readToken) {
|
|
1328
|
-
response.error =
|
|
1329
|
-
'File has changed since last read (readToken mismatch). ' +
|
|
1330
|
-
'Re-read with read_project_file before editing.';
|
|
1331
|
-
break;
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
const source = fs.readFileSync(filePath, 'utf-8');
|
|
1335
|
-
// Snapshot for undo
|
|
1336
|
-
undoStack.push({ filePath, prevContent: source, operation: 'EDIT_CLASSNAME', timestamp: Date.now(), relativePath: message.filePath });
|
|
1337
|
-
if (undoStack.length > 20)
|
|
1338
|
-
undoStack.shift();
|
|
1339
|
-
const result = editJSXClassName(source, {
|
|
1340
|
-
oldValue: message.oldValue,
|
|
1341
|
-
newValue: message.newValue,
|
|
1342
|
-
lineHint: message.lineHint,
|
|
1343
|
-
});
|
|
1344
|
-
if (!result.success) {
|
|
1345
|
-
undoStack.pop();
|
|
1346
|
-
response.error = result.error ?? 'AST edit failed';
|
|
1347
|
-
break;
|
|
1348
|
-
}
|
|
1349
|
-
await withFileLock(filePath, async () => {
|
|
1350
|
-
fs.writeFileSync(filePath, result.code, 'utf-8');
|
|
1351
|
-
const formatter = await autoFormat(filePath, projectPath);
|
|
1352
|
-
response.data = {
|
|
1353
|
-
success: true,
|
|
1354
|
-
path: filePath,
|
|
1355
|
-
strategy: result.strategy,
|
|
1356
|
-
matchedLine: result.matchedLine,
|
|
1357
|
-
formatted: formatter,
|
|
1358
|
-
undoAvailable: true,
|
|
1359
|
-
};
|
|
1360
|
-
console.log(chalk.blue('ℹ') +
|
|
1361
|
-
` EDIT_CLASSNAME: ${message.filePath}` +
|
|
1362
|
-
chalk.dim(` [${result.strategy} @ line ${result.matchedLine}]`) +
|
|
1363
|
-
(formatter ? chalk.dim(` (${formatter})`) : '') +
|
|
1364
|
-
chalk.dim(' [undo saved]'));
|
|
1365
|
-
});
|
|
1366
|
-
// LSP type-check after write
|
|
1367
|
-
const lsp = await checkDiagnostics(filePath, projectPath);
|
|
1368
|
-
if (lsp.ran && response.data) {
|
|
1369
|
-
response.data.lspDiagnostics = lsp.diagnostics;
|
|
1370
|
-
if (lsp.summary)
|
|
1371
|
-
response.data.lspSummary = lsp.summary;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
catch (e) {
|
|
1375
|
-
response.error = e.message;
|
|
1376
|
-
}
|
|
1377
|
-
break;
|
|
1215
|
+
} catch (_) {
|
|
1216
|
+
}
|
|
1217
|
+
ws.on("message", async (data) => {
|
|
1218
|
+
try {
|
|
1219
|
+
const message = JSON.parse(data.toString());
|
|
1220
|
+
const { id, type } = message;
|
|
1221
|
+
let response = { id };
|
|
1222
|
+
switch (type) {
|
|
1223
|
+
case "GET_PROJECT_INFO":
|
|
1224
|
+
response.data = projectInfo;
|
|
1225
|
+
break;
|
|
1226
|
+
case "READ_FILE":
|
|
1227
|
+
try {
|
|
1228
|
+
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1229
|
+
if (!filePath.startsWith(projectPath)) {
|
|
1230
|
+
response.error = "Access denied";
|
|
1231
|
+
} else if (fs4.existsSync(filePath)) {
|
|
1232
|
+
const stat = fs4.statSync(filePath);
|
|
1233
|
+
if (stat.size > 0) {
|
|
1234
|
+
const fd = fs4.openSync(filePath, "r");
|
|
1235
|
+
const sample = Buffer.alloc(Math.min(4096, stat.size));
|
|
1236
|
+
fs4.readSync(fd, sample, 0, sample.length, 0);
|
|
1237
|
+
fs4.closeSync(fd);
|
|
1238
|
+
if (sample.includes(0)) {
|
|
1239
|
+
response.error = `Cannot read binary file: ${message.filePath}`;
|
|
1240
|
+
break;
|
|
1378
1241
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
response.data.lspDiagnostics = _eLsp.diagnostics;
|
|
1501
|
-
if (_eLsp.summary) {
|
|
1502
|
-
response.data.lspSummary = _eLsp.summary;
|
|
1503
|
-
if (_eLsp.diagnostics.some(d => d.severity === 'error')) {
|
|
1504
|
-
console.log(chalk.yellow('⚠') + ` ${_eLsp.summary}`);
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
// ──────────────────────────────────────────────────────────────
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
catch (e) {
|
|
1513
|
-
response.error = e.message;
|
|
1514
|
-
}
|
|
1242
|
+
}
|
|
1243
|
+
const rawContent = fs4.readFileSync(filePath, "utf-8");
|
|
1244
|
+
const allLines = rawContent.split("\n");
|
|
1245
|
+
const offset = message.offset || 1;
|
|
1246
|
+
const limit = message.limit || 2e3;
|
|
1247
|
+
const startIdx = Math.max(0, offset - 1);
|
|
1248
|
+
const endIdx = Math.min(allLines.length, startIdx + limit);
|
|
1249
|
+
const sliced = allLines.slice(startIdx, endIdx);
|
|
1250
|
+
const numbered = sliced.map((line, i) => `${startIdx + i + 1}: ${line}`).join("\n");
|
|
1251
|
+
const truncated = endIdx < allLines.length;
|
|
1252
|
+
const _rtStat = fs4.statSync(filePath);
|
|
1253
|
+
const _readToken = `${Math.round(_rtStat.mtimeMs)}-${_rtStat.size}`;
|
|
1254
|
+
response.data = {
|
|
1255
|
+
content: numbered,
|
|
1256
|
+
// rawContent intentionally omitted — agents use line-numbered
|
|
1257
|
+
// content for edits; sending raw doubles context size per read.
|
|
1258
|
+
exists: true,
|
|
1259
|
+
path: message.filePath,
|
|
1260
|
+
totalLines: allLines.length,
|
|
1261
|
+
showing: { from: offset, to: endIdx, truncated },
|
|
1262
|
+
readToken: _readToken
|
|
1263
|
+
};
|
|
1264
|
+
} else {
|
|
1265
|
+
const dir = path4.dirname(filePath);
|
|
1266
|
+
const base = path4.basename(filePath);
|
|
1267
|
+
let suggestions = [];
|
|
1268
|
+
try {
|
|
1269
|
+
suggestions = fs4.readdirSync(dir).filter((e) => e.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(e.toLowerCase())).slice(0, 3).map((e) => path4.relative(projectPath, path4.join(dir, e)));
|
|
1270
|
+
} catch (_) {
|
|
1271
|
+
}
|
|
1272
|
+
response.data = { exists: false, suggestions };
|
|
1273
|
+
}
|
|
1274
|
+
} catch (e) {
|
|
1275
|
+
response.error = e.message;
|
|
1276
|
+
}
|
|
1277
|
+
break;
|
|
1278
|
+
case "GET_SOURCE":
|
|
1279
|
+
try {
|
|
1280
|
+
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1281
|
+
if (!filePath.startsWith(projectPath)) {
|
|
1282
|
+
response.error = "Access denied";
|
|
1283
|
+
} else if (fs4.existsSync(filePath)) {
|
|
1284
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1285
|
+
const lines = content.split("\n");
|
|
1286
|
+
const start = Math.max(0, (message.lineStart || 1) - 1);
|
|
1287
|
+
const end = message.lineEnd ? Math.min(lines.length, message.lineEnd) : lines.length;
|
|
1288
|
+
response.data = {
|
|
1289
|
+
lines: lines.slice(start, end),
|
|
1290
|
+
startLine: start + 1,
|
|
1291
|
+
endLine: end,
|
|
1292
|
+
totalLines: lines.length
|
|
1293
|
+
};
|
|
1294
|
+
} else {
|
|
1295
|
+
response.error = "File not found";
|
|
1296
|
+
}
|
|
1297
|
+
} catch (e) {
|
|
1298
|
+
response.error = e.message;
|
|
1299
|
+
}
|
|
1300
|
+
break;
|
|
1301
|
+
case "GET_ERROR_CONTEXT":
|
|
1302
|
+
try {
|
|
1303
|
+
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1304
|
+
if (!filePath.startsWith(projectPath)) {
|
|
1305
|
+
response.error = "Access denied";
|
|
1306
|
+
} else if (fs4.existsSync(filePath)) {
|
|
1307
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1308
|
+
const lines = content.split("\n");
|
|
1309
|
+
const targetLine = message.line || 1;
|
|
1310
|
+
const contextLines = message.contextLines || 5;
|
|
1311
|
+
const start = Math.max(0, targetLine - contextLines - 1);
|
|
1312
|
+
const end = Math.min(lines.length, targetLine + contextLines);
|
|
1313
|
+
response.data = {
|
|
1314
|
+
lines: lines.slice(start, end).map((line, i) => ({
|
|
1315
|
+
number: start + i + 1,
|
|
1316
|
+
content: line,
|
|
1317
|
+
isError: start + i + 1 === targetLine
|
|
1318
|
+
})),
|
|
1319
|
+
errorLine: targetLine,
|
|
1320
|
+
filePath: message.filePath
|
|
1321
|
+
};
|
|
1322
|
+
} else {
|
|
1323
|
+
response.error = "File not found";
|
|
1324
|
+
}
|
|
1325
|
+
} catch (e) {
|
|
1326
|
+
response.error = e.message;
|
|
1327
|
+
}
|
|
1328
|
+
break;
|
|
1329
|
+
case "GET_FILE_TREE":
|
|
1330
|
+
try {
|
|
1331
|
+
const depth = message.depth || 3;
|
|
1332
|
+
const tree = getFileTree(projectPath, depth);
|
|
1333
|
+
response.data = { tree };
|
|
1334
|
+
} catch (e) {
|
|
1335
|
+
response.error = e.message;
|
|
1336
|
+
}
|
|
1337
|
+
break;
|
|
1338
|
+
case "SEARCH_CODE":
|
|
1339
|
+
try {
|
|
1340
|
+
const { matches, engine } = await searchCode(
|
|
1341
|
+
projectPath,
|
|
1342
|
+
message.query,
|
|
1343
|
+
{
|
|
1344
|
+
isRegex: message.isRegex || false,
|
|
1345
|
+
caseSensitive: message.caseSensitive !== false,
|
|
1346
|
+
maxResults: message.maxResults || 20
|
|
1347
|
+
}
|
|
1348
|
+
);
|
|
1349
|
+
response.data = { matches, engine };
|
|
1350
|
+
} catch (e) {
|
|
1351
|
+
response.error = e.message;
|
|
1352
|
+
}
|
|
1353
|
+
break;
|
|
1354
|
+
case "FIND_FILES":
|
|
1355
|
+
try {
|
|
1356
|
+
let findFiles2 = function(dir) {
|
|
1357
|
+
if (found.length >= maxResults)
|
|
1358
|
+
return;
|
|
1359
|
+
try {
|
|
1360
|
+
const items = fs4.readdirSync(dir);
|
|
1361
|
+
for (const item of items) {
|
|
1362
|
+
if (found.length >= maxResults)
|
|
1515
1363
|
break;
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
// REPLACE_LINES. readToken proves the file is exactly
|
|
1529
|
-
// the version the agent read before computing its range.
|
|
1530
|
-
if (message.readToken) {
|
|
1531
|
-
const _rStat = fs.statSync(filePath);
|
|
1532
|
-
const _rToken = `${Math.round(_rStat.mtimeMs)}-${_rStat.size}`;
|
|
1533
|
-
if (_rToken !== message.readToken) {
|
|
1534
|
-
response.error =
|
|
1535
|
-
'File has changed since last read (readToken mismatch). ' +
|
|
1536
|
-
'Re-read the file with read_project_file to get fresh line numbers ' +
|
|
1537
|
-
'before replacing. The file was NOT modified.';
|
|
1538
|
-
break;
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
// ────────────────────────────────────────────────────────────
|
|
1542
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1543
|
-
const lines = content.split('\n');
|
|
1544
|
-
const startLine = Math.max(1, message.startLine || 1);
|
|
1545
|
-
const endLine = Math.min(lines.length, message.endLine || startLine);
|
|
1546
|
-
if (startLine > lines.length) {
|
|
1547
|
-
response.error = `Start line ${startLine} is beyond file end (${lines.length} lines)`;
|
|
1548
|
-
}
|
|
1549
|
-
else if (startLine > endLine) {
|
|
1550
|
-
response.error = `Invalid range: start (${startLine}) > end (${endLine})`;
|
|
1551
|
-
}
|
|
1552
|
-
else {
|
|
1553
|
-
// Snapshot for undo
|
|
1554
|
-
undoStack.push({
|
|
1555
|
-
filePath,
|
|
1556
|
-
prevContent: content,
|
|
1557
|
-
operation: 'REPLACE_LINES',
|
|
1558
|
-
timestamp: Date.now(),
|
|
1559
|
-
relativePath: message.filePath
|
|
1560
|
-
});
|
|
1561
|
-
if (undoStack.length > 20)
|
|
1562
|
-
undoStack.shift();
|
|
1563
|
-
const oldLineCount = endLine - startLine + 1;
|
|
1564
|
-
const newLines = (message.newContent || '').split('\n');
|
|
1565
|
-
// ── LAYER 5: JSX content validation (full-file + range hint) ──
|
|
1566
|
-
// Validates the RESULTING file. If unbalanced because the
|
|
1567
|
-
// deleted range has unclosed syntax, scans forward to find
|
|
1568
|
-
// the precise suggested end_line — agent fixes in ONE retry.
|
|
1569
|
-
const _jsExts2 = ['.js', '.jsx', '.tsx', '.ts'];
|
|
1570
|
-
if (_jsExts2.some(ext => filePath.endsWith(ext))) {
|
|
1571
|
-
const _strip = (s) => s
|
|
1572
|
-
.replace(/`[^`]*`/g, '``')
|
|
1573
|
-
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
|
1574
|
-
.replace(/\'(?:[^\'\\]|\\.)*\'/g, "''");
|
|
1575
|
-
// ── Baseline: measure original file's brace debt ──
|
|
1576
|
-
const _origStripped2 = _strip(content);
|
|
1577
|
-
const _origDebt2 = (_origStripped2.match(/\{/g) || []).length - (_origStripped2.match(/\}/g) || []).length;
|
|
1578
|
-
const _origPDebt2 = (_origStripped2.match(/\(/g) || []).length - (_origStripped2.match(/\)/g) || []).length;
|
|
1579
|
-
const _previewLines = [...lines];
|
|
1580
|
-
const _previewNew = (message.newContent || '').split('\n');
|
|
1581
|
-
_previewLines.splice(startLine - 1, endLine - startLine + 1, ..._previewNew);
|
|
1582
|
-
const _stripped = _strip(_previewLines.join('\n'));
|
|
1583
|
-
const _bo = (_stripped.match(/\{/g) || []).length;
|
|
1584
|
-
const _bc = (_stripped.match(/\}/g) || []).length;
|
|
1585
|
-
const _po = (_stripped.match(/\(/g) || []).length;
|
|
1586
|
-
const _pc = (_stripped.match(/\)/g) || []).length;
|
|
1587
|
-
const _newDebt2 = _bo - _bc;
|
|
1588
|
-
const _newPDebt2 = _po - _pc;
|
|
1589
|
-
// Only fail if the edit makes the imbalance WORSE than baseline
|
|
1590
|
-
if (Math.abs(_newDebt2) > Math.abs(_origDebt2) || Math.abs(_newPDebt2) > Math.abs(_origPDebt2)) {
|
|
1591
|
-
undoStack.pop();
|
|
1592
|
-
// Determine if deleted lines are the source of imbalance
|
|
1593
|
-
const _deletedBlock = lines.slice(startLine - 1, endLine).join('\n');
|
|
1594
|
-
const _ds = _strip(_deletedBlock);
|
|
1595
|
-
const _braceDebt = (_ds.match(/\{/g) || []).length - (_ds.match(/\}/g) || []).length;
|
|
1596
|
-
const _parenDebt = (_ds.match(/\(/g) || []).length - (_ds.match(/\)/g) || []).length;
|
|
1597
|
-
let _hint = '';
|
|
1598
|
-
if (_braceDebt > 0 || _parenDebt > 0) {
|
|
1599
|
-
// Scan forward line-by-line to find where the debt is repaid
|
|
1600
|
-
let _bd = _braceDebt;
|
|
1601
|
-
let _pd = _parenDebt;
|
|
1602
|
-
let _sugEnd = endLine;
|
|
1603
|
-
const _tail = lines.slice(endLine);
|
|
1604
|
-
for (let _i = 0; _i < _tail.length; _i++) {
|
|
1605
|
-
const _ls = _strip(_tail[_i]);
|
|
1606
|
-
_bd -= (_ls.match(/\}/g) || []).length - (_ls.match(/\{/g) || []).length;
|
|
1607
|
-
_pd -= (_ls.match(/\)/g) || []).length - (_ls.match(/\(/g) || []).length;
|
|
1608
|
-
_sugEnd = endLine + _i + 1;
|
|
1609
|
-
if (_bd <= 0 && _pd <= 0)
|
|
1610
|
-
break;
|
|
1611
|
-
}
|
|
1612
|
-
_hint = ` The range ${startLine}-${endLine} has unclosed syntax.` +
|
|
1613
|
-
` Expand end_line to ~${_sugEnd} to include the closing syntax, then retry.`;
|
|
1614
|
-
}
|
|
1615
|
-
else {
|
|
1616
|
-
_hint = ' Re-read the file and ensure the replacement keeps all JSX balanced.';
|
|
1617
|
-
}
|
|
1618
|
-
response.error = `JSX validation failed: resulting file has unbalanced syntax ` +
|
|
1619
|
-
`(braces: ${_bo} open / ${_bc} close, parens: ${_po} open / ${_pc} close). ` +
|
|
1620
|
-
`The file was NOT modified.${_hint}`;
|
|
1621
|
-
break;
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
// ──────────────────────────────────────────────────────────────
|
|
1625
|
-
// splice + write inside lock
|
|
1626
|
-
await withFileLock(filePath, async () => {
|
|
1627
|
-
lines.splice(startLine - 1, oldLineCount, ...newLines);
|
|
1628
|
-
const newContent = lines.join('\n');
|
|
1629
|
-
fs.writeFileSync(filePath, newContent, 'utf-8');
|
|
1630
|
-
// Auto-format
|
|
1631
|
-
const formatter = await autoFormat(filePath, projectPath);
|
|
1632
|
-
const lineDelta = newLines.length - oldLineCount;
|
|
1633
|
-
response.data = {
|
|
1634
|
-
success: true,
|
|
1635
|
-
path: filePath,
|
|
1636
|
-
linesReplaced: `${startLine}-${endLine}`,
|
|
1637
|
-
oldLineCount,
|
|
1638
|
-
newLineCount: newLines.length,
|
|
1639
|
-
lineDelta: lineDelta,
|
|
1640
|
-
totalLines: lines.length,
|
|
1641
|
-
formatted: formatter,
|
|
1642
|
-
undoAvailable: true,
|
|
1643
|
-
hint: lineDelta !== 0
|
|
1644
|
-
? `Line count changed by ${lineDelta > 0 ? '+' : ''}${lineDelta}. Adjust subsequent line numbers if making more edits.`
|
|
1645
|
-
: null
|
|
1646
|
-
};
|
|
1647
|
-
const formatLabel = formatter ? chalk.dim(` (${formatter})`) : '';
|
|
1648
|
-
console.log(chalk.blue('ℹ') + ` Replaced lines ${startLine}-${endLine} in ${message.filePath} (${oldLineCount}→${newLines.length} lines)${formatLabel}` + chalk.dim(' [undo saved]'));
|
|
1649
|
-
});
|
|
1650
|
-
// ── LSP: Post-replace TypeScript diagnostics ──────────────────
|
|
1651
|
-
const _rLsp = await checkDiagnostics(filePath, projectPath);
|
|
1652
|
-
if (_rLsp.ran && response.data) {
|
|
1653
|
-
response.data.lspDiagnostics = _rLsp.diagnostics;
|
|
1654
|
-
if (_rLsp.summary) {
|
|
1655
|
-
response.data.lspSummary = _rLsp.summary;
|
|
1656
|
-
if (_rLsp.diagnostics.some(d => d.severity === 'error')) {
|
|
1657
|
-
console.log(chalk.yellow('⚠') + ` ${_rLsp.summary}`);
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
// ──────────────────────────────────────────────────────────────
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
catch (e) {
|
|
1666
|
-
response.error = e.message;
|
|
1364
|
+
if (ignorePatterns.includes(item) || item.startsWith("."))
|
|
1365
|
+
continue;
|
|
1366
|
+
const fullPath = path4.join(dir, item);
|
|
1367
|
+
try {
|
|
1368
|
+
const stat = fs4.statSync(fullPath);
|
|
1369
|
+
if (stat.isDirectory()) {
|
|
1370
|
+
findFiles2(fullPath);
|
|
1371
|
+
} else {
|
|
1372
|
+
const matchByName = !fileName || item === fileName || pattern.includes("*") && item.endsWith(fileName);
|
|
1373
|
+
if (matchByName) {
|
|
1374
|
+
found.push(path4.relative(projectPath, fullPath));
|
|
1375
|
+
}
|
|
1667
1376
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1377
|
+
} catch (e) {
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
} catch (e) {
|
|
1381
|
+
}
|
|
1382
|
+
};
|
|
1383
|
+
var findFiles = findFiles2;
|
|
1384
|
+
const pattern = message.pattern || "";
|
|
1385
|
+
const fileName = pattern.split("/").pop().replace(/\*\*/g, "").replace(/\*/g, "");
|
|
1386
|
+
const maxResults = message.maxResults || 20;
|
|
1387
|
+
const ignorePatterns = ["node_modules", ".git", "dist", "build", ".next", ".cache"];
|
|
1388
|
+
const found = [];
|
|
1389
|
+
findFiles2(projectPath);
|
|
1390
|
+
response.data = found;
|
|
1391
|
+
} catch (e) {
|
|
1392
|
+
response.error = e.message;
|
|
1393
|
+
}
|
|
1394
|
+
case "DETECT_PROJECT":
|
|
1395
|
+
try {
|
|
1396
|
+
const pkgPath = path4.join(projectPath, "package.json");
|
|
1397
|
+
let deps = {};
|
|
1398
|
+
let devDeps = {};
|
|
1399
|
+
let pkgName = "";
|
|
1400
|
+
if (fs4.existsSync(pkgPath)) {
|
|
1401
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
1402
|
+
deps = pkg.dependencies || {};
|
|
1403
|
+
devDeps = pkg.devDependencies || {};
|
|
1404
|
+
pkgName = pkg.name || "";
|
|
1405
|
+
}
|
|
1406
|
+
const allDeps = { ...deps, ...devDeps };
|
|
1407
|
+
let framework = "html";
|
|
1408
|
+
if (allDeps["next"])
|
|
1409
|
+
framework = "next.js";
|
|
1410
|
+
else if (allDeps["nuxt"] || allDeps["nuxt3"])
|
|
1411
|
+
framework = "nuxt";
|
|
1412
|
+
else if (allDeps["gatsby"])
|
|
1413
|
+
framework = "gatsby";
|
|
1414
|
+
else if (allDeps["@remix-run/react"])
|
|
1415
|
+
framework = "remix";
|
|
1416
|
+
else if (allDeps["svelte"] || allDeps["@sveltejs/kit"])
|
|
1417
|
+
framework = "svelte";
|
|
1418
|
+
else if (allDeps["vue"])
|
|
1419
|
+
framework = "vue";
|
|
1420
|
+
else if (allDeps["@angular/core"])
|
|
1421
|
+
framework = "angular";
|
|
1422
|
+
else if (allDeps["react"])
|
|
1423
|
+
framework = "react";
|
|
1424
|
+
const typescript = !!allDeps["typescript"] || fs4.existsSync(path4.join(projectPath, "tsconfig.json"));
|
|
1425
|
+
const ext = typescript ? ".tsx" : ".jsx";
|
|
1426
|
+
let styling = "plain-css";
|
|
1427
|
+
if (allDeps["tailwindcss"])
|
|
1428
|
+
styling = "tailwind";
|
|
1429
|
+
else if (allDeps["styled-components"])
|
|
1430
|
+
styling = "styled-components";
|
|
1431
|
+
else if (allDeps["@emotion/react"] || allDeps["@emotion/styled"])
|
|
1432
|
+
styling = "emotion";
|
|
1433
|
+
else if (allDeps["sass"] || allDeps["node-sass"])
|
|
1434
|
+
styling = "sass";
|
|
1435
|
+
let router = null;
|
|
1436
|
+
if (framework === "next.js") {
|
|
1437
|
+
if (fs4.existsSync(path4.join(projectPath, "src/app")) || fs4.existsSync(path4.join(projectPath, "app"))) {
|
|
1438
|
+
router = "app";
|
|
1439
|
+
} else if (fs4.existsSync(path4.join(projectPath, "src/pages")) || fs4.existsSync(path4.join(projectPath, "pages"))) {
|
|
1440
|
+
router = "pages";
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
const findFirst = (candidates) => {
|
|
1444
|
+
for (const c of candidates) {
|
|
1445
|
+
if (fs4.existsSync(path4.join(projectPath, c)))
|
|
1446
|
+
return c;
|
|
1447
|
+
}
|
|
1448
|
+
return null;
|
|
1449
|
+
};
|
|
1450
|
+
const layoutFile = findFirst([
|
|
1451
|
+
"src/app/layout.tsx",
|
|
1452
|
+
"src/app/layout.jsx",
|
|
1453
|
+
"src/app/layout.js",
|
|
1454
|
+
"app/layout.tsx",
|
|
1455
|
+
"app/layout.jsx",
|
|
1456
|
+
"app/layout.js",
|
|
1457
|
+
"src/pages/_app.tsx",
|
|
1458
|
+
"src/pages/_app.jsx",
|
|
1459
|
+
"pages/_app.tsx",
|
|
1460
|
+
"pages/_app.jsx",
|
|
1461
|
+
"src/App.tsx",
|
|
1462
|
+
"src/App.jsx",
|
|
1463
|
+
"src/App.js",
|
|
1464
|
+
"src/main.tsx",
|
|
1465
|
+
"src/main.jsx",
|
|
1466
|
+
"src/main.js",
|
|
1467
|
+
"index.html"
|
|
1468
|
+
]);
|
|
1469
|
+
const globalStyleFile = findFirst([
|
|
1470
|
+
"src/app/globals.css",
|
|
1471
|
+
"src/app/global.css",
|
|
1472
|
+
"app/globals.css",
|
|
1473
|
+
"app/global.css",
|
|
1474
|
+
"src/index.css",
|
|
1475
|
+
"src/styles/globals.css",
|
|
1476
|
+
"src/styles/global.css",
|
|
1477
|
+
"src/App.css",
|
|
1478
|
+
"src/styles.css",
|
|
1479
|
+
"styles/globals.css",
|
|
1480
|
+
"styles/global.css",
|
|
1481
|
+
"css/style.css",
|
|
1482
|
+
"css/main.css",
|
|
1483
|
+
"style.css",
|
|
1484
|
+
"styles.css"
|
|
1485
|
+
]);
|
|
1486
|
+
let componentsDir = findFirst([
|
|
1487
|
+
"src/components",
|
|
1488
|
+
"src/app/components",
|
|
1489
|
+
"components",
|
|
1490
|
+
"src/ui",
|
|
1491
|
+
"src/app/ui"
|
|
1492
|
+
]);
|
|
1493
|
+
let tailwindConfig = null;
|
|
1494
|
+
let tailwindVersion = null;
|
|
1495
|
+
if (styling === "tailwind") {
|
|
1496
|
+
tailwindConfig = findFirst([
|
|
1497
|
+
"tailwind.config.ts",
|
|
1498
|
+
"tailwind.config.js",
|
|
1499
|
+
"tailwind.config.mjs",
|
|
1500
|
+
"tailwind.config.cjs"
|
|
1501
|
+
]);
|
|
1502
|
+
tailwindVersion = allDeps["tailwindcss"] || null;
|
|
1503
|
+
}
|
|
1504
|
+
let componentPattern = "arrow-function";
|
|
1505
|
+
let exportPattern = "default";
|
|
1506
|
+
let sampleComponentFile = null;
|
|
1507
|
+
let sampleComponentCode = null;
|
|
1508
|
+
if (componentsDir) {
|
|
1509
|
+
const compDir = path4.join(projectPath, componentsDir);
|
|
1510
|
+
try {
|
|
1511
|
+
const files = fs4.readdirSync(compDir).filter(
|
|
1512
|
+
(f) => f.endsWith(".tsx") || f.endsWith(".jsx") || f.endsWith(".js") || f.endsWith(".vue") || f.endsWith(".svelte")
|
|
1513
|
+
);
|
|
1514
|
+
if (files.length > 0) {
|
|
1515
|
+
sampleComponentFile = path4.join(componentsDir, files[0]);
|
|
1516
|
+
const code = fs4.readFileSync(path4.join(compDir, files[0]), "utf-8");
|
|
1517
|
+
sampleComponentCode = code.split("\n").slice(0, 80).join("\n");
|
|
1518
|
+
if (/^export default function /m.test(code)) {
|
|
1519
|
+
componentPattern = "function-declaration";
|
|
1520
|
+
exportPattern = "default";
|
|
1521
|
+
} else if (/^export function /m.test(code)) {
|
|
1522
|
+
componentPattern = "function-declaration";
|
|
1523
|
+
exportPattern = "named";
|
|
1524
|
+
} else if (/const \w+ = \(/.test(code) || /const \w+ = \(\) =>/.test(code)) {
|
|
1525
|
+
componentPattern = "arrow-function";
|
|
1526
|
+
}
|
|
1527
|
+
if (/^export default /m.test(code))
|
|
1528
|
+
exportPattern = "default";
|
|
1529
|
+
else if (/^export \{/m.test(code) || /^export const/m.test(code))
|
|
1530
|
+
exportPattern = "named";
|
|
1531
|
+
if (/import\s+\w+\s+from\s+['"][^'"]+\.module\.(css|scss|sass)['"]/.test(code)) {
|
|
1532
|
+
styling = "css-modules";
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
} catch (e) {
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
if (styling === "plain-css" && layoutFile) {
|
|
1539
|
+
try {
|
|
1540
|
+
const layoutCode = fs4.readFileSync(path4.join(projectPath, layoutFile), "utf-8");
|
|
1541
|
+
if (/import\s+\w+\s+from\s+['"][^'"]+\.module\.(css|scss|sass)['"]/.test(layoutCode)) {
|
|
1542
|
+
styling = "css-modules";
|
|
1543
|
+
}
|
|
1544
|
+
} catch (e) {
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
let globalStylePreview = null;
|
|
1548
|
+
if (globalStyleFile) {
|
|
1549
|
+
try {
|
|
1550
|
+
const css = fs4.readFileSync(path4.join(projectPath, globalStyleFile), "utf-8");
|
|
1551
|
+
globalStylePreview = css.split("\n").slice(0, 40).join("\n");
|
|
1552
|
+
} catch (e) {
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1556
|
+
"node_modules",
|
|
1557
|
+
".git",
|
|
1558
|
+
".next",
|
|
1559
|
+
".nuxt",
|
|
1560
|
+
".svelte-kit",
|
|
1561
|
+
"dist",
|
|
1562
|
+
"build",
|
|
1563
|
+
".cache",
|
|
1564
|
+
".turbo",
|
|
1565
|
+
"coverage",
|
|
1566
|
+
"__pycache__",
|
|
1567
|
+
".vercel",
|
|
1568
|
+
".output",
|
|
1569
|
+
".parcel-cache"
|
|
1570
|
+
]);
|
|
1571
|
+
const UI_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1572
|
+
".tsx",
|
|
1573
|
+
".jsx",
|
|
1574
|
+
".js",
|
|
1575
|
+
".ts",
|
|
1576
|
+
".vue",
|
|
1577
|
+
".svelte",
|
|
1578
|
+
".css",
|
|
1579
|
+
".scss",
|
|
1580
|
+
".sass",
|
|
1581
|
+
".less",
|
|
1582
|
+
".module.css",
|
|
1583
|
+
".module.scss",
|
|
1584
|
+
".html",
|
|
1585
|
+
".astro"
|
|
1586
|
+
]);
|
|
1587
|
+
const treeLines = [];
|
|
1588
|
+
const MAX_TREE_LINES = 120;
|
|
1589
|
+
const dirComponentCount = /* @__PURE__ */ new Map();
|
|
1590
|
+
const buildTree = (dir, prefix, depth) => {
|
|
1591
|
+
if (depth > 4 || treeLines.length >= MAX_TREE_LINES)
|
|
1592
|
+
return;
|
|
1593
|
+
try {
|
|
1594
|
+
const entries = fs4.readdirSync(path4.join(projectPath, dir), { withFileTypes: true });
|
|
1595
|
+
const sorted = entries.filter((e) => !e.name.startsWith(".") || e.name === ".env").sort((a, b) => {
|
|
1596
|
+
if (a.isDirectory() && !b.isDirectory())
|
|
1597
|
+
return -1;
|
|
1598
|
+
if (!a.isDirectory() && b.isDirectory())
|
|
1599
|
+
return 1;
|
|
1600
|
+
return a.name.localeCompare(b.name);
|
|
1601
|
+
});
|
|
1602
|
+
for (let i = 0; i < sorted.length && treeLines.length < MAX_TREE_LINES; i++) {
|
|
1603
|
+
const entry = sorted[i];
|
|
1604
|
+
const isLast = i === sorted.length - 1;
|
|
1605
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1606
|
+
const childPrefix = isLast ? " " : "\u2502 ";
|
|
1607
|
+
const childPath = dir ? `${dir}/${entry.name}` : entry.name;
|
|
1608
|
+
if (entry.isDirectory()) {
|
|
1609
|
+
if (SKIP_DIRS.has(entry.name))
|
|
1610
|
+
continue;
|
|
1611
|
+
treeLines.push(`${prefix}${connector}${entry.name}/`);
|
|
1612
|
+
buildTree(childPath, prefix + childPrefix, depth + 1);
|
|
1613
|
+
} else {
|
|
1614
|
+
const ext2 = path4.extname(entry.name).toLowerCase();
|
|
1615
|
+
const COMPONENT_EXTS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".vue", ".svelte"]);
|
|
1616
|
+
if (COMPONENT_EXTS.has(ext2) && /^[A-Z]/.test(entry.name)) {
|
|
1617
|
+
dirComponentCount.set(dir, (dirComponentCount.get(dir) ?? 0) + 1);
|
|
1712
1618
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1619
|
+
if (UI_EXTENSIONS.has(ext2) || depth <= 1 && (entry.name === "package.json" || entry.name === "tsconfig.json" || entry.name.startsWith("tailwind.config") || entry.name.startsWith("next.config") || entry.name.startsWith("vite.config"))) {
|
|
1620
|
+
treeLines.push(`${prefix}${connector}${entry.name}`);
|
|
1715
1621
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
} catch (e) {
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
const scanRoots = ["src", "app", "pages", "components", "styles", "public", "lib", "utils"];
|
|
1628
|
+
const existingRoots = [];
|
|
1629
|
+
for (const root of scanRoots) {
|
|
1630
|
+
if (fs4.existsSync(path4.join(projectPath, root))) {
|
|
1631
|
+
existingRoots.push(root);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
try {
|
|
1635
|
+
const rootEntries = fs4.readdirSync(projectPath, { withFileTypes: true });
|
|
1636
|
+
for (const entry of rootEntries) {
|
|
1637
|
+
if (!entry.isDirectory() && (entry.name === "package.json" || entry.name === "tsconfig.json" || entry.name.startsWith("tailwind.config") || entry.name.startsWith("next.config") || entry.name.startsWith("vite.config") || entry.name === "index.html")) {
|
|
1638
|
+
treeLines.push(entry.name);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
} catch (e) {
|
|
1642
|
+
}
|
|
1643
|
+
for (const root of existingRoots) {
|
|
1644
|
+
treeLines.push(`${root}/`);
|
|
1645
|
+
buildTree(root, "", 1);
|
|
1646
|
+
}
|
|
1647
|
+
const projectTree = treeLines.length > 0 ? treeLines.join("\n") : null;
|
|
1648
|
+
if (dirComponentCount.size > 0) {
|
|
1649
|
+
let bestDir = "";
|
|
1650
|
+
let bestCount = 0;
|
|
1651
|
+
for (const [dir, count] of dirComponentCount.entries()) {
|
|
1652
|
+
if (count > bestCount) {
|
|
1653
|
+
bestCount = count;
|
|
1654
|
+
bestDir = dir;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
if (bestCount > 0)
|
|
1658
|
+
componentsDir = bestDir;
|
|
1659
|
+
}
|
|
1660
|
+
response.data = {
|
|
1661
|
+
framework,
|
|
1662
|
+
typescript,
|
|
1663
|
+
styling,
|
|
1664
|
+
router,
|
|
1665
|
+
ext,
|
|
1666
|
+
pkgName,
|
|
1667
|
+
// Key file locations
|
|
1668
|
+
layoutFile,
|
|
1669
|
+
globalStyleFile,
|
|
1670
|
+
componentsDir,
|
|
1671
|
+
tailwindConfig,
|
|
1672
|
+
tailwindVersion,
|
|
1673
|
+
// Code conventions
|
|
1674
|
+
componentPattern,
|
|
1675
|
+
exportPattern,
|
|
1676
|
+
sampleComponentFile,
|
|
1677
|
+
sampleComponentCode,
|
|
1678
|
+
// CSS context
|
|
1679
|
+
globalStylePreview,
|
|
1680
|
+
// Scaffolding tree
|
|
1681
|
+
projectTree
|
|
1682
|
+
};
|
|
1683
|
+
console.log(chalk.blue("\u2139") + ` Project detected: ${chalk.yellow(framework)} + ${chalk.cyan(styling)}${typescript ? chalk.dim(" (TS)") : ""}${router ? chalk.dim(` [${router} router]`) : ""}`);
|
|
1684
|
+
} catch (e) {
|
|
1685
|
+
response.error = e.message;
|
|
1686
|
+
}
|
|
1687
|
+
break;
|
|
1688
|
+
case "BUILD_CLASS_INDEX":
|
|
1689
|
+
try {
|
|
1690
|
+
const COMPONENT_EXTS_IDX = /* @__PURE__ */ new Set([".tsx", ".jsx", ".vue", ".svelte", ".js", ".ts"]);
|
|
1691
|
+
const SKIP_DIRS_IDX = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", ".nuxt", "dist", "build", ".cache"]);
|
|
1692
|
+
const index = {};
|
|
1693
|
+
let filesScanned = 0;
|
|
1694
|
+
let classesIndexed = 0;
|
|
1695
|
+
const scanDir = (dir) => {
|
|
1696
|
+
try {
|
|
1697
|
+
const entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
1698
|
+
for (const entry of entries) {
|
|
1699
|
+
if (entry.name.startsWith("."))
|
|
1700
|
+
continue;
|
|
1701
|
+
const fullPath = path4.join(dir, entry.name);
|
|
1702
|
+
const relPath = path4.relative(projectPath, fullPath).replace(/\\/g, "/");
|
|
1703
|
+
if (entry.isDirectory()) {
|
|
1704
|
+
if (SKIP_DIRS_IDX.has(entry.name))
|
|
1705
|
+
continue;
|
|
1706
|
+
scanDir(fullPath);
|
|
1707
|
+
} else {
|
|
1708
|
+
const ext = path4.extname(entry.name).toLowerCase();
|
|
1709
|
+
if (!COMPONENT_EXTS_IDX.has(ext))
|
|
1710
|
+
continue;
|
|
1718
1711
|
try {
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
// Env files
|
|
1748
|
-
'.env', '.env.local', '.env.production',
|
|
1749
|
-
'.env.development', '.env.staging',
|
|
1750
|
-
// Next.js/React layout & entry roots
|
|
1751
|
-
/^layout\.(tsx|jsx|js|ts)$/,
|
|
1752
|
-
/^_app\.(tsx|jsx|js|ts)$/,
|
|
1753
|
-
/^_document\.(tsx|jsx|js|ts)$/,
|
|
1754
|
-
/^App\.(tsx|jsx|js|ts)$/,
|
|
1755
|
-
/^main\.(tsx|jsx|js|ts)$/,
|
|
1756
|
-
// Dockerfile & CI
|
|
1757
|
-
'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
|
|
1758
|
-
'.gitignore', '.gitattributes',
|
|
1759
|
-
];
|
|
1760
|
-
const isProtected = PROTECTED_PATTERNS.some(p => typeof p === 'string' ? baseName === p || relPath.endsWith('/' + p)
|
|
1761
|
-
: p.test(baseName));
|
|
1762
|
-
if (isProtected) {
|
|
1763
|
-
response.error = `🛡 Protected file: "${message.filePath}" cannot be deleted by the agent. This file is structurally critical to the project. To resolve conflicts involving this file, edit its contents instead of deleting it.`;
|
|
1764
|
-
break;
|
|
1765
|
-
}
|
|
1766
|
-
// ────────────────────────────────────────────────────────
|
|
1767
|
-
// ── SOFT DELETE → .trace-trash/ ────────────────────────
|
|
1768
|
-
// Instead of hard-deleting, move the file to .trace-trash/
|
|
1769
|
-
// with a timestamp prefix. This survives CLI restarts and
|
|
1770
|
-
// undo-stack evictions — the file is always recoverable
|
|
1771
|
-
// from the filesystem even after the session ends.
|
|
1772
|
-
const trashDir = path.join(projectPath, '.trace-trash');
|
|
1773
|
-
if (!fs.existsSync(trashDir)) {
|
|
1774
|
-
fs.mkdirSync(trashDir, { recursive: true });
|
|
1775
|
-
// Add to .gitignore if not already there
|
|
1776
|
-
const gitignorePath = path.join(projectPath, '.gitignore');
|
|
1777
|
-
if (fs.existsSync(gitignorePath)) {
|
|
1778
|
-
const gi = fs.readFileSync(gitignorePath, 'utf-8');
|
|
1779
|
-
if (!gi.includes('.trace-trash')) {
|
|
1780
|
-
fs.appendFileSync(gitignorePath, '\n# Trace soft-delete recovery folder\n.trace-trash/\n');
|
|
1781
|
-
}
|
|
1712
|
+
const content = fs4.readFileSync(fullPath, "utf-8");
|
|
1713
|
+
const lines = content.split("\n");
|
|
1714
|
+
filesScanned++;
|
|
1715
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1716
|
+
const line = lines[i];
|
|
1717
|
+
const classMatches = line.matchAll(
|
|
1718
|
+
/(?:className|class)\s*=\s*(?:"([^"]+)"|'([^']+)'|\{(?:`([^`]+)`|"([^"]+)"|'([^']+)')\})/g
|
|
1719
|
+
);
|
|
1720
|
+
for (const match of classMatches) {
|
|
1721
|
+
const classValue = match[1] || match[2] || match[3] || match[4] || match[5];
|
|
1722
|
+
if (!classValue)
|
|
1723
|
+
continue;
|
|
1724
|
+
const classes = classValue.split(/\s+/).filter(
|
|
1725
|
+
(c) => c.length > 2 && !c.startsWith("$") && !c.includes("{") && !c.includes("}")
|
|
1726
|
+
);
|
|
1727
|
+
if (classes.length === 0)
|
|
1728
|
+
continue;
|
|
1729
|
+
const sig = classes.slice(0, 3).sort().join(" ");
|
|
1730
|
+
if (sig && !index[sig]) {
|
|
1731
|
+
index[sig] = { file: relPath, line: i + 1 };
|
|
1732
|
+
classesIndexed++;
|
|
1733
|
+
}
|
|
1734
|
+
for (const cls of classes) {
|
|
1735
|
+
if (/^(flex|grid|block|hidden|relative|absolute|fixed|sticky|inline|w-|h-|p-|m-|text-|bg-|border|rounded|flex-|grid-|col-|row-)/.test(cls))
|
|
1736
|
+
continue;
|
|
1737
|
+
if (cls.length > 5 && !index[cls]) {
|
|
1738
|
+
index[cls] = { file: relPath, line: i + 1 };
|
|
1739
|
+
classesIndexed++;
|
|
1782
1740
|
}
|
|
1741
|
+
}
|
|
1783
1742
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
const trashPath = path.join(trashDir, trashName);
|
|
1787
|
-
// Snapshot for in-session UNDO_LAST restore
|
|
1788
|
-
const prevContent = fs.readFileSync(filePath, 'utf-8');
|
|
1789
|
-
undoStack.push({ filePath, prevContent, operation: 'DELETE_FILE', timestamp, relativePath: message.filePath });
|
|
1790
|
-
if (undoStack.length > 20)
|
|
1791
|
-
undoStack.shift();
|
|
1792
|
-
// Move to trash (not hard-delete)
|
|
1793
|
-
fs.renameSync(filePath, trashPath);
|
|
1794
|
-
// ────────────────────────────────────────────────────────
|
|
1795
|
-
response.data = {
|
|
1796
|
-
success: true,
|
|
1797
|
-
deleted: message.filePath,
|
|
1798
|
-
recoveryPath: path.relative(projectPath, trashPath),
|
|
1799
|
-
undoAvailable: true,
|
|
1800
|
-
hint: 'File moved to .trace-trash/ — recoverable even after CLI restart. Call UNDO_LAST to restore to original path.'
|
|
1801
|
-
};
|
|
1802
|
-
console.log(chalk.yellow('🗑') + ` Soft-deleted: ${message.filePath} → .trace-trash/${trashName}` + chalk.dim(' [recoverable]'));
|
|
1803
|
-
}
|
|
1804
|
-
catch (e) {
|
|
1805
|
-
response.error = e.message;
|
|
1806
|
-
}
|
|
1807
|
-
break;
|
|
1808
|
-
case 'RENAME_FILE':
|
|
1809
|
-
try {
|
|
1810
|
-
const oldPath = path.resolve(projectPath, message.oldPath);
|
|
1811
|
-
const newPath = path.resolve(projectPath, message.newPath);
|
|
1812
|
-
if (!oldPath.startsWith(projectPath) || !newPath.startsWith(projectPath)) {
|
|
1813
|
-
response.error = 'Access denied: Path outside project';
|
|
1814
|
-
}
|
|
1815
|
-
else if (!fs.existsSync(oldPath)) {
|
|
1816
|
-
response.error = 'File not found: ' + message.oldPath;
|
|
1817
|
-
}
|
|
1818
|
-
else if (fs.existsSync(newPath)) {
|
|
1819
|
-
response.error = 'Target already exists: ' + message.newPath + '. Delete it first or choose a different name.';
|
|
1820
|
-
}
|
|
1821
|
-
else {
|
|
1822
|
-
// Snapshot the content at the old path so UNDO_LAST can restore it
|
|
1823
|
-
const prevContent = fs.readFileSync(oldPath, 'utf-8');
|
|
1824
|
-
undoStack.push({ filePath: oldPath, prevContent, operation: 'RENAME_FILE', timestamp: Date.now(), relativePath: message.oldPath });
|
|
1825
|
-
if (undoStack.length > 20)
|
|
1826
|
-
undoStack.shift();
|
|
1827
|
-
// Ensure destination directory exists
|
|
1828
|
-
const destDir = path.dirname(newPath);
|
|
1829
|
-
if (!fs.existsSync(destDir))
|
|
1830
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
1831
|
-
fs.renameSync(oldPath, newPath);
|
|
1832
|
-
response.data = {
|
|
1833
|
-
success: true,
|
|
1834
|
-
oldPath: message.oldPath,
|
|
1835
|
-
newPath: message.newPath,
|
|
1836
|
-
undoAvailable: true,
|
|
1837
|
-
};
|
|
1838
|
-
console.log(chalk.blue('ℹ') + ` Renamed: ${message.oldPath} → ${message.newPath}` + chalk.dim(' [undo saved]'));
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
catch (e) {
|
|
1842
|
-
response.error = e.message;
|
|
1743
|
+
}
|
|
1744
|
+
} catch (e) {
|
|
1843
1745
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
} catch (e) {
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
scanDir(projectPath);
|
|
1752
|
+
response.data = {
|
|
1753
|
+
index,
|
|
1754
|
+
filesScanned,
|
|
1755
|
+
classesIndexed
|
|
1756
|
+
};
|
|
1757
|
+
console.log(chalk.blue("\u2139") + ` Class index built: ${chalk.yellow(classesIndexed)} classes across ${chalk.cyan(filesScanned)} files`);
|
|
1758
|
+
} catch (e) {
|
|
1759
|
+
response.error = e.message;
|
|
1760
|
+
}
|
|
1761
|
+
break;
|
|
1762
|
+
case "WRITE_FILE":
|
|
1763
|
+
try {
|
|
1764
|
+
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1765
|
+
if (!filePath.startsWith(projectPath)) {
|
|
1766
|
+
response.error = "Access denied: Path outside project";
|
|
1767
|
+
} else {
|
|
1768
|
+
const prevContent = fs4.existsSync(filePath) ? fs4.readFileSync(filePath, "utf-8") : null;
|
|
1769
|
+
undoStack.push({ filePath, prevContent, operation: "WRITE_FILE", timestamp: Date.now(), relativePath: message.filePath });
|
|
1770
|
+
if (undoStack.length > 20)
|
|
1771
|
+
undoStack.shift();
|
|
1772
|
+
if (filePath.endsWith(".css")) {
|
|
1773
|
+
const writeContent = message.content || "";
|
|
1774
|
+
const opens = (writeContent.match(/\{/g) || []).length;
|
|
1775
|
+
const closes = (writeContent.match(/\}/g) || []).length;
|
|
1776
|
+
if (opens !== closes) {
|
|
1777
|
+
undoStack.pop();
|
|
1778
|
+
response.error = `CSS validation failed: unbalanced braces (${opens} opening vs ${closes} closing). Fix the CSS before writing.`;
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
const dirPath = path4.dirname(filePath);
|
|
1783
|
+
if (!fs4.existsSync(dirPath)) {
|
|
1784
|
+
fs4.mkdirSync(dirPath, { recursive: true });
|
|
1785
|
+
}
|
|
1786
|
+
await withFileLock(filePath, async () => {
|
|
1787
|
+
fs4.writeFileSync(filePath, message.content, "utf-8");
|
|
1788
|
+
const formatter = await autoFormat(filePath, projectPath);
|
|
1789
|
+
response.data = { success: true, path: filePath, formatted: formatter, undoAvailable: true };
|
|
1790
|
+
console.log(chalk.blue("\u2139") + ` Wrote file: ${message.filePath}` + (formatter ? chalk.dim(` (formatted with ${formatter})`) : "") + chalk.dim(" [undo saved]"));
|
|
1791
|
+
});
|
|
1792
|
+
const _wLsp = await checkDiagnostics(filePath, projectPath);
|
|
1793
|
+
if (_wLsp.ran && response.data) {
|
|
1794
|
+
response.data.lspDiagnostics = _wLsp.diagnostics;
|
|
1795
|
+
if (_wLsp.summary) {
|
|
1796
|
+
response.data.lspSummary = _wLsp.summary;
|
|
1797
|
+
if (_wLsp.diagnostics.some((d) => d.severity === "error")) {
|
|
1798
|
+
console.log(chalk.yellow("\u26A0") + ` ${_wLsp.summary}`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
} catch (e) {
|
|
1804
|
+
response.error = e.message;
|
|
1805
|
+
}
|
|
1806
|
+
break;
|
|
1807
|
+
case "APPEND_FILE":
|
|
1808
|
+
try {
|
|
1809
|
+
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1810
|
+
if (!filePath.startsWith(projectPath)) {
|
|
1811
|
+
response.error = "Access denied: Path outside project";
|
|
1812
|
+
} else {
|
|
1813
|
+
const isCssFile = filePath.endsWith(".css");
|
|
1814
|
+
const newContent = message.content || "";
|
|
1815
|
+
if (isCssFile) {
|
|
1816
|
+
const opens = (newContent.match(/\{/g) || []).length;
|
|
1817
|
+
const closes = (newContent.match(/\}/g) || []).length;
|
|
1818
|
+
if (opens !== closes) {
|
|
1819
|
+
response.error = `CSS validation failed: unbalanced braces (${opens} opening vs ${closes} closing). Fix the CSS before appending.`;
|
|
1820
|
+
break;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
const prevContent = fs4.existsSync(filePath) ? fs4.readFileSync(filePath, "utf-8") : null;
|
|
1824
|
+
undoStack.push({ filePath, prevContent, operation: "APPEND_FILE", timestamp: Date.now(), relativePath: message.filePath });
|
|
1825
|
+
if (undoStack.length > 20)
|
|
1826
|
+
undoStack.shift();
|
|
1827
|
+
if (isCssFile && prevContent !== null) {
|
|
1828
|
+
const importRegex = /^@(?:import|charset)\s+[^;]+;\s*$/gm;
|
|
1829
|
+
const newImports = [];
|
|
1830
|
+
const bodyContent = newContent.replace(importRegex, (match) => {
|
|
1831
|
+
newImports.push(match.trim());
|
|
1832
|
+
return "";
|
|
1833
|
+
}).trim();
|
|
1834
|
+
if (newImports.length > 0) {
|
|
1835
|
+
const existingLines = prevContent.split("\n");
|
|
1836
|
+
let lastImportLineIdx = -1;
|
|
1837
|
+
for (let i = 0; i < existingLines.length; i++) {
|
|
1838
|
+
const trimmed = existingLines[i].trim();
|
|
1839
|
+
if (trimmed.startsWith("@import") || trimmed.startsWith("@charset")) {
|
|
1840
|
+
lastImportLineIdx = i;
|
|
1883
1841
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
try {
|
|
1887
|
-
const filePath = path.resolve(projectPath, message.filePath);
|
|
1888
|
-
if (fs.existsSync(filePath)) {
|
|
1889
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1890
|
-
const importRegex = /import\s+(?:[\w*\s{},]*)\s+from\s+['"]([^'"]+)['"]/g;
|
|
1891
|
-
const imports = [];
|
|
1892
|
-
let match;
|
|
1893
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
1894
|
-
imports.push(match[1]);
|
|
1895
|
-
}
|
|
1896
|
-
response.data = { imports };
|
|
1897
|
-
}
|
|
1898
|
-
else {
|
|
1899
|
-
response.error = 'File not found';
|
|
1900
|
-
}
|
|
1842
|
+
if (trimmed && !trimmed.startsWith("@import") && !trimmed.startsWith("@charset") && !trimmed.startsWith("/*") && !trimmed.startsWith("*") && !trimmed.startsWith("//")) {
|
|
1843
|
+
break;
|
|
1901
1844
|
}
|
|
1902
|
-
|
|
1903
|
-
|
|
1845
|
+
}
|
|
1846
|
+
const dedupedImports = newImports.filter((imp) => !prevContent.includes(imp));
|
|
1847
|
+
const insertIdx = lastImportLineIdx + 1;
|
|
1848
|
+
const topLines = existingLines.slice(0, insertIdx);
|
|
1849
|
+
const restLines = existingLines.slice(insertIdx);
|
|
1850
|
+
const merged = [
|
|
1851
|
+
...topLines,
|
|
1852
|
+
...dedupedImports,
|
|
1853
|
+
...restLines,
|
|
1854
|
+
"",
|
|
1855
|
+
// separator
|
|
1856
|
+
bodyContent
|
|
1857
|
+
].join("\n");
|
|
1858
|
+
fs4.writeFileSync(filePath, merged, "utf-8");
|
|
1859
|
+
console.log(chalk.blue("\u2139") + ` CSS-safe append: hoisted ${dedupedImports.length} @import(s) to top of ${message.filePath}`);
|
|
1860
|
+
} else {
|
|
1861
|
+
fs4.appendFileSync(filePath, "\n\n" + newContent, "utf-8");
|
|
1862
|
+
}
|
|
1863
|
+
} else {
|
|
1864
|
+
fs4.appendFileSync(filePath, "\n" + newContent, "utf-8");
|
|
1865
|
+
}
|
|
1866
|
+
const formatter = await autoFormat(filePath, projectPath);
|
|
1867
|
+
response.data = { success: true, path: filePath, formatted: formatter, undoAvailable: true };
|
|
1868
|
+
console.log(chalk.blue("\u2139") + ` Appended file: ${message.filePath}` + (formatter ? chalk.dim(` (formatted with ${formatter})`) : "") + chalk.dim(" [undo saved]"));
|
|
1869
|
+
}
|
|
1870
|
+
} catch (e) {
|
|
1871
|
+
response.error = e.message;
|
|
1872
|
+
}
|
|
1873
|
+
break;
|
|
1874
|
+
case "EDIT_CLASSNAME": {
|
|
1875
|
+
try {
|
|
1876
|
+
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1877
|
+
if (!filePath.startsWith(projectPath)) {
|
|
1878
|
+
response.error = "Access denied: Path outside project";
|
|
1879
|
+
break;
|
|
1880
|
+
}
|
|
1881
|
+
if (!fs4.existsSync(filePath)) {
|
|
1882
|
+
response.error = "File not found: " + message.filePath;
|
|
1883
|
+
break;
|
|
1884
|
+
}
|
|
1885
|
+
if (message.readToken) {
|
|
1886
|
+
const stat = fs4.statSync(filePath);
|
|
1887
|
+
const token = `${Math.round(stat.mtimeMs)}-${stat.size}`;
|
|
1888
|
+
if (token !== message.readToken) {
|
|
1889
|
+
response.error = "File has changed since last read (readToken mismatch). Re-read with read_project_file before editing.";
|
|
1890
|
+
break;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
const source = fs4.readFileSync(filePath, "utf-8");
|
|
1894
|
+
undoStack.push({ filePath, prevContent: source, operation: "EDIT_CLASSNAME", timestamp: Date.now(), relativePath: message.filePath });
|
|
1895
|
+
if (undoStack.length > 20)
|
|
1896
|
+
undoStack.shift();
|
|
1897
|
+
const result = editJSXClassName(source, {
|
|
1898
|
+
oldValue: message.oldValue,
|
|
1899
|
+
newValue: message.newValue,
|
|
1900
|
+
lineHint: message.lineHint
|
|
1901
|
+
});
|
|
1902
|
+
if (!result.success) {
|
|
1903
|
+
undoStack.pop();
|
|
1904
|
+
response.error = result.error ?? "AST edit failed";
|
|
1905
|
+
break;
|
|
1906
|
+
}
|
|
1907
|
+
await withFileLock(filePath, async () => {
|
|
1908
|
+
fs4.writeFileSync(filePath, result.code, "utf-8");
|
|
1909
|
+
const formatter = await autoFormat(filePath, projectPath);
|
|
1910
|
+
response.data = {
|
|
1911
|
+
success: true,
|
|
1912
|
+
path: filePath,
|
|
1913
|
+
strategy: result.strategy,
|
|
1914
|
+
matchedLine: result.matchedLine,
|
|
1915
|
+
formatted: formatter,
|
|
1916
|
+
undoAvailable: true
|
|
1917
|
+
};
|
|
1918
|
+
console.log(
|
|
1919
|
+
chalk.blue("\u2139") + ` EDIT_CLASSNAME: ${message.filePath}` + chalk.dim(` [${result.strategy} @ line ${result.matchedLine}]`) + (formatter ? chalk.dim(` (${formatter})`) : "") + chalk.dim(" [undo saved]")
|
|
1920
|
+
);
|
|
1921
|
+
});
|
|
1922
|
+
const lsp = await checkDiagnostics(filePath, projectPath);
|
|
1923
|
+
if (lsp.ran && response.data) {
|
|
1924
|
+
response.data.lspDiagnostics = lsp.diagnostics;
|
|
1925
|
+
if (lsp.summary)
|
|
1926
|
+
response.data.lspSummary = lsp.summary;
|
|
1927
|
+
}
|
|
1928
|
+
} catch (e) {
|
|
1929
|
+
response.error = e.message;
|
|
1930
|
+
}
|
|
1931
|
+
break;
|
|
1932
|
+
}
|
|
1933
|
+
case "EDIT_FILE":
|
|
1934
|
+
try {
|
|
1935
|
+
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1936
|
+
if (!filePath.startsWith(projectPath)) {
|
|
1937
|
+
response.error = "Access denied: Path outside project";
|
|
1938
|
+
} else if (!fs4.existsSync(filePath)) {
|
|
1939
|
+
const requestedBase = path4.basename(message.filePath);
|
|
1940
|
+
const requestedExt = path4.extname(requestedBase);
|
|
1941
|
+
const requestedStem = requestedBase.replace(requestedExt, "");
|
|
1942
|
+
const suggestions = [];
|
|
1943
|
+
const dirsToSearch = ["src/app", "app", "src/pages", "pages", "src/components", "components", "src"];
|
|
1944
|
+
for (const dir of dirsToSearch) {
|
|
1945
|
+
const absDir = path4.join(projectPath, dir);
|
|
1946
|
+
if (fs4.existsSync(absDir)) {
|
|
1947
|
+
try {
|
|
1948
|
+
const files = fs4.readdirSync(absDir, { recursive: true });
|
|
1949
|
+
for (const f of files.slice(0, 100)) {
|
|
1950
|
+
const fBase = path4.basename(f);
|
|
1951
|
+
if (fBase.startsWith(requestedStem + ".") || fBase === requestedBase) {
|
|
1952
|
+
suggestions.push(path4.join(dir, f).replace(/\\/g, "/"));
|
|
1953
|
+
}
|
|
1904
1954
|
}
|
|
1955
|
+
} catch (e) {
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
const hint = suggestions.length > 0 ? " Did you mean: " + suggestions.slice(0, 3).join(" or ") + "?" : " Double-check the path against detect_project() output (layoutFile, globalStyleFile) or projectTree.";
|
|
1960
|
+
response.error = 'File not found: "' + message.filePath + '".' + hint;
|
|
1961
|
+
} else {
|
|
1962
|
+
if (message.readToken) {
|
|
1963
|
+
const _eStat = fs4.statSync(filePath);
|
|
1964
|
+
const _eToken = `${Math.round(_eStat.mtimeMs)}-${_eStat.size}`;
|
|
1965
|
+
if (_eToken !== message.readToken) {
|
|
1966
|
+
response.error = "File has changed since last read (readToken mismatch). Re-read the file with read_project_file to get fresh content and line numbers before editing. The file was NOT modified.";
|
|
1967
|
+
break;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
1971
|
+
undoStack.push({ filePath, prevContent: content, operation: "EDIT_FILE", timestamp: Date.now(), relativePath: message.filePath });
|
|
1972
|
+
if (undoStack.length > 20)
|
|
1973
|
+
undoStack.shift();
|
|
1974
|
+
const result = fuzzyReplace(content, message.target, message.replacement, message.replaceAll || false);
|
|
1975
|
+
if ("error" in result) {
|
|
1976
|
+
undoStack.pop();
|
|
1977
|
+
response.error = result.error;
|
|
1978
|
+
} else {
|
|
1979
|
+
const _jsExts = [".js", ".jsx", ".tsx", ".ts"];
|
|
1980
|
+
if (_jsExts.some((ext) => filePath.endsWith(ext))) {
|
|
1981
|
+
const _resultContent = result.result;
|
|
1982
|
+
const _strip = (s) => s.replace(/`[^`]*`/g, "``").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/\'(?:[^\'\\]|\\.)*\'/g, "''");
|
|
1983
|
+
const _origStripped = _strip(content);
|
|
1984
|
+
const _origDebt = (_origStripped.match(/\{/g) || []).length - (_origStripped.match(/\}/g) || []).length;
|
|
1985
|
+
const _origPDebt = (_origStripped.match(/\(/g) || []).length - (_origStripped.match(/\)/g) || []).length;
|
|
1986
|
+
const _stripped = _strip(_resultContent);
|
|
1987
|
+
const _bo = (_stripped.match(/\{/g) || []).length;
|
|
1988
|
+
const _bc = (_stripped.match(/\}/g) || []).length;
|
|
1989
|
+
const _po = (_stripped.match(/\(/g) || []).length;
|
|
1990
|
+
const _pc = (_stripped.match(/\)/g) || []).length;
|
|
1991
|
+
const _newDebt = _bo - _bc;
|
|
1992
|
+
const _newPDebt = _po - _pc;
|
|
1993
|
+
if (Math.abs(_newDebt) > Math.abs(_origDebt) || Math.abs(_newPDebt) > Math.abs(_origPDebt)) {
|
|
1994
|
+
undoStack.pop();
|
|
1995
|
+
response.error = `JSX validation failed: resulting file has unbalanced syntax (braces: ${_bo} open / ${_bc} close, parens: ${_po} open / ${_pc} close). The file was NOT modified. Re-read the file and fix the replacement so the overall file stays balanced.`;
|
|
1905
1996
|
break;
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
await withFileLock(filePath, async () => {
|
|
2000
|
+
fs4.writeFileSync(filePath, result.result, "utf-8");
|
|
2001
|
+
const formatter = await autoFormat(filePath, projectPath);
|
|
2002
|
+
response.data = {
|
|
2003
|
+
success: true,
|
|
2004
|
+
path: filePath,
|
|
2005
|
+
strategy: result.strategy,
|
|
2006
|
+
formatted: formatter,
|
|
2007
|
+
undoAvailable: true
|
|
2008
|
+
};
|
|
2009
|
+
const strategyLabel = result.strategy === "exact" ? "" : chalk.dim(` [${result.strategy}]`);
|
|
2010
|
+
const formatLabel = formatter ? chalk.dim(` (${formatter})`) : "";
|
|
2011
|
+
console.log(chalk.blue("\u2139") + ` Edited file: ${message.filePath}${strategyLabel}${formatLabel}` + chalk.dim(" [undo saved]"));
|
|
2012
|
+
});
|
|
2013
|
+
const _eLsp = await checkDiagnostics(filePath, projectPath);
|
|
2014
|
+
if (_eLsp.ran && response.data) {
|
|
2015
|
+
response.data.lspDiagnostics = _eLsp.diagnostics;
|
|
2016
|
+
if (_eLsp.summary) {
|
|
2017
|
+
response.data.lspSummary = _eLsp.summary;
|
|
2018
|
+
if (_eLsp.diagnostics.some((d) => d.severity === "error")) {
|
|
2019
|
+
console.log(chalk.yellow("\u26A0") + ` ${_eLsp.summary}`);
|
|
1920
2020
|
}
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
} catch (e) {
|
|
2026
|
+
response.error = e.message;
|
|
2027
|
+
}
|
|
2028
|
+
break;
|
|
2029
|
+
case "REPLACE_LINES":
|
|
2030
|
+
try {
|
|
2031
|
+
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2032
|
+
if (!filePath.startsWith(projectPath)) {
|
|
2033
|
+
response.error = "Access denied: Path outside project";
|
|
2034
|
+
} else if (!fs4.existsSync(filePath)) {
|
|
2035
|
+
response.error = "File not found: " + message.filePath;
|
|
2036
|
+
} else {
|
|
2037
|
+
if (message.readToken) {
|
|
2038
|
+
const _rStat = fs4.statSync(filePath);
|
|
2039
|
+
const _rToken = `${Math.round(_rStat.mtimeMs)}-${_rStat.size}`;
|
|
2040
|
+
if (_rToken !== message.readToken) {
|
|
2041
|
+
response.error = "File has changed since last read (readToken mismatch). Re-read the file with read_project_file to get fresh line numbers before replacing. The file was NOT modified.";
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2046
|
+
const lines = content.split("\n");
|
|
2047
|
+
const startLine = Math.max(1, message.startLine || 1);
|
|
2048
|
+
const endLine = Math.min(lines.length, message.endLine || startLine);
|
|
2049
|
+
if (startLine > lines.length) {
|
|
2050
|
+
response.error = `Start line ${startLine} is beyond file end (${lines.length} lines)`;
|
|
2051
|
+
} else if (startLine > endLine) {
|
|
2052
|
+
response.error = `Invalid range: start (${startLine}) > end (${endLine})`;
|
|
2053
|
+
} else {
|
|
2054
|
+
undoStack.push({
|
|
2055
|
+
filePath,
|
|
2056
|
+
prevContent: content,
|
|
2057
|
+
operation: "REPLACE_LINES",
|
|
2058
|
+
timestamp: Date.now(),
|
|
2059
|
+
relativePath: message.filePath
|
|
2060
|
+
});
|
|
2061
|
+
if (undoStack.length > 20)
|
|
2062
|
+
undoStack.shift();
|
|
2063
|
+
const oldLineCount = endLine - startLine + 1;
|
|
2064
|
+
const newLines = (message.newContent || "").split("\n");
|
|
2065
|
+
const _jsExts2 = [".js", ".jsx", ".tsx", ".ts"];
|
|
2066
|
+
if (_jsExts2.some((ext) => filePath.endsWith(ext))) {
|
|
2067
|
+
const _strip = (s) => s.replace(/`[^`]*`/g, "``").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/\'(?:[^\'\\]|\\.)*\'/g, "''");
|
|
2068
|
+
const _origStripped2 = _strip(content);
|
|
2069
|
+
const _origDebt2 = (_origStripped2.match(/\{/g) || []).length - (_origStripped2.match(/\}/g) || []).length;
|
|
2070
|
+
const _origPDebt2 = (_origStripped2.match(/\(/g) || []).length - (_origStripped2.match(/\)/g) || []).length;
|
|
2071
|
+
const _previewLines = [...lines];
|
|
2072
|
+
const _previewNew = (message.newContent || "").split("\n");
|
|
2073
|
+
_previewLines.splice(startLine - 1, endLine - startLine + 1, ..._previewNew);
|
|
2074
|
+
const _stripped = _strip(_previewLines.join("\n"));
|
|
2075
|
+
const _bo = (_stripped.match(/\{/g) || []).length;
|
|
2076
|
+
const _bc = (_stripped.match(/\}/g) || []).length;
|
|
2077
|
+
const _po = (_stripped.match(/\(/g) || []).length;
|
|
2078
|
+
const _pc = (_stripped.match(/\)/g) || []).length;
|
|
2079
|
+
const _newDebt2 = _bo - _bc;
|
|
2080
|
+
const _newPDebt2 = _po - _pc;
|
|
2081
|
+
if (Math.abs(_newDebt2) > Math.abs(_origDebt2) || Math.abs(_newPDebt2) > Math.abs(_origPDebt2)) {
|
|
2082
|
+
undoStack.pop();
|
|
2083
|
+
const _deletedBlock = lines.slice(startLine - 1, endLine).join("\n");
|
|
2084
|
+
const _ds = _strip(_deletedBlock);
|
|
2085
|
+
const _braceDebt = (_ds.match(/\{/g) || []).length - (_ds.match(/\}/g) || []).length;
|
|
2086
|
+
const _parenDebt = (_ds.match(/\(/g) || []).length - (_ds.match(/\)/g) || []).length;
|
|
2087
|
+
let _hint = "";
|
|
2088
|
+
if (_braceDebt > 0 || _parenDebt > 0) {
|
|
2089
|
+
let _bd = _braceDebt;
|
|
2090
|
+
let _pd = _parenDebt;
|
|
2091
|
+
let _sugEnd = endLine;
|
|
2092
|
+
const _tail = lines.slice(endLine);
|
|
2093
|
+
for (let _i = 0; _i < _tail.length; _i++) {
|
|
2094
|
+
const _ls = _strip(_tail[_i]);
|
|
2095
|
+
_bd -= (_ls.match(/\}/g) || []).length - (_ls.match(/\{/g) || []).length;
|
|
2096
|
+
_pd -= (_ls.match(/\)/g) || []).length - (_ls.match(/\(/g) || []).length;
|
|
2097
|
+
_sugEnd = endLine + _i + 1;
|
|
2098
|
+
if (_bd <= 0 && _pd <= 0)
|
|
2099
|
+
break;
|
|
2100
|
+
}
|
|
2101
|
+
_hint = ` The range ${startLine}-${endLine} has unclosed syntax. Expand end_line to ~${_sugEnd} to include the closing syntax, then retry.`;
|
|
2102
|
+
} else {
|
|
2103
|
+
_hint = " Re-read the file and ensure the replacement keeps all JSX balanced.";
|
|
1926
2104
|
}
|
|
2105
|
+
response.error = `JSX validation failed: resulting file has unbalanced syntax (braces: ${_bo} open / ${_bc} close, parens: ${_po} open / ${_pc} close). The file was NOT modified.${_hint}`;
|
|
1927
2106
|
break;
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
await withFileLock(filePath, async () => {
|
|
2110
|
+
lines.splice(startLine - 1, oldLineCount, ...newLines);
|
|
2111
|
+
const newContent = lines.join("\n");
|
|
2112
|
+
fs4.writeFileSync(filePath, newContent, "utf-8");
|
|
2113
|
+
const formatter = await autoFormat(filePath, projectPath);
|
|
2114
|
+
const lineDelta = newLines.length - oldLineCount;
|
|
2115
|
+
response.data = {
|
|
2116
|
+
success: true,
|
|
2117
|
+
path: filePath,
|
|
2118
|
+
linesReplaced: `${startLine}-${endLine}`,
|
|
2119
|
+
oldLineCount,
|
|
2120
|
+
newLineCount: newLines.length,
|
|
2121
|
+
lineDelta,
|
|
2122
|
+
totalLines: lines.length,
|
|
2123
|
+
formatted: formatter,
|
|
2124
|
+
undoAvailable: true,
|
|
2125
|
+
hint: lineDelta !== 0 ? `Line count changed by ${lineDelta > 0 ? "+" : ""}${lineDelta}. Adjust subsequent line numbers if making more edits.` : null
|
|
2126
|
+
};
|
|
2127
|
+
const formatLabel = formatter ? chalk.dim(` (${formatter})`) : "";
|
|
2128
|
+
console.log(chalk.blue("\u2139") + ` Replaced lines ${startLine}-${endLine} in ${message.filePath} (${oldLineCount}\u2192${newLines.length} lines)${formatLabel}` + chalk.dim(" [undo saved]"));
|
|
2129
|
+
});
|
|
2130
|
+
const _rLsp = await checkDiagnostics(filePath, projectPath);
|
|
2131
|
+
if (_rLsp.ran && response.data) {
|
|
2132
|
+
response.data.lspDiagnostics = _rLsp.diagnostics;
|
|
2133
|
+
if (_rLsp.summary) {
|
|
2134
|
+
response.data.lspSummary = _rLsp.summary;
|
|
2135
|
+
if (_rLsp.diagnostics.some((d) => d.severity === "error")) {
|
|
2136
|
+
console.log(chalk.yellow("\u26A0") + ` ${_rLsp.summary}`);
|
|
1947
2137
|
}
|
|
1948
|
-
|
|
1949
|
-
// ========== TERMINAL BUFFER ACCESS ==========
|
|
1950
|
-
case 'GET_TERMINAL_BUFFER': {
|
|
1951
|
-
// Returns the last N lines of the dev server terminal output.
|
|
1952
|
-
// Agents use this to get compiler errors without running a build.
|
|
1953
|
-
const lines = parseInt(message.lines) || 100;
|
|
1954
|
-
const buffer = globalTerminalBuffer.getLast(lines);
|
|
1955
|
-
response.data = {
|
|
1956
|
-
lines: buffer,
|
|
1957
|
-
totalLines: globalTerminalBuffer.length,
|
|
1958
|
-
hasDevProcess: devProcess !== null && !devProcess.killed,
|
|
1959
|
-
};
|
|
1960
|
-
break;
|
|
2138
|
+
}
|
|
1961
2139
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
} catch (e) {
|
|
2143
|
+
response.error = e.message;
|
|
2144
|
+
}
|
|
2145
|
+
break;
|
|
2146
|
+
case "UNDO_LAST":
|
|
2147
|
+
try {
|
|
2148
|
+
if (undoStack.length === 0) {
|
|
2149
|
+
response.error = "Nothing to undo \u2014 no file changes recorded in this session.";
|
|
2150
|
+
} else {
|
|
2151
|
+
const snapshot = undoStack.pop();
|
|
2152
|
+
if (snapshot.prevContent === null) {
|
|
2153
|
+
if (fs4.existsSync(snapshot.filePath)) {
|
|
2154
|
+
fs4.unlinkSync(snapshot.filePath);
|
|
2155
|
+
response.data = {
|
|
2156
|
+
success: true,
|
|
2157
|
+
undone: snapshot.operation,
|
|
2158
|
+
file: snapshot.relativePath,
|
|
2159
|
+
action: "deleted (was new file)",
|
|
2160
|
+
remaining: undoStack.length
|
|
2161
|
+
};
|
|
2162
|
+
console.log(chalk.yellow("\u21A9") + ` Undo: deleted ${snapshot.relativePath} (was a new file)`);
|
|
2163
|
+
} else {
|
|
2164
|
+
response.data = {
|
|
2165
|
+
success: true,
|
|
2166
|
+
undone: snapshot.operation,
|
|
2167
|
+
file: snapshot.relativePath,
|
|
2168
|
+
action: "already gone",
|
|
2169
|
+
remaining: undoStack.length
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
} else {
|
|
2173
|
+
fs4.writeFileSync(snapshot.filePath, snapshot.prevContent, "utf-8");
|
|
2174
|
+
response.data = {
|
|
2175
|
+
success: true,
|
|
2176
|
+
undone: snapshot.operation,
|
|
2177
|
+
file: snapshot.relativePath,
|
|
2178
|
+
action: "restored",
|
|
2179
|
+
remaining: undoStack.length
|
|
2180
|
+
};
|
|
2181
|
+
console.log(chalk.yellow("\u21A9") + ` Undo: restored ${snapshot.relativePath} (reverted ${snapshot.operation})`);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
} catch (e) {
|
|
2185
|
+
response.error = e.message;
|
|
2186
|
+
}
|
|
2187
|
+
break;
|
|
2188
|
+
case "DELETE_FILE":
|
|
2189
|
+
try {
|
|
2190
|
+
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2191
|
+
if (!filePath.startsWith(projectPath)) {
|
|
2192
|
+
response.error = "Access denied: Path outside project";
|
|
2193
|
+
break;
|
|
2194
|
+
}
|
|
2195
|
+
if (!fs4.existsSync(filePath)) {
|
|
2196
|
+
response.error = "File not found: " + message.filePath;
|
|
2197
|
+
break;
|
|
2198
|
+
}
|
|
2199
|
+
const relPath = message.filePath.replace(/\\/g, "/");
|
|
2200
|
+
const baseName = path4.basename(relPath);
|
|
2201
|
+
const PROTECTED_PATTERNS = [
|
|
2202
|
+
// Package & lock files
|
|
2203
|
+
"package.json",
|
|
2204
|
+
"package-lock.json",
|
|
2205
|
+
"yarn.lock",
|
|
2206
|
+
"pnpm-lock.yaml",
|
|
2207
|
+
"bun.lockb",
|
|
2208
|
+
"bun.lock",
|
|
2209
|
+
// TypeScript / JS config
|
|
2210
|
+
"tsconfig.json",
|
|
2211
|
+
"tsconfig.node.json",
|
|
2212
|
+
"jsconfig.json",
|
|
2213
|
+
// Framework configs
|
|
2214
|
+
/^next\.config\./,
|
|
2215
|
+
/^vite\.config\./,
|
|
2216
|
+
/^nuxt\.config\./,
|
|
2217
|
+
/^svelte\.config\./,
|
|
2218
|
+
/^remix\.config\./,
|
|
2219
|
+
/^astro\.config\./,
|
|
2220
|
+
/^webpack\.config\./,
|
|
2221
|
+
/^babel\.config\./,
|
|
2222
|
+
/^tailwind\.config\./,
|
|
2223
|
+
/^postcss\.config\./,
|
|
2224
|
+
/^prettier\.config\./,
|
|
2225
|
+
/^eslint\.config\./,
|
|
2226
|
+
".eslintrc",
|
|
2227
|
+
".prettierrc",
|
|
2228
|
+
".babelrc",
|
|
2229
|
+
// Env files
|
|
2230
|
+
".env",
|
|
2231
|
+
".env.local",
|
|
2232
|
+
".env.production",
|
|
2233
|
+
".env.development",
|
|
2234
|
+
".env.staging",
|
|
2235
|
+
// Next.js/React layout & entry roots
|
|
2236
|
+
/^layout\.(tsx|jsx|js|ts)$/,
|
|
2237
|
+
/^_app\.(tsx|jsx|js|ts)$/,
|
|
2238
|
+
/^_document\.(tsx|jsx|js|ts)$/,
|
|
2239
|
+
/^App\.(tsx|jsx|js|ts)$/,
|
|
2240
|
+
/^main\.(tsx|jsx|js|ts)$/,
|
|
2241
|
+
// Dockerfile & CI
|
|
2242
|
+
"Dockerfile",
|
|
2243
|
+
"docker-compose.yml",
|
|
2244
|
+
"docker-compose.yaml",
|
|
2245
|
+
".gitignore",
|
|
2246
|
+
".gitattributes"
|
|
2247
|
+
];
|
|
2248
|
+
const isProtected = PROTECTED_PATTERNS.some(
|
|
2249
|
+
(p) => typeof p === "string" ? baseName === p || relPath.endsWith("/" + p) : p.test(baseName)
|
|
2250
|
+
);
|
|
2251
|
+
if (isProtected) {
|
|
2252
|
+
response.error = `\u{1F6E1} Protected file: "${message.filePath}" cannot be deleted by the agent. This file is structurally critical to the project. To resolve conflicts involving this file, edit its contents instead of deleting it.`;
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
const trashDir = path4.join(projectPath, ".trace-trash");
|
|
2256
|
+
if (!fs4.existsSync(trashDir)) {
|
|
2257
|
+
fs4.mkdirSync(trashDir, { recursive: true });
|
|
2258
|
+
const gitignorePath = path4.join(projectPath, ".gitignore");
|
|
2259
|
+
if (fs4.existsSync(gitignorePath)) {
|
|
2260
|
+
const gi = fs4.readFileSync(gitignorePath, "utf-8");
|
|
2261
|
+
if (!gi.includes(".trace-trash")) {
|
|
2262
|
+
fs4.appendFileSync(gitignorePath, "\n# Trace soft-delete recovery folder\n.trace-trash/\n");
|
|
1968
2263
|
}
|
|
1969
|
-
|
|
1970
|
-
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
const timestamp = Date.now();
|
|
2267
|
+
const trashName = `${timestamp}_${baseName}`;
|
|
2268
|
+
const trashPath = path4.join(trashDir, trashName);
|
|
2269
|
+
const prevContent = fs4.readFileSync(filePath, "utf-8");
|
|
2270
|
+
undoStack.push({ filePath, prevContent, operation: "DELETE_FILE", timestamp, relativePath: message.filePath });
|
|
2271
|
+
if (undoStack.length > 20)
|
|
2272
|
+
undoStack.shift();
|
|
2273
|
+
fs4.renameSync(filePath, trashPath);
|
|
2274
|
+
response.data = {
|
|
2275
|
+
success: true,
|
|
2276
|
+
deleted: message.filePath,
|
|
2277
|
+
recoveryPath: path4.relative(projectPath, trashPath),
|
|
2278
|
+
undoAvailable: true,
|
|
2279
|
+
hint: "File moved to .trace-trash/ \u2014 recoverable even after CLI restart. Call UNDO_LAST to restore to original path."
|
|
2280
|
+
};
|
|
2281
|
+
console.log(chalk.yellow("\u{1F5D1}") + ` Soft-deleted: ${message.filePath} \u2192 .trace-trash/${trashName}` + chalk.dim(" [recoverable]"));
|
|
2282
|
+
} catch (e) {
|
|
2283
|
+
response.error = e.message;
|
|
2284
|
+
}
|
|
2285
|
+
break;
|
|
2286
|
+
case "RENAME_FILE":
|
|
2287
|
+
try {
|
|
2288
|
+
const oldPath = path4.resolve(projectPath, message.oldPath);
|
|
2289
|
+
const newPath = path4.resolve(projectPath, message.newPath);
|
|
2290
|
+
if (!oldPath.startsWith(projectPath) || !newPath.startsWith(projectPath)) {
|
|
2291
|
+
response.error = "Access denied: Path outside project";
|
|
2292
|
+
} else if (!fs4.existsSync(oldPath)) {
|
|
2293
|
+
response.error = "File not found: " + message.oldPath;
|
|
2294
|
+
} else if (fs4.existsSync(newPath)) {
|
|
2295
|
+
response.error = "Target already exists: " + message.newPath + ". Delete it first or choose a different name.";
|
|
2296
|
+
} else {
|
|
2297
|
+
const prevContent = fs4.readFileSync(oldPath, "utf-8");
|
|
2298
|
+
undoStack.push({ filePath: oldPath, prevContent, operation: "RENAME_FILE", timestamp: Date.now(), relativePath: message.oldPath });
|
|
2299
|
+
if (undoStack.length > 20)
|
|
2300
|
+
undoStack.shift();
|
|
2301
|
+
const destDir = path4.dirname(newPath);
|
|
2302
|
+
if (!fs4.existsSync(destDir))
|
|
2303
|
+
fs4.mkdirSync(destDir, { recursive: true });
|
|
2304
|
+
fs4.renameSync(oldPath, newPath);
|
|
2305
|
+
response.data = {
|
|
2306
|
+
success: true,
|
|
2307
|
+
oldPath: message.oldPath,
|
|
2308
|
+
newPath: message.newPath,
|
|
2309
|
+
undoAvailable: true
|
|
2310
|
+
};
|
|
2311
|
+
console.log(chalk.blue("\u2139") + ` Renamed: ${message.oldPath} \u2192 ${message.newPath}` + chalk.dim(" [undo saved]"));
|
|
2312
|
+
}
|
|
2313
|
+
} catch (e) {
|
|
2314
|
+
response.error = e.message;
|
|
2315
|
+
}
|
|
2316
|
+
break;
|
|
2317
|
+
case "GIT_BLAME":
|
|
2318
|
+
try {
|
|
2319
|
+
const filePath = message.filePath;
|
|
2320
|
+
const line = message.line;
|
|
2321
|
+
const { stdout } = await execAsync3(`git blame -L ${line},${line} --porcelain "${filePath}"`, { cwd: projectPath });
|
|
2322
|
+
const lines = stdout.split("\n");
|
|
2323
|
+
const commitHash = lines[0].split(" ")[0];
|
|
2324
|
+
const author = lines.find((l) => l.startsWith("author "))?.substring(7);
|
|
2325
|
+
const email = lines.find((l) => l.startsWith("author-mail "))?.substring(12);
|
|
2326
|
+
const date = lines.find((l) => l.startsWith("author-time "))?.substring(12);
|
|
2327
|
+
const summary = lines.find((l) => l.startsWith("summary "))?.substring(8);
|
|
2328
|
+
response.data = {
|
|
2329
|
+
commit: commitHash,
|
|
2330
|
+
author,
|
|
2331
|
+
email,
|
|
2332
|
+
date: new Date(parseInt(date) * 1e3).toISOString().split("T")[0],
|
|
2333
|
+
message: summary
|
|
2334
|
+
};
|
|
2335
|
+
} catch (e) {
|
|
2336
|
+
response.error = `Git blame failed: ${e.message}`;
|
|
2337
|
+
}
|
|
2338
|
+
break;
|
|
2339
|
+
case "GIT_RECENT_CHANGES":
|
|
2340
|
+
try {
|
|
2341
|
+
const filePath = message.filePath;
|
|
2342
|
+
const days = message.days || 7;
|
|
2343
|
+
const { stdout } = await execAsync3(`git log -n 10 --since="${days} days ago" --pretty=format:"%h|%an|%ad|%s" --date=short "${filePath}"`, { cwd: projectPath });
|
|
2344
|
+
response.data = {
|
|
2345
|
+
history: stdout.split("\n").filter(Boolean).map((line) => {
|
|
2346
|
+
const [hash, author, date, message2] = line.split("|");
|
|
2347
|
+
return { hash, author, date, message: message2 };
|
|
2348
|
+
})
|
|
2349
|
+
};
|
|
2350
|
+
} catch (e) {
|
|
2351
|
+
response.error = `Git log failed: ${e.message}`;
|
|
2352
|
+
}
|
|
2353
|
+
break;
|
|
2354
|
+
case "GET_IMPORTS":
|
|
2355
|
+
try {
|
|
2356
|
+
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2357
|
+
if (fs4.existsSync(filePath)) {
|
|
2358
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2359
|
+
const importRegex = /import\s+(?:[\w*\s{},]*)\s+from\s+['"]([^'"]+)['"]/g;
|
|
2360
|
+
const imports = [];
|
|
2361
|
+
let match;
|
|
2362
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
2363
|
+
imports.push(match[1]);
|
|
2364
|
+
}
|
|
2365
|
+
response.data = { imports };
|
|
2366
|
+
} else {
|
|
2367
|
+
response.error = "File not found";
|
|
1971
2368
|
}
|
|
1972
|
-
|
|
2369
|
+
} catch (e) {
|
|
2370
|
+
response.error = e.message;
|
|
2371
|
+
}
|
|
2372
|
+
break;
|
|
2373
|
+
case "FIND_USAGES":
|
|
2374
|
+
try {
|
|
2375
|
+
const query = message.query;
|
|
2376
|
+
const { stdout } = await execAsync3(`git grep -n "${query}"`, { cwd: projectPath });
|
|
2377
|
+
response.data = {
|
|
2378
|
+
usages: stdout.split("\n").filter(Boolean).slice(0, 20).map((line) => {
|
|
2379
|
+
const parts = line.split(":");
|
|
2380
|
+
return {
|
|
2381
|
+
file: parts[0],
|
|
2382
|
+
line: parseInt(parts[1]),
|
|
2383
|
+
content: parts.slice(2).join(":").trim()
|
|
2384
|
+
};
|
|
2385
|
+
})
|
|
2386
|
+
};
|
|
2387
|
+
} catch (e) {
|
|
2388
|
+
if (e.code === 1)
|
|
2389
|
+
response.data = { usages: [] };
|
|
2390
|
+
else
|
|
2391
|
+
response.error = `Grep failed: ${e.message}`;
|
|
2392
|
+
}
|
|
2393
|
+
break;
|
|
2394
|
+
case "GET_ENV_VARS":
|
|
2395
|
+
try {
|
|
2396
|
+
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2397
|
+
if (fs4.existsSync(filePath)) {
|
|
2398
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2399
|
+
const envRegex = /(?:process\.env\.|import\.meta\.env\.)([A-Z_][A-Z0-9_]*)/g;
|
|
2400
|
+
const vars = /* @__PURE__ */ new Set();
|
|
2401
|
+
let match;
|
|
2402
|
+
while ((match = envRegex.exec(content)) !== null) {
|
|
2403
|
+
vars.add(match[1]);
|
|
2404
|
+
}
|
|
2405
|
+
response.data = { envVars: Array.from(vars) };
|
|
2406
|
+
} else {
|
|
2407
|
+
response.error = "File not found";
|
|
2408
|
+
}
|
|
2409
|
+
} catch (e) {
|
|
2410
|
+
response.error = e.message;
|
|
2411
|
+
}
|
|
2412
|
+
break;
|
|
2413
|
+
case "GET_TERMINAL_BUFFER": {
|
|
2414
|
+
const lines = parseInt(message.lines) || 100;
|
|
2415
|
+
const buffer = globalTerminalBuffer.getLast(lines);
|
|
2416
|
+
response.data = {
|
|
2417
|
+
lines: buffer,
|
|
2418
|
+
totalLines: globalTerminalBuffer.length,
|
|
2419
|
+
hasDevProcess: devProcess !== null && !devProcess.killed
|
|
2420
|
+
};
|
|
2421
|
+
break;
|
|
1973
2422
|
}
|
|
1974
|
-
|
|
1975
|
-
|
|
2423
|
+
case "GET_DEV_STATUS": {
|
|
2424
|
+
response.data = {
|
|
2425
|
+
running: devProcess !== null && !devProcess.killed,
|
|
2426
|
+
pid: devProcess?.pid ?? null
|
|
2427
|
+
};
|
|
2428
|
+
break;
|
|
1976
2429
|
}
|
|
1977
|
-
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
.option('-p, --port <port>', 'WebSocket port', '8765')
|
|
1987
|
-
.action(async (options) => {
|
|
1988
|
-
const port = parseInt(options.port);
|
|
1989
|
-
const projectPath = process.cwd();
|
|
1990
|
-
console.log();
|
|
1991
|
-
console.log(chalk.bold.cyan('🔗 Trace IDE Bridge'));
|
|
1992
|
-
console.log(chalk.gray('─'.repeat(55)));
|
|
1993
|
-
console.log();
|
|
1994
|
-
console.log(`Project: ${chalk.green(projectPath)}`);
|
|
1995
|
-
console.log(`Port: ${chalk.cyan(port)}`);
|
|
1996
|
-
console.log();
|
|
2430
|
+
default:
|
|
2431
|
+
response.error = `Unknown message type: ${type}`;
|
|
2432
|
+
}
|
|
2433
|
+
ws.send(JSON.stringify(response));
|
|
2434
|
+
} catch (e) {
|
|
2435
|
+
console.error(chalk.red("Parse error:"), e.message);
|
|
2436
|
+
}
|
|
2437
|
+
});
|
|
2438
|
+
ws.on("message", (rawData) => {
|
|
1997
2439
|
try {
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
}
|
|
2440
|
+
const msg = JSON.parse(rawData.toString());
|
|
2441
|
+
if (msg.type === "RESPONSE" && msg.id && globalBrowserPending.has(msg.id)) {
|
|
2442
|
+
const { resolve: resolve3 } = globalBrowserPending.get(msg.id);
|
|
2443
|
+
globalBrowserPending.delete(msg.id);
|
|
2444
|
+
resolve3(msg.error ? { error: msg.error } : msg.data ?? {});
|
|
2445
|
+
}
|
|
2446
|
+
} catch {
|
|
2003
2447
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
program.command("connect", { isDefault: true }).alias("c").description('Start IDE bridge only (use "trace dev" to also start your dev server)').option("-p, --port <port>", "WebSocket port", "8765").action(async (options) => {
|
|
2451
|
+
const port = parseInt(options.port);
|
|
2452
|
+
const projectPath = process.cwd();
|
|
2453
|
+
console.log();
|
|
2454
|
+
console.log(chalk.bold.cyan("\u{1F517} Trace IDE Bridge"));
|
|
2455
|
+
console.log(chalk.gray("\u2500".repeat(55)));
|
|
2456
|
+
console.log();
|
|
2457
|
+
console.log(`Project: ${chalk.green(projectPath)}`);
|
|
2458
|
+
console.log(`Port: ${chalk.cyan(port)}`);
|
|
2459
|
+
console.log();
|
|
2460
|
+
try {
|
|
2461
|
+
const pkgPath = path4.join(projectPath, "package.json");
|
|
2462
|
+
if (fs4.existsSync(pkgPath)) {
|
|
2463
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
2464
|
+
console.log(`\u{1F4E6} Package: ${chalk.yellow(pkg.name)} v${pkg.version}`);
|
|
2465
|
+
}
|
|
2466
|
+
} catch (_) {
|
|
2467
|
+
}
|
|
2468
|
+
const wss = new WebSocketServer({ port });
|
|
2469
|
+
let clientCount = 0;
|
|
2470
|
+
console.log();
|
|
2471
|
+
console.log(chalk.green("\u2713") + " WebSocket server started");
|
|
2472
|
+
console.log(chalk.dim("Waiting for extension to connect..."));
|
|
2473
|
+
console.log();
|
|
2474
|
+
console.log(chalk.gray("\u2500".repeat(55)));
|
|
2475
|
+
console.log(chalk.dim("Press Ctrl+C to stop"));
|
|
2476
|
+
console.log();
|
|
2477
|
+
wss.on("connection", (ws) => {
|
|
2478
|
+
clientCount++;
|
|
2479
|
+
console.log(chalk.green("\u25CF") + ` Extension connected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
2480
|
+
attachMessageHandler(ws, projectPath);
|
|
2481
|
+
ws.on("close", () => {
|
|
2482
|
+
clientCount--;
|
|
2483
|
+
console.log(chalk.yellow("\u25CF") + ` Extension disconnected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
2036
2484
|
});
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
console.log(chalk.dim('Stopping...'));
|
|
2040
|
-
wss.close();
|
|
2041
|
-
process.exit(0);
|
|
2485
|
+
ws.on("error", (error) => {
|
|
2486
|
+
console.error(chalk.red("WebSocket error:"), error.message);
|
|
2042
2487
|
});
|
|
2488
|
+
});
|
|
2489
|
+
wss.on("error", (error) => {
|
|
2490
|
+
if (error.code === "EADDRINUSE") {
|
|
2491
|
+
console.log(chalk.red(`\u2717 Port ${port} is already in use`));
|
|
2492
|
+
console.log(chalk.dim("Try: trace-connect --port 8766"));
|
|
2493
|
+
} else {
|
|
2494
|
+
console.error(chalk.red("Server error:"), error.message);
|
|
2495
|
+
}
|
|
2496
|
+
process.exit(1);
|
|
2497
|
+
});
|
|
2498
|
+
process.on("SIGINT", () => {
|
|
2499
|
+
console.log();
|
|
2500
|
+
console.log(chalk.dim("Stopping..."));
|
|
2501
|
+
wss.close();
|
|
2502
|
+
process.exit(0);
|
|
2503
|
+
});
|
|
2043
2504
|
});
|
|
2044
2505
|
program.parse(process.argv);
|
|
2045
|
-
// Helper: Get file tree
|
|
2046
2506
|
function getFileTree(dir, depth, currentDepth = 0) {
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
result[item] = 'file';
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
catch (e) {
|
|
2067
|
-
// Skip inaccessible
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
catch (e) {
|
|
2072
|
-
// Skip inaccessible dirs
|
|
2073
|
-
}
|
|
2074
|
-
return result;
|
|
2075
|
-
}
|
|
2076
|
-
// Helper: Simple search in files
|
|
2077
|
-
function searchInFiles(dir, query, maxResults) {
|
|
2078
|
-
const results = [];
|
|
2079
|
-
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte', '.css', '.html', '.json'];
|
|
2080
|
-
const ignorePatterns = ['node_modules', '.git', 'dist', 'build', '.next', '.cache'];
|
|
2081
|
-
function searchDir(currentDir) {
|
|
2082
|
-
if (results.length >= maxResults)
|
|
2083
|
-
return;
|
|
2084
|
-
try {
|
|
2085
|
-
const items = fs.readdirSync(currentDir);
|
|
2086
|
-
for (const item of items) {
|
|
2087
|
-
if (results.length >= maxResults)
|
|
2088
|
-
return;
|
|
2089
|
-
if (ignorePatterns.includes(item) || item.startsWith('.'))
|
|
2090
|
-
continue;
|
|
2091
|
-
const fullPath = path.join(currentDir, item);
|
|
2092
|
-
try {
|
|
2093
|
-
const stat = fs.statSync(fullPath);
|
|
2094
|
-
if (stat.isDirectory()) {
|
|
2095
|
-
searchDir(fullPath);
|
|
2096
|
-
}
|
|
2097
|
-
else if (extensions.some(ext => item.endsWith(ext))) {
|
|
2098
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
2099
|
-
const lines = content.split('\n');
|
|
2100
|
-
for (let i = 0; i < lines.length && results.length < maxResults; i++) {
|
|
2101
|
-
if (lines[i].includes(query)) {
|
|
2102
|
-
results.push({
|
|
2103
|
-
file: path.relative(dir, fullPath),
|
|
2104
|
-
line: i + 1,
|
|
2105
|
-
content: lines[i].trim().substring(0, 200)
|
|
2106
|
-
});
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
catch (e) {
|
|
2112
|
-
// ignore
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
catch (e) {
|
|
2117
|
-
// ignore
|
|
2507
|
+
if (currentDepth >= depth)
|
|
2508
|
+
return null;
|
|
2509
|
+
const result = {};
|
|
2510
|
+
const ignorePatterns = ["node_modules", ".git", "dist", "build", ".next", "coverage", ".cache"];
|
|
2511
|
+
try {
|
|
2512
|
+
const items = fs4.readdirSync(dir);
|
|
2513
|
+
for (const item of items.slice(0, 50)) {
|
|
2514
|
+
if (ignorePatterns.includes(item) || item.startsWith("."))
|
|
2515
|
+
continue;
|
|
2516
|
+
const fullPath = path4.join(dir, item);
|
|
2517
|
+
try {
|
|
2518
|
+
const stat = fs4.statSync(fullPath);
|
|
2519
|
+
if (stat.isDirectory()) {
|
|
2520
|
+
result[item] = getFileTree(fullPath, depth, currentDepth + 1);
|
|
2521
|
+
} else {
|
|
2522
|
+
result[item] = "file";
|
|
2118
2523
|
}
|
|
2524
|
+
} catch (e) {
|
|
2525
|
+
}
|
|
2119
2526
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2527
|
+
} catch (e) {
|
|
2528
|
+
}
|
|
2529
|
+
return result;
|
|
2122
2530
|
}
|
|
2123
|
-
//# sourceMappingURL=index.js.map
|