@gettrace/cli 1.4.24 → 2.0.1
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 +1039 -177
- package/package.json +3 -4
package/dist/index.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { program } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import chalk2 from "chalk";
|
|
6
6
|
import { WebSocketServer, WebSocket } from "ws";
|
|
7
7
|
import * as fs4 from "fs";
|
|
8
8
|
import * as path4 from "path";
|
|
9
|
-
import { exec as
|
|
9
|
+
import { exec, execFile as execFile3, spawn as spawn2 } from "child_process";
|
|
10
10
|
import { promisify as promisify3 } from "util";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
|
|
@@ -31,9 +31,9 @@ async function withFileLock(filePath, fn) {
|
|
|
31
31
|
// src/format.ts
|
|
32
32
|
import * as fs from "fs";
|
|
33
33
|
import * as path from "path";
|
|
34
|
-
import {
|
|
34
|
+
import { execFile } from "child_process";
|
|
35
35
|
import { promisify } from "util";
|
|
36
|
-
var
|
|
36
|
+
var execFileAsync = promisify(execFile);
|
|
37
37
|
var JS_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
38
38
|
var CSS_EXTS = /* @__PURE__ */ new Set([".css", ".scss", ".less"]);
|
|
39
39
|
var HTML_EXTS = /* @__PURE__ */ new Set([".html", ".htm", ".vue", ".svelte"]);
|
|
@@ -53,7 +53,7 @@ async function autoFormat(filePath, projectPath) {
|
|
|
53
53
|
const prettierBin = path.join(projectPath, "node_modules", ".bin", "prettier");
|
|
54
54
|
if (fs.existsSync(prettierBin)) {
|
|
55
55
|
try {
|
|
56
|
-
await
|
|
56
|
+
await execFileAsync(prettierBin, ["--write", filePath], { cwd: projectPath });
|
|
57
57
|
return "prettier";
|
|
58
58
|
} catch {
|
|
59
59
|
}
|
|
@@ -62,7 +62,7 @@ async function autoFormat(filePath, projectPath) {
|
|
|
62
62
|
const eslintBin = path.join(projectPath, "node_modules", ".bin", "eslint");
|
|
63
63
|
if (fs.existsSync(eslintBin)) {
|
|
64
64
|
try {
|
|
65
|
-
await
|
|
65
|
+
await execFileAsync(eslintBin, ["--fix", filePath], { cwd: projectPath });
|
|
66
66
|
return "eslint";
|
|
67
67
|
} catch {
|
|
68
68
|
}
|
|
@@ -74,7 +74,7 @@ async function autoFormat(filePath, projectPath) {
|
|
|
74
74
|
// src/lsp.ts
|
|
75
75
|
import * as fs2 from "fs";
|
|
76
76
|
import * as path2 from "path";
|
|
77
|
-
import {
|
|
77
|
+
import { spawn } from "child_process";
|
|
78
78
|
var TS_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
79
79
|
var JS_EXTS2 = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs"]);
|
|
80
80
|
var CSS_EXTS2 = /* @__PURE__ */ new Set([".css", ".scss", ".less", ".sass"]);
|
|
@@ -145,10 +145,9 @@ async function checkDiagnostics(filePath, projectPath) {
|
|
|
145
145
|
}
|
|
146
146
|
async function runTsc(projectPath) {
|
|
147
147
|
const localTsc = path2.join(projectPath, "node_modules", ".bin", "tsc");
|
|
148
|
-
const bin = fs2.existsSync(localTsc) ?
|
|
149
|
-
const cmd = `${bin} --noEmit --pretty false`;
|
|
148
|
+
const bin = fs2.existsSync(localTsc) ? localTsc : "tsc";
|
|
150
149
|
try {
|
|
151
|
-
const raw = await spawnWithTimeout(
|
|
150
|
+
const raw = await spawnWithTimeout(bin, ["--noEmit", "--pretty", "false"], projectPath);
|
|
152
151
|
return parseTscOutput(raw, projectPath);
|
|
153
152
|
} catch {
|
|
154
153
|
return [];
|
|
@@ -174,9 +173,8 @@ function parseTscOutput(output, projectPath) {
|
|
|
174
173
|
return diagnostics;
|
|
175
174
|
}
|
|
176
175
|
async function runEslint(eslintBin, filePath, projectPath) {
|
|
177
|
-
const cmd = `"${eslintBin}" --format json "${filePath}"`;
|
|
178
176
|
try {
|
|
179
|
-
const raw = await spawnWithTimeout(
|
|
177
|
+
const raw = await spawnWithTimeout(eslintBin, ["--format", "json", filePath], projectPath);
|
|
180
178
|
return parseEslintOutput(raw, projectPath);
|
|
181
179
|
} catch {
|
|
182
180
|
return [];
|
|
@@ -210,9 +208,8 @@ function parseEslintOutput(raw, projectPath) {
|
|
|
210
208
|
return diagnostics;
|
|
211
209
|
}
|
|
212
210
|
async function runStylelint(stylelintBin, filePath, projectPath) {
|
|
213
|
-
const cmd = `"${stylelintBin}" --formatter json "${filePath}"`;
|
|
214
211
|
try {
|
|
215
|
-
const raw = await spawnWithTimeout(
|
|
212
|
+
const raw = await spawnWithTimeout(stylelintBin, ["--formatter", "json", filePath], projectPath);
|
|
216
213
|
return parseStylelintOutput(raw, projectPath);
|
|
217
214
|
} catch {
|
|
218
215
|
return [];
|
|
@@ -243,17 +240,36 @@ function parseStylelintOutput(raw, projectPath) {
|
|
|
243
240
|
}
|
|
244
241
|
return diagnostics;
|
|
245
242
|
}
|
|
246
|
-
function spawnWithTimeout(
|
|
243
|
+
function spawnWithTimeout(bin, args, cwd) {
|
|
247
244
|
return new Promise((resolve3, reject) => {
|
|
248
245
|
let settled = false;
|
|
246
|
+
let stdout = "";
|
|
247
|
+
let stderr = "";
|
|
248
|
+
const proc = spawn(bin, args, { cwd, shell: false });
|
|
249
249
|
const timer = setTimeout(() => {
|
|
250
250
|
if (settled)
|
|
251
251
|
return;
|
|
252
252
|
settled = true;
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
try {
|
|
254
|
+
proc.kill("SIGTERM");
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
reject(new Error(`Checker timed out: ${bin} ${args.slice(0, 2).join(" ")}`));
|
|
255
258
|
}, CHECKER_TIMEOUT_MS);
|
|
256
|
-
|
|
259
|
+
proc.stdout?.on("data", (d) => {
|
|
260
|
+
stdout += d.toString("utf-8");
|
|
261
|
+
});
|
|
262
|
+
proc.stderr?.on("data", (d) => {
|
|
263
|
+
stderr += d.toString("utf-8");
|
|
264
|
+
});
|
|
265
|
+
proc.on("error", () => {
|
|
266
|
+
if (settled)
|
|
267
|
+
return;
|
|
268
|
+
settled = true;
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
resolve3("");
|
|
271
|
+
});
|
|
272
|
+
proc.on("close", () => {
|
|
257
273
|
if (settled)
|
|
258
274
|
return;
|
|
259
275
|
settled = true;
|
|
@@ -274,14 +290,14 @@ function tsconfigAllowsJs(tsconfigPath) {
|
|
|
274
290
|
// src/search.ts
|
|
275
291
|
import * as fs3 from "fs";
|
|
276
292
|
import * as path3 from "path";
|
|
277
|
-
import {
|
|
293
|
+
import { execFile as execFile2 } from "child_process";
|
|
278
294
|
import { promisify as promisify2 } from "util";
|
|
279
|
-
var
|
|
295
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
280
296
|
async function searchWithRipgrep(projectPath, query, opts) {
|
|
281
|
-
const
|
|
297
|
+
const args = [
|
|
282
298
|
"--json",
|
|
283
299
|
"--line-number",
|
|
284
|
-
opts.isRegex ?
|
|
300
|
+
...opts.isRegex ? [] : ["--fixed-strings"],
|
|
285
301
|
opts.caseSensitive ? "--case-sensitive" : "--ignore-case",
|
|
286
302
|
`--max-count=${opts.maxResults}`,
|
|
287
303
|
// Exclude directories that are never source code
|
|
@@ -300,11 +316,13 @@ async function searchWithRipgrep(projectPath, query, opts) {
|
|
|
300
316
|
"--glob",
|
|
301
317
|
"!.cache/**",
|
|
302
318
|
"--glob",
|
|
303
|
-
"!.turbo/**"
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
319
|
+
"!.turbo/**",
|
|
320
|
+
// `--` ends flags so a query that starts with `-` doesn't get parsed
|
|
321
|
+
// as a flag by ripgrep itself.
|
|
322
|
+
"--",
|
|
323
|
+
query
|
|
324
|
+
];
|
|
325
|
+
const { stdout } = await execFileAsync2("rg", args, {
|
|
308
326
|
cwd: projectPath,
|
|
309
327
|
maxBuffer: 10 * 1024 * 1024
|
|
310
328
|
// 10 MB — generous for large codebases
|
|
@@ -539,11 +557,455 @@ function _extractClassNameString(value) {
|
|
|
539
557
|
return null;
|
|
540
558
|
}
|
|
541
559
|
|
|
560
|
+
// src/tui.ts
|
|
561
|
+
import chalk from "chalk";
|
|
562
|
+
import * as readline from "readline";
|
|
563
|
+
var CSI = "\x1B[";
|
|
564
|
+
var AGENT_NOISE = [
|
|
565
|
+
/service=tool\.registry/,
|
|
566
|
+
/service=permission .* (evaluate|evaluated)$/,
|
|
567
|
+
/service=permission permission=skill/,
|
|
568
|
+
/service=bus type=.* (subscribing|publishing)/,
|
|
569
|
+
/service=server method=GET path=\/(session|global\/event|event)/,
|
|
570
|
+
/service=server status=(started|completed) .* path=\/(session|global\/event|event)/,
|
|
571
|
+
/service=server status=(started|completed) duration=\d+ method=GET/,
|
|
572
|
+
/service=lsp\.client .* path=/,
|
|
573
|
+
/service=lsp\.server tsserver=/,
|
|
574
|
+
/service=lsp\.server (downloading|removing|building)/,
|
|
575
|
+
/service=lsp file=/,
|
|
576
|
+
/service=snapshot hash=.* tracking/,
|
|
577
|
+
/service=session\.prompt .* resolveTools/,
|
|
578
|
+
/service=file\.time/,
|
|
579
|
+
/service=session\.compaction/,
|
|
580
|
+
/service=session\.processor/,
|
|
581
|
+
/service=plugin name=.* loading internal plugin/,
|
|
582
|
+
/service=provider .* (using bundled provider|getSDK)/,
|
|
583
|
+
/service=db count=\d+ mode=bundled applying migrations/,
|
|
584
|
+
/service=db path=.* opening database/,
|
|
585
|
+
/service=format /,
|
|
586
|
+
/service=bash-tool /,
|
|
587
|
+
/service=server-proxy/,
|
|
588
|
+
/service=project .* fromDirectory/,
|
|
589
|
+
/service=file init/,
|
|
590
|
+
/service=lsp serverIds=/,
|
|
591
|
+
/service=vcs branch=.* initialized/,
|
|
592
|
+
/service=mcp key=.* found$/,
|
|
593
|
+
/service=mcp .* startup failed/
|
|
594
|
+
// pencil mcp ENOENT — pre-existing
|
|
595
|
+
];
|
|
596
|
+
var TraceTUI = class {
|
|
597
|
+
currentView = "all";
|
|
598
|
+
logs = [];
|
|
599
|
+
status;
|
|
600
|
+
originalStdoutWrite = null;
|
|
601
|
+
originalStderrWrite = null;
|
|
602
|
+
headerHeight = 0;
|
|
603
|
+
// calculated when drawn
|
|
604
|
+
tabBarRow = 0;
|
|
605
|
+
scrollTop = 0;
|
|
606
|
+
// first row of the scroll region (1-indexed)
|
|
607
|
+
terminalRows = process.stdout.rows || 30;
|
|
608
|
+
terminalCols = process.stdout.columns || 100;
|
|
609
|
+
resizeHandler = null;
|
|
610
|
+
keyHandler = null;
|
|
611
|
+
rl = null;
|
|
612
|
+
isShutdown = false;
|
|
613
|
+
/** Max number of buffered log lines (keeps memory bounded). */
|
|
614
|
+
MAX_LOGS = 5e3;
|
|
615
|
+
/** Max number of agent lines per second to display (cheap rate-limit). */
|
|
616
|
+
agentRateBucket = 0;
|
|
617
|
+
agentRateResetAt = 0;
|
|
618
|
+
agentDroppedThisSec = 0;
|
|
619
|
+
AGENT_RATE_PER_SEC = 60;
|
|
620
|
+
/**
|
|
621
|
+
* Per-source partial-line buffer. Node streams hand us chunks that are
|
|
622
|
+
* sliced at arbitrary byte boundaries, NOT at line boundaries — a single
|
|
623
|
+
* logical line can arrive split across two chunks. Without buffering,
|
|
624
|
+
* we'd render the half-lines as if they were complete, producing the
|
|
625
|
+
* mangled "warning … truncatedry for more info" output the user saw.
|
|
626
|
+
* Each source gets its own buffer so app/agent/trace streams don't mix.
|
|
627
|
+
*/
|
|
628
|
+
partialBuffer = /* @__PURE__ */ new Map();
|
|
629
|
+
constructor(status) {
|
|
630
|
+
this.status = status;
|
|
631
|
+
}
|
|
632
|
+
/** Activate the TUI: install interceptors, draw chrome, capture keys. */
|
|
633
|
+
start() {
|
|
634
|
+
this.installInterceptors();
|
|
635
|
+
this.installResizeHandler();
|
|
636
|
+
this.installKeyHandler();
|
|
637
|
+
this.render();
|
|
638
|
+
}
|
|
639
|
+
/** Restore terminal state. Idempotent. */
|
|
640
|
+
shutdown() {
|
|
641
|
+
if (this.isShutdown)
|
|
642
|
+
return;
|
|
643
|
+
for (const [source, buf] of this.partialBuffer) {
|
|
644
|
+
if (buf)
|
|
645
|
+
this.pushLine(source, buf);
|
|
646
|
+
}
|
|
647
|
+
this.partialBuffer.clear();
|
|
648
|
+
this.isShutdown = true;
|
|
649
|
+
if (this.originalStdoutWrite) {
|
|
650
|
+
process.stdout.write = this.originalStdoutWrite;
|
|
651
|
+
this.originalStdoutWrite = null;
|
|
652
|
+
}
|
|
653
|
+
if (this.originalStderrWrite) {
|
|
654
|
+
process.stderr.write = this.originalStderrWrite;
|
|
655
|
+
this.originalStderrWrite = null;
|
|
656
|
+
}
|
|
657
|
+
if (this.resizeHandler) {
|
|
658
|
+
process.stdout.removeListener("resize", this.resizeHandler);
|
|
659
|
+
this.resizeHandler = null;
|
|
660
|
+
}
|
|
661
|
+
if (this.rl) {
|
|
662
|
+
try {
|
|
663
|
+
this.rl.close();
|
|
664
|
+
} catch {
|
|
665
|
+
}
|
|
666
|
+
this.rl = null;
|
|
667
|
+
}
|
|
668
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
669
|
+
try {
|
|
670
|
+
process.stdin.setRawMode(false);
|
|
671
|
+
} catch {
|
|
672
|
+
}
|
|
673
|
+
process.stdin.pause();
|
|
674
|
+
}
|
|
675
|
+
process.stdout.write(`${CSI}r${CSI}?25h${CSI}${this.terminalRows};1H
|
|
676
|
+
`);
|
|
677
|
+
}
|
|
678
|
+
/** Push a line that came from the user's dev server.
|
|
679
|
+
*
|
|
680
|
+
* Stream data — Node hands us chunks at arbitrary byte boundaries, so
|
|
681
|
+
* we run them through the per-source partial-line buffer to reassemble
|
|
682
|
+
* complete lines before display. */
|
|
683
|
+
pushApp(text) {
|
|
684
|
+
this.pushChunk("app", text);
|
|
685
|
+
}
|
|
686
|
+
/** Push a CLI status / banner message.
|
|
687
|
+
*
|
|
688
|
+
* Internal messages are ALWAYS complete logical lines — `chalk(...)
|
|
689
|
+
* + chalk(...)` style concatenations, called once per event. They do
|
|
690
|
+
* NOT go through the chunk buffer; otherwise messages without a
|
|
691
|
+
* trailing \n accumulate forever and only flush when some other source
|
|
692
|
+
* produces a newline, glueing many messages onto one rendered row. */
|
|
693
|
+
pushTrace(line) {
|
|
694
|
+
const clean = line.replace(/\n+$/, "");
|
|
695
|
+
for (const part of clean.split("\n")) {
|
|
696
|
+
this.pushLine("trace", part);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/** Update status fields and redraw the header in place. */
|
|
700
|
+
setStatus(updates) {
|
|
701
|
+
Object.assign(this.status, updates);
|
|
702
|
+
if (!this.isShutdown)
|
|
703
|
+
this.drawHeader();
|
|
704
|
+
}
|
|
705
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
706
|
+
// Internal: input handling
|
|
707
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
708
|
+
installKeyHandler() {
|
|
709
|
+
if (!process.stdin.isTTY)
|
|
710
|
+
return;
|
|
711
|
+
readline.emitKeypressEvents(process.stdin);
|
|
712
|
+
if (process.stdin.setRawMode)
|
|
713
|
+
process.stdin.setRawMode(true);
|
|
714
|
+
process.stdin.resume();
|
|
715
|
+
this.keyHandler = (_str, key) => {
|
|
716
|
+
if (!key)
|
|
717
|
+
return;
|
|
718
|
+
if (key.ctrl && key.name === "c") {
|
|
719
|
+
process.kill(process.pid, "SIGINT");
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (key.name === "q" && !key.ctrl && !key.meta) {
|
|
723
|
+
process.kill(process.pid, "SIGINT");
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (key.name === "1")
|
|
727
|
+
return this.setView("app");
|
|
728
|
+
if (key.name === "2")
|
|
729
|
+
return this.setView("agent");
|
|
730
|
+
if (key.name === "3")
|
|
731
|
+
return this.setView("all");
|
|
732
|
+
if (key.name === "c" && !key.ctrl && !key.meta) {
|
|
733
|
+
this.clearLogArea();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
process.stdin.on("keypress", this.keyHandler);
|
|
738
|
+
}
|
|
739
|
+
installResizeHandler() {
|
|
740
|
+
this.resizeHandler = () => {
|
|
741
|
+
this.terminalRows = process.stdout.rows || 30;
|
|
742
|
+
this.terminalCols = process.stdout.columns || 100;
|
|
743
|
+
this.render();
|
|
744
|
+
};
|
|
745
|
+
process.stdout.on("resize", this.resizeHandler);
|
|
746
|
+
}
|
|
747
|
+
setView(view) {
|
|
748
|
+
if (this.currentView === view)
|
|
749
|
+
return;
|
|
750
|
+
this.currentView = view;
|
|
751
|
+
this.drawTabBar();
|
|
752
|
+
this.replayLogs();
|
|
753
|
+
}
|
|
754
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
755
|
+
// Internal: stdout/stderr interception
|
|
756
|
+
//
|
|
757
|
+
// The trace-agent module is loaded into THIS process via dynamic import
|
|
758
|
+
// and writes its logs straight to stdout. We replace process.stdout.write
|
|
759
|
+
// so those lines flow through pushChunk("agent", ...) and get filtered
|
|
760
|
+
// + tagged before display.
|
|
761
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
762
|
+
installInterceptors() {
|
|
763
|
+
this.originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
764
|
+
this.originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
765
|
+
const intercept = (orig) => (chunk, encoding, cb) => {
|
|
766
|
+
const text = typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString(
|
|
767
|
+
typeof encoding === "string" ? encoding : "utf8"
|
|
768
|
+
) : String(chunk);
|
|
769
|
+
const trimmed = text.trimStart();
|
|
770
|
+
const looksLikeAgent = /^(INFO |WARN |ERROR|DEBUG)\s+\d{4}-\d{2}-\d{2}T/.test(trimmed);
|
|
771
|
+
const source = looksLikeAgent ? "agent" : "trace";
|
|
772
|
+
this.pushChunk(source, text);
|
|
773
|
+
if (typeof encoding === "function")
|
|
774
|
+
encoding();
|
|
775
|
+
else if (typeof cb === "function")
|
|
776
|
+
cb();
|
|
777
|
+
return true;
|
|
778
|
+
};
|
|
779
|
+
process.stdout.write = intercept(this.originalStdoutWrite);
|
|
780
|
+
process.stderr.write = intercept(this.originalStderrWrite);
|
|
781
|
+
}
|
|
782
|
+
pushChunk(source, chunk) {
|
|
783
|
+
const prev = this.partialBuffer.get(source) ?? "";
|
|
784
|
+
const full = prev + chunk;
|
|
785
|
+
const parts = full.split("\n");
|
|
786
|
+
const trailing = parts.pop() ?? "";
|
|
787
|
+
this.partialBuffer.set(source, trailing);
|
|
788
|
+
for (const line of parts)
|
|
789
|
+
this.pushLine(source, line);
|
|
790
|
+
}
|
|
791
|
+
pushLine(source, line) {
|
|
792
|
+
if (source === "agent" && this.isNoise(line))
|
|
793
|
+
return;
|
|
794
|
+
if (source === "agent") {
|
|
795
|
+
const now = Date.now();
|
|
796
|
+
if (now > this.agentRateResetAt) {
|
|
797
|
+
if (this.agentDroppedThisSec > 0) {
|
|
798
|
+
this.appendDisplayLine(this.formatLine({
|
|
799
|
+
source: "trace",
|
|
800
|
+
time: now,
|
|
801
|
+
text: chalk.dim(`(suppressed ${this.agentDroppedThisSec} agent lines this second)`)
|
|
802
|
+
}));
|
|
803
|
+
this.agentDroppedThisSec = 0;
|
|
804
|
+
}
|
|
805
|
+
this.agentRateBucket = 0;
|
|
806
|
+
this.agentRateResetAt = now + 1e3;
|
|
807
|
+
}
|
|
808
|
+
if (++this.agentRateBucket > this.AGENT_RATE_PER_SEC) {
|
|
809
|
+
this.agentDroppedThisSec++;
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const entry = { source, text: line, time: Date.now() };
|
|
814
|
+
this.logs.push(entry);
|
|
815
|
+
if (this.logs.length > this.MAX_LOGS)
|
|
816
|
+
this.logs.shift();
|
|
817
|
+
if (this.matchesView(source)) {
|
|
818
|
+
this.appendDisplayLine(this.formatLine(entry));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
isNoise(line) {
|
|
822
|
+
if (!line)
|
|
823
|
+
return true;
|
|
824
|
+
return AGENT_NOISE.some((re) => re.test(line));
|
|
825
|
+
}
|
|
826
|
+
matchesView(source) {
|
|
827
|
+
if (this.currentView === "all")
|
|
828
|
+
return true;
|
|
829
|
+
if (this.currentView === "app")
|
|
830
|
+
return source === "app";
|
|
831
|
+
if (this.currentView === "agent")
|
|
832
|
+
return source !== "app";
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
formatLine(entry) {
|
|
836
|
+
const tag = (() => {
|
|
837
|
+
switch (entry.source) {
|
|
838
|
+
case "app":
|
|
839
|
+
return chalk.cyan("app ");
|
|
840
|
+
case "agent":
|
|
841
|
+
return chalk.magenta("agent ");
|
|
842
|
+
case "trace":
|
|
843
|
+
return chalk.yellow("trace ");
|
|
844
|
+
}
|
|
845
|
+
})();
|
|
846
|
+
const sep2 = chalk.dim("\u2502");
|
|
847
|
+
return ` ${tag}${sep2} ${entry.text}`;
|
|
848
|
+
}
|
|
849
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
850
|
+
// Internal: drawing
|
|
851
|
+
//
|
|
852
|
+
// We keep header + tab bar fixed at the top via a DECSTBM scroll region.
|
|
853
|
+
// After the header is drawn, the cursor is parked at the bottom of the
|
|
854
|
+
// scroll region; logs land there and scroll up naturally.
|
|
855
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
856
|
+
/** Full re-render: header + tab bar + scroll region + replay. */
|
|
857
|
+
render() {
|
|
858
|
+
if (this.isShutdown)
|
|
859
|
+
return;
|
|
860
|
+
this.write(`${CSI}2J${CSI}H${CSI}?25l`);
|
|
861
|
+
this.drawHeader();
|
|
862
|
+
this.drawTabBar();
|
|
863
|
+
this.installScrollRegion();
|
|
864
|
+
this.replayLogs();
|
|
865
|
+
}
|
|
866
|
+
drawHeader() {
|
|
867
|
+
this.write(`${CSI}s`);
|
|
868
|
+
this.write(`${CSI}H`);
|
|
869
|
+
const cyan = chalk.cyan;
|
|
870
|
+
const cyanBold = chalk.bold.cyan;
|
|
871
|
+
const dim = chalk.dim;
|
|
872
|
+
const green = chalk.green;
|
|
873
|
+
const red = chalk.red;
|
|
874
|
+
const yellow = chalk.yellow;
|
|
875
|
+
const mark = [
|
|
876
|
+
" \u2572 \u2571 ",
|
|
877
|
+
" \u2572 \u2571 ",
|
|
878
|
+
" \u2572___ ___\u2571 ",
|
|
879
|
+
" \u25CF ",
|
|
880
|
+
" \u2503 \u2503 ",
|
|
881
|
+
" \u2503 \u2503 "
|
|
882
|
+
];
|
|
883
|
+
const wordmark = "T R A C E";
|
|
884
|
+
const lines = [];
|
|
885
|
+
lines.push("");
|
|
886
|
+
for (const row of mark)
|
|
887
|
+
lines.push(" " + cyan(row));
|
|
888
|
+
lines.push("");
|
|
889
|
+
lines.push(" " + cyanBold(wordmark) + " " + dim("v" + this.status.cliVersion));
|
|
890
|
+
lines.push("");
|
|
891
|
+
lines.push(" " + dim("\u2500".repeat(Math.max(40, this.terminalCols - 4))));
|
|
892
|
+
lines.push("");
|
|
893
|
+
const dot = (ok) => ok ? green("\u25CF") : red("\u25CF");
|
|
894
|
+
const labelW = 9;
|
|
895
|
+
const fmt = (label, value) => " " + dim(label.padEnd(labelW)) + value;
|
|
896
|
+
const ext = this.status.extConnected;
|
|
897
|
+
const bridgeStatus = !this.status.bridgeReady ? `${yellow("\u25CB")} starting` : ext ? `${green("\u25CF")} connected ${dim("\xB7")} ${this.status.clientCount} ` + dim(`client${this.status.clientCount === 1 ? "" : "s"}`) : `${dim("\u25CC")} ${dim("ready \xB7 waiting for extension")}`;
|
|
898
|
+
const agentStatus = !this.status.agentReady ? `${yellow("\u25CB")} starting` : ext ? `${green("\u25CF")} connected` + (this.status.agentExtras ? dim(` ${this.status.agentExtras}`) : "") : `${dim("\u25CC")} ${dim("ready \xB7 waiting for extension")}` + (this.status.agentExtras ? dim(` ${this.status.agentExtras}`) : "");
|
|
899
|
+
const devStatus = !this.status.devReady ? `${yellow("\u25CB")} starting` : `${green("\u25CF")} ready`;
|
|
900
|
+
const projectShort = this.truncate(this.status.project, this.terminalCols - 18);
|
|
901
|
+
const cmdLine = `${this.status.devCommand} ${devStatus}`;
|
|
902
|
+
const bridgeLine = `ws://localhost:${this.status.bridgePort} ${bridgeStatus}`;
|
|
903
|
+
const agentLine = `http://localhost:${this.status.agentPort} ${agentStatus}`;
|
|
904
|
+
const browserLine = `http://localhost:${this.status.browserPort} ${dim("for coding-agent CDP queries")}`;
|
|
905
|
+
lines.push(fmt("project", projectShort));
|
|
906
|
+
lines.push(fmt("command", cmdLine));
|
|
907
|
+
lines.push(fmt("bridge", bridgeLine));
|
|
908
|
+
lines.push(fmt("agent", agentLine));
|
|
909
|
+
lines.push(fmt("browser", browserLine));
|
|
910
|
+
lines.push("");
|
|
911
|
+
lines.push(" " + dim("\u2500".repeat(Math.max(40, this.terminalCols - 4))));
|
|
912
|
+
for (const raw of lines) {
|
|
913
|
+
this.write(this.padToWidth(raw) + "\n");
|
|
914
|
+
}
|
|
915
|
+
this.headerHeight = lines.length;
|
|
916
|
+
this.tabBarRow = this.headerHeight + 1;
|
|
917
|
+
this.write(`${CSI}u`);
|
|
918
|
+
}
|
|
919
|
+
drawTabBar() {
|
|
920
|
+
if (this.isShutdown)
|
|
921
|
+
return;
|
|
922
|
+
this.write(`${CSI}s`);
|
|
923
|
+
this.write(`${CSI}${this.tabBarRow};1H`);
|
|
924
|
+
const dim = chalk.dim;
|
|
925
|
+
const sel = (label, view) => this.currentView === view ? chalk.bold.cyan(label) : chalk.dim(label);
|
|
926
|
+
const left = " " + sel("1 App", "app") + " " + sel("2 Agent", "agent") + " " + sel("3 All", "all");
|
|
927
|
+
const right = dim("c clear \xB7 q quit") + " ";
|
|
928
|
+
const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, "");
|
|
929
|
+
const leftLen = stripAnsi(left).length;
|
|
930
|
+
const rightLen = stripAnsi(right).length;
|
|
931
|
+
const gap = Math.max(2, this.terminalCols - leftLen - rightLen);
|
|
932
|
+
const bar = left + " ".repeat(gap) + right;
|
|
933
|
+
const sep2 = " " + dim("\u2500".repeat(Math.max(40, this.terminalCols - 4)));
|
|
934
|
+
this.write(this.padToWidth(bar) + "\n");
|
|
935
|
+
this.write(this.padToWidth(sep2) + "\n");
|
|
936
|
+
this.scrollTop = this.tabBarRow + 2;
|
|
937
|
+
this.write(`${CSI}u`);
|
|
938
|
+
}
|
|
939
|
+
installScrollRegion() {
|
|
940
|
+
this.write(`${CSI}${this.scrollTop};${this.terminalRows}r`);
|
|
941
|
+
this.write(`${CSI}${this.terminalRows};1H`);
|
|
942
|
+
}
|
|
943
|
+
clearLogArea() {
|
|
944
|
+
this.write(`${CSI}s`);
|
|
945
|
+
for (let row = this.scrollTop; row <= this.terminalRows; row++) {
|
|
946
|
+
this.write(`${CSI}${row};1H${CSI}2K`);
|
|
947
|
+
}
|
|
948
|
+
this.write(`${CSI}${this.terminalRows};1H`);
|
|
949
|
+
this.write(`${CSI}u`);
|
|
950
|
+
}
|
|
951
|
+
replayLogs() {
|
|
952
|
+
this.clearLogArea();
|
|
953
|
+
this.write(`${CSI}${this.terminalRows};1H`);
|
|
954
|
+
const visibleRows = this.terminalRows - this.scrollTop + 1;
|
|
955
|
+
const start = Math.max(0, this.logs.length - visibleRows);
|
|
956
|
+
for (let i = start; i < this.logs.length; i++) {
|
|
957
|
+
const entry = this.logs[i];
|
|
958
|
+
if (this.matchesView(entry.source)) {
|
|
959
|
+
this.appendDisplayLine(this.formatLine(entry));
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
appendDisplayLine(line) {
|
|
964
|
+
if (this.isShutdown)
|
|
965
|
+
return;
|
|
966
|
+
this.write(line + "\n");
|
|
967
|
+
}
|
|
968
|
+
/** Direct write that bypasses our interceptor (uses originalStdoutWrite). */
|
|
969
|
+
write(s) {
|
|
970
|
+
if (this.originalStdoutWrite) {
|
|
971
|
+
this.originalStdoutWrite(s);
|
|
972
|
+
} else {
|
|
973
|
+
process.stdout.write(s);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
977
|
+
// Helpers
|
|
978
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
979
|
+
truncate(s, width) {
|
|
980
|
+
if (width <= 3)
|
|
981
|
+
return s.slice(0, Math.max(0, width));
|
|
982
|
+
if (s.length <= width)
|
|
983
|
+
return s;
|
|
984
|
+
const head = Math.floor((width - 3) * 0.4);
|
|
985
|
+
const tail = width - 3 - head;
|
|
986
|
+
return s.slice(0, head) + "..." + s.slice(s.length - tail);
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Pad a string out to terminal width so a redraw fully overwrites any
|
|
990
|
+
* leftover characters from a previous, longer line. Strips ANSI for
|
|
991
|
+
* length calculation but preserves it in the output.
|
|
992
|
+
*/
|
|
993
|
+
padToWidth(s) {
|
|
994
|
+
const visible = s.replace(/\x1B\[[0-9;]*m/g, "");
|
|
995
|
+
const pad = Math.max(0, this.terminalCols - visible.length);
|
|
996
|
+
return s + " ".repeat(pad);
|
|
997
|
+
}
|
|
998
|
+
};
|
|
999
|
+
|
|
542
1000
|
// src/index.ts
|
|
543
1001
|
import { createRequire as _createRequire } from "module";
|
|
544
1002
|
var __filename = fileURLToPath(import.meta.url);
|
|
545
1003
|
var __dirname = path4.dirname(__filename);
|
|
546
|
-
var
|
|
1004
|
+
var execAsync = promisify3(exec);
|
|
1005
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
1006
|
+
function _isInsideProject(projectPath, fullPath) {
|
|
1007
|
+
return fullPath === projectPath || fullPath.startsWith(projectPath + path4.sep);
|
|
1008
|
+
}
|
|
547
1009
|
var TerminalBuffer = class {
|
|
548
1010
|
lines = [];
|
|
549
1011
|
MAX_LINES = 500;
|
|
@@ -934,21 +1396,53 @@ function fuzzyReplace(content, oldString, newString, replaceAll = false) {
|
|
|
934
1396
|
return { error: "Found multiple matches for oldString. Provide more surrounding context to make the match unique." };
|
|
935
1397
|
}
|
|
936
1398
|
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) => {
|
|
1399
|
+
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").option("--no-ui", "Disable the branded TUI and stream raw logs (useful for piping/CI)").action(async (commandOverride, options) => {
|
|
938
1400
|
const port = parseInt(options.port);
|
|
939
1401
|
const projectPath = process.cwd();
|
|
940
1402
|
const { command, pm, script } = detectDevCommand(projectPath, commandOverride);
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1403
|
+
const useTui = options.ui !== false && process.stdout.isTTY;
|
|
1404
|
+
const tui = useTui ? new TraceTUI({
|
|
1405
|
+
project: projectPath,
|
|
1406
|
+
pm,
|
|
1407
|
+
devCommand: command,
|
|
1408
|
+
bridgePort: port,
|
|
1409
|
+
agentPort: 8766,
|
|
1410
|
+
browserPort: 8767,
|
|
1411
|
+
bridgeReady: false,
|
|
1412
|
+
agentReady: false,
|
|
1413
|
+
devReady: false,
|
|
1414
|
+
extConnected: false,
|
|
1415
|
+
clientCount: 0,
|
|
1416
|
+
agentExtras: "",
|
|
1417
|
+
cliVersion: VERSION
|
|
1418
|
+
}) : null;
|
|
1419
|
+
if (tui)
|
|
1420
|
+
tui.start();
|
|
1421
|
+
const logTrace = (msg) => {
|
|
1422
|
+
if (tui)
|
|
1423
|
+
tui.pushTrace(msg);
|
|
1424
|
+
else
|
|
1425
|
+
console.log(msg);
|
|
1426
|
+
};
|
|
1427
|
+
const logTraceErr = (msg) => {
|
|
1428
|
+
if (tui)
|
|
1429
|
+
tui.pushTrace(chalk2.red(msg));
|
|
1430
|
+
else
|
|
1431
|
+
console.error(msg);
|
|
1432
|
+
};
|
|
1433
|
+
if (!tui) {
|
|
1434
|
+
console.log();
|
|
1435
|
+
console.log(chalk2.bold.cyan("\u26A1 Trace Dev"));
|
|
1436
|
+
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
1437
|
+
console.log();
|
|
1438
|
+
console.log(`\u{1F4C1} Project: ${chalk2.green(projectPath)}`);
|
|
1439
|
+
console.log(`\u{1F4E6} Package Mgr: ${chalk2.yellow(pm)}`);
|
|
1440
|
+
console.log(`\u{1F680} Dev Command: ${chalk2.cyan(command)}`);
|
|
1441
|
+
console.log(`\u{1F310} Bridge Port: ${chalk2.cyan(port)}`);
|
|
1442
|
+
console.log();
|
|
1443
|
+
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
1444
|
+
console.log();
|
|
1445
|
+
}
|
|
952
1446
|
const connectedClients = /* @__PURE__ */ new Set();
|
|
953
1447
|
const broadcast = (payload) => {
|
|
954
1448
|
const msg = JSON.stringify(payload);
|
|
@@ -958,63 +1452,125 @@ program.command("dev").description("Start dev server + IDE bridge together (reco
|
|
|
958
1452
|
}
|
|
959
1453
|
}
|
|
960
1454
|
};
|
|
961
|
-
|
|
962
|
-
console.log();
|
|
1455
|
+
logTrace(chalk2.dim("Starting dev server..."));
|
|
963
1456
|
const childEnv = { ...process.env, FORCE_COLOR: "1" };
|
|
964
1457
|
delete childEnv.MallocNanoZone;
|
|
965
1458
|
delete childEnv.MallocStackLogging;
|
|
966
1459
|
delete childEnv.MallocScribble;
|
|
967
1460
|
delete childEnv.MallocGuardEdges;
|
|
968
1461
|
delete childEnv.MallocErrorAbort;
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1462
|
+
function hasRunnableDevScript() {
|
|
1463
|
+
const result = _checkPkg(projectPath, commandOverride);
|
|
1464
|
+
if (result.ok)
|
|
1465
|
+
return result;
|
|
1466
|
+
try {
|
|
1467
|
+
const entries = fs4.readdirSync(projectPath, { withFileTypes: true });
|
|
1468
|
+
for (const entry of entries) {
|
|
1469
|
+
if (!entry.isDirectory())
|
|
1470
|
+
continue;
|
|
1471
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
1472
|
+
continue;
|
|
1473
|
+
const sub = path4.join(projectPath, entry.name);
|
|
1474
|
+
const r = _checkPkg(sub, void 0);
|
|
1475
|
+
if (r.ok)
|
|
1476
|
+
return r;
|
|
1477
|
+
}
|
|
1478
|
+
} catch {
|
|
1479
|
+
}
|
|
1480
|
+
return { ok: false };
|
|
1481
|
+
}
|
|
1482
|
+
function _checkPkg(cwd, override) {
|
|
1483
|
+
const pkgPath = path4.join(cwd, "package.json");
|
|
1484
|
+
if (!fs4.existsSync(pkgPath))
|
|
1485
|
+
return { ok: false };
|
|
1486
|
+
try {
|
|
1487
|
+
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
1488
|
+
const scripts = pkg.scripts || {};
|
|
1489
|
+
const candidates = ["dev", "start", "serve", "preview"];
|
|
1490
|
+
const script2 = candidates.find((c) => !!scripts[c]);
|
|
1491
|
+
if (!script2)
|
|
1492
|
+
return { ok: false };
|
|
1493
|
+
const detected = detectDevCommand(cwd, override);
|
|
1494
|
+
return { ok: true, cmd: detected.command, script: detected.script, cwd };
|
|
1495
|
+
} catch {
|
|
1496
|
+
return { ok: false };
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
function spawnDevProcess(cmd, cwd = projectPath) {
|
|
1500
|
+
if (devProcess && !devProcess.killed) {
|
|
1501
|
+
logTrace(chalk2.dim(`[Trace] Dev process already running \u2014 skipping duplicate spawn for ${cwd}`));
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
devProcess = spawn2(cmd, [], {
|
|
1505
|
+
cwd,
|
|
1506
|
+
shell: true,
|
|
1507
|
+
env: childEnv
|
|
1508
|
+
});
|
|
1509
|
+
devProcess.stdout?.on("data", (chunk) => {
|
|
1510
|
+
const text = chunk.toString();
|
|
1511
|
+
if (tui)
|
|
1512
|
+
tui.pushApp(text);
|
|
1513
|
+
else
|
|
1514
|
+
process.stdout.write(text);
|
|
1515
|
+
globalTerminalBuffer.push(text);
|
|
1516
|
+
broadcast({ type: "STREAM_CHUNK", stream: "stdout", chunk: text });
|
|
1517
|
+
if (tui && /\b(Ready in|ready in|listening on|Local:)\b/i.test(text)) {
|
|
1518
|
+
tui.setStatus({ devReady: true });
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
devProcess.stderr?.on("data", (chunk) => {
|
|
1522
|
+
const text = chunk.toString();
|
|
1523
|
+
if (tui)
|
|
1524
|
+
tui.pushApp(text);
|
|
1525
|
+
else
|
|
1526
|
+
process.stderr.write(text);
|
|
1527
|
+
globalTerminalBuffer.push(text);
|
|
1528
|
+
broadcast({ type: "STREAM_CHUNK", stream: "stderr", chunk: text });
|
|
1529
|
+
});
|
|
1530
|
+
devProcess.on("close", (code) => {
|
|
1531
|
+
const msg = `[Trace] Dev server exited with code ${code}`;
|
|
1532
|
+
if (tui)
|
|
1533
|
+
tui.pushTrace(chalk2.yellow(msg));
|
|
1534
|
+
else
|
|
1535
|
+
process.stdout.write(chalk2.yellow(msg) + "\n");
|
|
1536
|
+
globalTerminalBuffer.push(msg);
|
|
1537
|
+
broadcast({ type: "STREAM_END", exitCode: code });
|
|
1538
|
+
if (tui)
|
|
1539
|
+
tui.setStatus({ devReady: false });
|
|
1540
|
+
devProcess = null;
|
|
1541
|
+
});
|
|
1542
|
+
devProcess.on("error", (err) => {
|
|
1543
|
+
const msg = `[Trace] Failed to start dev server: ${err.message}`;
|
|
1544
|
+
logTraceErr(msg);
|
|
1545
|
+
logTraceErr(`\u2717 Could not start dev server. Command: ${cmd}`);
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1003
1548
|
const wss = new WebSocketServer({ port });
|
|
1004
1549
|
let clientCount = 0;
|
|
1005
1550
|
wss.on("listening", () => {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1551
|
+
if (tui) {
|
|
1552
|
+
tui.setStatus({ bridgeReady: true });
|
|
1553
|
+
tui.pushTrace(chalk2.green("\u2713") + ` IDE Bridge listening on port ${port}`);
|
|
1554
|
+
tui.pushTrace(chalk2.dim("Waiting for Trace extension to connect..."));
|
|
1555
|
+
} else {
|
|
1556
|
+
console.log();
|
|
1557
|
+
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
1558
|
+
console.log(chalk2.green("\u2713") + " IDE Bridge listening on port " + chalk2.cyan(port));
|
|
1559
|
+
console.log(chalk2.dim("Waiting for Trace extension to connect..."));
|
|
1560
|
+
console.log(chalk2.dim("Press Ctrl+C to stop both"));
|
|
1561
|
+
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
1562
|
+
console.log();
|
|
1563
|
+
}
|
|
1013
1564
|
});
|
|
1014
1565
|
wss.on("connection", (ws) => {
|
|
1015
1566
|
clientCount++;
|
|
1016
1567
|
connectedClients.add(ws);
|
|
1017
|
-
|
|
1568
|
+
if (tui) {
|
|
1569
|
+
tui.setStatus({ extConnected: true, clientCount });
|
|
1570
|
+
tui.pushTrace(chalk2.green("\u25CF") + ` Extension connected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
1571
|
+
} else {
|
|
1572
|
+
console.log(chalk2.green("\u25CF") + ` Extension connected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
1573
|
+
}
|
|
1018
1574
|
attachMessageHandler(ws, projectPath);
|
|
1019
1575
|
const catchup = globalTerminalBuffer.getLast(100);
|
|
1020
1576
|
if (catchup.length > 0) {
|
|
@@ -1023,18 +1579,23 @@ program.command("dev").description("Start dev server + IDE bridge together (reco
|
|
|
1023
1579
|
ws.on("close", () => {
|
|
1024
1580
|
clientCount--;
|
|
1025
1581
|
connectedClients.delete(ws);
|
|
1026
|
-
|
|
1582
|
+
if (tui) {
|
|
1583
|
+
tui.setStatus({ extConnected: clientCount > 0, clientCount });
|
|
1584
|
+
tui.pushTrace(chalk2.yellow("\u25CF") + ` Extension disconnected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
1585
|
+
} else {
|
|
1586
|
+
console.log(chalk2.yellow("\u25CF") + ` Extension disconnected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
1587
|
+
}
|
|
1027
1588
|
});
|
|
1028
1589
|
ws.on("error", (err) => {
|
|
1029
|
-
console.error(
|
|
1590
|
+
console.error(chalk2.red("WebSocket error:"), err.message);
|
|
1030
1591
|
connectedClients.delete(ws);
|
|
1031
1592
|
});
|
|
1032
1593
|
});
|
|
1033
1594
|
wss.on("error", (error) => {
|
|
1034
1595
|
if (error.code === "EADDRINUSE") {
|
|
1035
|
-
console.error(
|
|
1596
|
+
console.error(chalk2.red(`\u2717 Port ${port} is already in use. Try: trace dev --port 8766`));
|
|
1036
1597
|
} else {
|
|
1037
|
-
console.error(
|
|
1598
|
+
console.error(chalk2.red("Bridge error:"), error.message);
|
|
1038
1599
|
}
|
|
1039
1600
|
});
|
|
1040
1601
|
const http = await import("http");
|
|
@@ -1057,9 +1618,9 @@ program.command("dev").description("Start dev server + IDE bridge together (reco
|
|
|
1057
1618
|
res.end(JSON.stringify({ error: "Trace extension not connected." }));
|
|
1058
1619
|
return;
|
|
1059
1620
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1621
|
+
let body = {};
|
|
1622
|
+
if (req.method === "POST") {
|
|
1623
|
+
try {
|
|
1063
1624
|
body = await new Promise((resolve3, reject) => {
|
|
1064
1625
|
let raw = "";
|
|
1065
1626
|
req.on("data", (chunk) => raw += chunk.toString());
|
|
@@ -1072,39 +1633,88 @@ program.command("dev").description("Start dev server + IDE bridge together (reco
|
|
|
1072
1633
|
});
|
|
1073
1634
|
req.on("error", reject);
|
|
1074
1635
|
});
|
|
1636
|
+
} catch (e) {
|
|
1637
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1638
|
+
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
1639
|
+
return;
|
|
1075
1640
|
}
|
|
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 });
|
|
1089
|
-
});
|
|
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
1641
|
}
|
|
1642
|
+
res.writeHead(200, {
|
|
1643
|
+
"Content-Type": "text/event-stream",
|
|
1644
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1645
|
+
"Connection": "keep-alive",
|
|
1646
|
+
// Some HTTP layers (proxies, fetch in node) buffer responses
|
|
1647
|
+
// unless we tell them not to.
|
|
1648
|
+
"X-Accel-Buffering": "no"
|
|
1649
|
+
});
|
|
1650
|
+
const writeEvent = (payload) => {
|
|
1651
|
+
try {
|
|
1652
|
+
if (!res.writableEnded) {
|
|
1653
|
+
res.write(`data: ${JSON.stringify(payload)}
|
|
1654
|
+
|
|
1655
|
+
`);
|
|
1656
|
+
}
|
|
1657
|
+
} catch {
|
|
1658
|
+
}
|
|
1659
|
+
};
|
|
1660
|
+
writeEvent({ type: "agent_start", agent: agentName });
|
|
1661
|
+
const reqId = ++globalBrowserRequestId;
|
|
1662
|
+
const timer = setTimeout(() => {
|
|
1663
|
+
const pending = globalBrowserPending.get(reqId);
|
|
1664
|
+
if (pending) {
|
|
1665
|
+
globalBrowserPending.delete(reqId);
|
|
1666
|
+
writeEvent({ type: "error", error: "Agent timeout (65s)" });
|
|
1667
|
+
try {
|
|
1668
|
+
res.end();
|
|
1669
|
+
} catch {
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}, 65e3);
|
|
1673
|
+
req.on("close", () => {
|
|
1674
|
+
if (globalBrowserPending.has(reqId)) {
|
|
1675
|
+
clearTimeout(timer);
|
|
1676
|
+
globalBrowserPending.delete(reqId);
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
globalBrowserPending.set(reqId, {
|
|
1680
|
+
resolve: (data) => {
|
|
1681
|
+
clearTimeout(timer);
|
|
1682
|
+
writeEvent({ type: "result", data });
|
|
1683
|
+
try {
|
|
1684
|
+
res.end();
|
|
1685
|
+
} catch {
|
|
1686
|
+
}
|
|
1687
|
+
},
|
|
1688
|
+
reject: (e) => {
|
|
1689
|
+
clearTimeout(timer);
|
|
1690
|
+
writeEvent({ type: "error", error: e?.message || String(e) });
|
|
1691
|
+
try {
|
|
1692
|
+
res.end();
|
|
1693
|
+
} catch {
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
timer,
|
|
1697
|
+
onProgress: (event) => writeEvent(event)
|
|
1698
|
+
});
|
|
1699
|
+
client2.send(JSON.stringify({
|
|
1700
|
+
id: reqId,
|
|
1701
|
+
type: "AGENT_INVOKE",
|
|
1702
|
+
agent: agentName,
|
|
1703
|
+
query: body.query || ""
|
|
1704
|
+
}));
|
|
1096
1705
|
return;
|
|
1097
1706
|
}
|
|
1098
1707
|
const browserQueryRoutes = {
|
|
1099
1708
|
"/browser/console": "BROWSER_GET_CONSOLE",
|
|
1100
1709
|
"/browser/network": "BROWSER_GET_NETWORK",
|
|
1101
1710
|
"/browser/dom": "BROWSER_GET_DOM",
|
|
1102
|
-
"/browser/screenshot": "BROWSER_SCREENSHOT"
|
|
1711
|
+
"/browser/screenshot": "BROWSER_SCREENSHOT",
|
|
1712
|
+
"/browser/verify-build": "BROWSER_VERIFY_BUILD"
|
|
1103
1713
|
};
|
|
1104
1714
|
const isBrowserRoute = url.pathname in browserQueryRoutes || url.pathname === "/browser/eval";
|
|
1105
1715
|
if (!isBrowserRoute) {
|
|
1106
1716
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1107
|
-
res.end(JSON.stringify({ error: "Not found. Available: /browser/{console,network,dom,eval,screenshot}" }));
|
|
1717
|
+
res.end(JSON.stringify({ error: "Not found. Available: /browser/{console,network,dom,eval,screenshot,verify-build}" }));
|
|
1108
1718
|
return;
|
|
1109
1719
|
}
|
|
1110
1720
|
const client = [...connectedClients].find(
|
|
@@ -1154,19 +1764,26 @@ program.command("dev").description("Start dev server + IDE bridge together (reco
|
|
|
1154
1764
|
});
|
|
1155
1765
|
const BROWSER_HTTP_PORT = 8767;
|
|
1156
1766
|
browserHttpServer.listen(BROWSER_HTTP_PORT, "127.0.0.1", () => {
|
|
1157
|
-
|
|
1158
|
-
|
|
1767
|
+
if (tui) {
|
|
1768
|
+
tui.pushTrace(chalk2.green("\u2713") + ` Browser Query HTTP server on port ${BROWSER_HTTP_PORT}`);
|
|
1769
|
+
} else {
|
|
1770
|
+
console.log(chalk2.green("\u2713") + " Browser Query HTTP server on port " + chalk2.cyan(BROWSER_HTTP_PORT));
|
|
1771
|
+
console.log(chalk2.dim(" Coding agents can query: curl http://localhost:8767/browser/console"));
|
|
1772
|
+
}
|
|
1159
1773
|
});
|
|
1160
1774
|
browserHttpServer.on("error", (e) => {
|
|
1161
1775
|
if (e.code !== "EADDRINUSE")
|
|
1162
|
-
console.warn(
|
|
1776
|
+
console.warn(chalk2.yellow("\u26A0 Browser HTTP server error:"), e.message);
|
|
1163
1777
|
});
|
|
1164
1778
|
let agentServer = null;
|
|
1165
1779
|
try {
|
|
1166
1780
|
let agentPath = "@gettrace/agent/dist/node/index.js";
|
|
1167
1781
|
if (process.env.TRACE_DEV_MODE) {
|
|
1168
1782
|
agentPath = path4.resolve(__dirname, "../../packages/trace-agent/dist/node/index.js");
|
|
1169
|
-
|
|
1783
|
+
if (tui)
|
|
1784
|
+
tui.pushTrace(chalk2.magenta("\u2699") + " Dev Mode: Using local trace agent at " + chalk2.dim(agentPath));
|
|
1785
|
+
else
|
|
1786
|
+
console.log(chalk2.magenta("\u2699") + " Dev Mode: Using local trace agent at " + chalk2.dim(agentPath));
|
|
1170
1787
|
}
|
|
1171
1788
|
const { Server } = await import(agentPath);
|
|
1172
1789
|
process.env.OPENCODE_EXPERIMENTAL = "1";
|
|
@@ -1181,14 +1798,210 @@ program.command("dev").description("Start dev server + IDE bridge together (reco
|
|
|
1181
1798
|
hostname: "127.0.0.1",
|
|
1182
1799
|
cors: ["*"]
|
|
1183
1800
|
});
|
|
1184
|
-
|
|
1185
|
-
|
|
1801
|
+
if (tui) {
|
|
1802
|
+
tui.setStatus({
|
|
1803
|
+
agentReady: true,
|
|
1804
|
+
agentExtras: "Sonnet 4.5 \xB7 LSP \xB7 Exa \xB7 Plan"
|
|
1805
|
+
});
|
|
1806
|
+
tui.pushTrace(chalk2.green("\u2713") + " Trace Agent Server listening on port 8766");
|
|
1807
|
+
} else {
|
|
1808
|
+
console.log(chalk2.green("\u2713") + " Trace Agent Server listening on port " + chalk2.cyan(8766));
|
|
1809
|
+
console.log(chalk2.dim(" LSP \xB7 Exa search \xB7 Plan mode \xB7 All tools unlocked"));
|
|
1810
|
+
}
|
|
1186
1811
|
} catch (e) {
|
|
1187
|
-
|
|
1812
|
+
const msg = "\u26A0 Failed to start Trace Agent Server: " + e.message;
|
|
1813
|
+
if (tui)
|
|
1814
|
+
tui.pushTrace(chalk2.yellow(msg));
|
|
1815
|
+
else
|
|
1816
|
+
console.error(chalk2.yellow(msg));
|
|
1817
|
+
}
|
|
1818
|
+
let _devServerId = null;
|
|
1819
|
+
let _devServerOutputSeen = 0;
|
|
1820
|
+
let _devServerPollTimer = null;
|
|
1821
|
+
let _devServerSseAbort = null;
|
|
1822
|
+
function _drainAgentDevOutput(lines) {
|
|
1823
|
+
if (!Array.isArray(lines) || !lines.length)
|
|
1824
|
+
return;
|
|
1825
|
+
if (_devServerOutputSeen >= lines.length) {
|
|
1826
|
+
_devServerOutputSeen = Math.max(0, lines.length - 100);
|
|
1827
|
+
}
|
|
1828
|
+
for (const line of lines.slice(_devServerOutputSeen)) {
|
|
1829
|
+
const text = line + "\n";
|
|
1830
|
+
if (tui)
|
|
1831
|
+
tui.pushApp(text);
|
|
1832
|
+
else
|
|
1833
|
+
process.stdout.write(text);
|
|
1834
|
+
globalTerminalBuffer.push(text);
|
|
1835
|
+
broadcast({ type: "STREAM_CHUNK", stream: "stdout", chunk: text });
|
|
1836
|
+
}
|
|
1837
|
+
_devServerOutputSeen = lines.length;
|
|
1838
|
+
}
|
|
1839
|
+
function _stopAgentDevPoll() {
|
|
1840
|
+
if (_devServerPollTimer) {
|
|
1841
|
+
clearInterval(_devServerPollTimer);
|
|
1842
|
+
_devServerPollTimer = null;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
function _startAgentDevPoll(id) {
|
|
1846
|
+
_stopAgentDevPoll();
|
|
1847
|
+
_devServerPollTimer = setInterval(async () => {
|
|
1848
|
+
try {
|
|
1849
|
+
const res = await fetch(`http://127.0.0.1:8766/dev-server/${encodeURIComponent(id)}`);
|
|
1850
|
+
if (!res.ok)
|
|
1851
|
+
return;
|
|
1852
|
+
const handle = await res.json().catch(() => null);
|
|
1853
|
+
if (!handle)
|
|
1854
|
+
return;
|
|
1855
|
+
_drainAgentDevOutput(handle.output || []);
|
|
1856
|
+
const status = handle.state?.status;
|
|
1857
|
+
if (status === "exited" || status === "stopped" || status === "failed") {
|
|
1858
|
+
_stopAgentDevPoll();
|
|
1859
|
+
if (tui)
|
|
1860
|
+
tui.setStatus({ devReady: false });
|
|
1861
|
+
} else if (status === "ready" && tui) {
|
|
1862
|
+
tui.setStatus({ devReady: true });
|
|
1863
|
+
}
|
|
1864
|
+
} catch {
|
|
1865
|
+
}
|
|
1866
|
+
}, 500);
|
|
1867
|
+
}
|
|
1868
|
+
async function _subscribeAgentDevEvents() {
|
|
1869
|
+
const ctrl = _devServerSseAbort;
|
|
1870
|
+
if (!ctrl)
|
|
1871
|
+
return;
|
|
1872
|
+
while (!ctrl.signal.aborted) {
|
|
1873
|
+
try {
|
|
1874
|
+
const res = await fetch("http://127.0.0.1:8766/global/event", {
|
|
1875
|
+
signal: ctrl.signal,
|
|
1876
|
+
headers: { Accept: "text/event-stream" }
|
|
1877
|
+
});
|
|
1878
|
+
if (!res.ok || !res.body) {
|
|
1879
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
const reader = res.body.getReader();
|
|
1883
|
+
const decoder = new TextDecoder();
|
|
1884
|
+
let buf = "";
|
|
1885
|
+
while (!ctrl.signal.aborted) {
|
|
1886
|
+
const { value, done } = await reader.read();
|
|
1887
|
+
if (done)
|
|
1888
|
+
break;
|
|
1889
|
+
buf += decoder.decode(value, { stream: true });
|
|
1890
|
+
let idx;
|
|
1891
|
+
while ((idx = buf.indexOf("\n\n")) !== -1) {
|
|
1892
|
+
const frame = buf.slice(0, idx);
|
|
1893
|
+
buf = buf.slice(idx + 2);
|
|
1894
|
+
const dataLine = frame.split("\n").find((l) => l.startsWith("data:"));
|
|
1895
|
+
if (!dataLine)
|
|
1896
|
+
continue;
|
|
1897
|
+
const dataStr = dataLine.replace(/^data:\s*/, "");
|
|
1898
|
+
try {
|
|
1899
|
+
const parsed = JSON.parse(dataStr);
|
|
1900
|
+
const type = parsed?.payload?.type;
|
|
1901
|
+
const props = parsed?.payload?.properties;
|
|
1902
|
+
if (!type || !props)
|
|
1903
|
+
continue;
|
|
1904
|
+
if (props.id && _devServerId && props.id !== _devServerId)
|
|
1905
|
+
continue;
|
|
1906
|
+
if (type === "dev_server.starting") {
|
|
1907
|
+
if (!_devServerId && props.id) {
|
|
1908
|
+
_devServerId = props.id;
|
|
1909
|
+
_startAgentDevPoll(props.id);
|
|
1910
|
+
}
|
|
1911
|
+
} else if (type === "dev_server.ready") {
|
|
1912
|
+
if (tui)
|
|
1913
|
+
tui.setStatus({ devReady: true });
|
|
1914
|
+
} else if (type === "dev_server.failed" || type === "dev_server.exited") {
|
|
1915
|
+
if (tui)
|
|
1916
|
+
tui.setStatus({ devReady: false });
|
|
1917
|
+
}
|
|
1918
|
+
} catch {
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
} catch {
|
|
1923
|
+
if (ctrl.signal.aborted)
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
async function spawnViaAgentServer(cmd, cwd) {
|
|
1930
|
+
if (_devServerId) {
|
|
1931
|
+
logTrace(chalk2.dim(`[Trace] Dev server already managed by agent (id=${_devServerId}) \u2014 skipping duplicate`));
|
|
1932
|
+
return { ok: true };
|
|
1933
|
+
}
|
|
1934
|
+
_devServerSseAbort = new AbortController();
|
|
1935
|
+
void _subscribeAgentDevEvents();
|
|
1936
|
+
let res;
|
|
1937
|
+
try {
|
|
1938
|
+
res = await fetch("http://127.0.0.1:8766/dev-server", {
|
|
1939
|
+
method: "POST",
|
|
1940
|
+
headers: { "Content-Type": "application/json" },
|
|
1941
|
+
body: JSON.stringify({ command: cmd, cwd, timeoutMs: 9e4 })
|
|
1942
|
+
});
|
|
1943
|
+
} catch (e) {
|
|
1944
|
+
return { ok: false, reason: e?.message ?? String(e) };
|
|
1945
|
+
}
|
|
1946
|
+
if (!res.ok) {
|
|
1947
|
+
const text = await res.text().catch(() => "");
|
|
1948
|
+
return { ok: false, reason: `HTTP ${res.status}: ${text || res.statusText}` };
|
|
1949
|
+
}
|
|
1950
|
+
const handle = await res.json().catch(() => null);
|
|
1951
|
+
if (!handle?.id)
|
|
1952
|
+
return { ok: false, reason: "no_id_returned" };
|
|
1953
|
+
_devServerId = handle.id;
|
|
1954
|
+
_drainAgentDevOutput(handle.output || []);
|
|
1955
|
+
_startAgentDevPoll(handle.id);
|
|
1956
|
+
if (handle.state?.status === "ready") {
|
|
1957
|
+
if (tui)
|
|
1958
|
+
tui.setStatus({ devReady: true });
|
|
1959
|
+
return { ok: true };
|
|
1960
|
+
}
|
|
1961
|
+
return {
|
|
1962
|
+
ok: false,
|
|
1963
|
+
reason: handle.state?.error || `state: ${handle.state?.status ?? "unknown"}`
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
const initial = commandOverride ? { ok: true, cmd: command, script: "override", cwd: projectPath } : hasRunnableDevScript();
|
|
1967
|
+
if (initial.ok && initial.cmd) {
|
|
1968
|
+
if (agentServer) {
|
|
1969
|
+
logTrace(chalk2.dim(`Starting dev server via agent: ${initial.cmd}`));
|
|
1970
|
+
const result = await spawnViaAgentServer(initial.cmd, initial.cwd ?? projectPath);
|
|
1971
|
+
if (!result.ok) {
|
|
1972
|
+
logTraceErr(`\u26A0 Agent-routed dev_server failed: ${result.reason}. Falling back to direct spawn.`);
|
|
1973
|
+
spawnDevProcess(initial.cmd, initial.cwd ?? projectPath);
|
|
1974
|
+
}
|
|
1975
|
+
} else {
|
|
1976
|
+
logTrace(chalk2.yellow("\u26A0 Agent server unavailable \u2014 using direct dev spawn (no cross-process dedup)"));
|
|
1977
|
+
spawnDevProcess(initial.cmd, initial.cwd ?? projectPath);
|
|
1978
|
+
}
|
|
1979
|
+
} else {
|
|
1980
|
+
logTrace(chalk2.dim("No dev script at startup \u2014 the agent will start one via dev_server when it scaffolds."));
|
|
1188
1981
|
}
|
|
1189
1982
|
const shutdown = async () => {
|
|
1983
|
+
if (tui)
|
|
1984
|
+
tui.shutdown();
|
|
1190
1985
|
console.log();
|
|
1191
|
-
console.log(
|
|
1986
|
+
console.log(chalk2.yellow("Shutting down..."));
|
|
1987
|
+
if (_devServerSseAbort) {
|
|
1988
|
+
try {
|
|
1989
|
+
_devServerSseAbort.abort();
|
|
1990
|
+
} catch {
|
|
1991
|
+
}
|
|
1992
|
+
_devServerSseAbort = null;
|
|
1993
|
+
}
|
|
1994
|
+
_stopAgentDevPoll();
|
|
1995
|
+
if (_devServerId) {
|
|
1996
|
+
try {
|
|
1997
|
+
await fetch(`http://127.0.0.1:8766/dev-server/${encodeURIComponent(_devServerId)}/stop`, {
|
|
1998
|
+
method: "POST",
|
|
1999
|
+
signal: AbortSignal.timeout(3e3)
|
|
2000
|
+
});
|
|
2001
|
+
} catch {
|
|
2002
|
+
}
|
|
2003
|
+
_devServerId = null;
|
|
2004
|
+
}
|
|
1192
2005
|
if (devProcess && !devProcess.killed) {
|
|
1193
2006
|
devProcess.kill("SIGTERM");
|
|
1194
2007
|
}
|
|
@@ -1226,7 +2039,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1226
2039
|
case "READ_FILE":
|
|
1227
2040
|
try {
|
|
1228
2041
|
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1229
|
-
if (!
|
|
2042
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
1230
2043
|
response.error = "Access denied";
|
|
1231
2044
|
} else if (fs4.existsSync(filePath)) {
|
|
1232
2045
|
const stat = fs4.statSync(filePath);
|
|
@@ -1278,7 +2091,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1278
2091
|
case "GET_SOURCE":
|
|
1279
2092
|
try {
|
|
1280
2093
|
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1281
|
-
if (!
|
|
2094
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
1282
2095
|
response.error = "Access denied";
|
|
1283
2096
|
} else if (fs4.existsSync(filePath)) {
|
|
1284
2097
|
const content = fs4.readFileSync(filePath, "utf-8");
|
|
@@ -1301,7 +2114,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1301
2114
|
case "GET_ERROR_CONTEXT":
|
|
1302
2115
|
try {
|
|
1303
2116
|
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1304
|
-
if (!
|
|
2117
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
1305
2118
|
response.error = "Access denied";
|
|
1306
2119
|
} else if (fs4.existsSync(filePath)) {
|
|
1307
2120
|
const content = fs4.readFileSync(filePath, "utf-8");
|
|
@@ -1680,7 +2493,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1680
2493
|
// Scaffolding tree
|
|
1681
2494
|
projectTree
|
|
1682
2495
|
};
|
|
1683
|
-
console.log(
|
|
2496
|
+
console.log(chalk2.blue("\u2139") + ` Project detected: ${chalk2.yellow(framework)} + ${chalk2.cyan(styling)}${typescript ? chalk2.dim(" (TS)") : ""}${router ? chalk2.dim(` [${router} router]`) : ""}`);
|
|
1684
2497
|
} catch (e) {
|
|
1685
2498
|
response.error = e.message;
|
|
1686
2499
|
}
|
|
@@ -1754,7 +2567,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1754
2567
|
filesScanned,
|
|
1755
2568
|
classesIndexed
|
|
1756
2569
|
};
|
|
1757
|
-
console.log(
|
|
2570
|
+
console.log(chalk2.blue("\u2139") + ` Class index built: ${chalk2.yellow(classesIndexed)} classes across ${chalk2.cyan(filesScanned)} files`);
|
|
1758
2571
|
} catch (e) {
|
|
1759
2572
|
response.error = e.message;
|
|
1760
2573
|
}
|
|
@@ -1762,7 +2575,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1762
2575
|
case "WRITE_FILE":
|
|
1763
2576
|
try {
|
|
1764
2577
|
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1765
|
-
if (!
|
|
2578
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
1766
2579
|
response.error = "Access denied: Path outside project";
|
|
1767
2580
|
} else {
|
|
1768
2581
|
const prevContent = fs4.existsSync(filePath) ? fs4.readFileSync(filePath, "utf-8") : null;
|
|
@@ -1787,7 +2600,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1787
2600
|
fs4.writeFileSync(filePath, message.content, "utf-8");
|
|
1788
2601
|
const formatter = await autoFormat(filePath, projectPath);
|
|
1789
2602
|
response.data = { success: true, path: filePath, formatted: formatter, undoAvailable: true };
|
|
1790
|
-
console.log(
|
|
2603
|
+
console.log(chalk2.blue("\u2139") + ` Wrote file: ${message.filePath}` + (formatter ? chalk2.dim(` (formatted with ${formatter})`) : "") + chalk2.dim(" [undo saved]"));
|
|
1791
2604
|
});
|
|
1792
2605
|
const _wLsp = await checkDiagnostics(filePath, projectPath);
|
|
1793
2606
|
if (_wLsp.ran && response.data) {
|
|
@@ -1795,7 +2608,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1795
2608
|
if (_wLsp.summary) {
|
|
1796
2609
|
response.data.lspSummary = _wLsp.summary;
|
|
1797
2610
|
if (_wLsp.diagnostics.some((d) => d.severity === "error")) {
|
|
1798
|
-
console.log(
|
|
2611
|
+
console.log(chalk2.yellow("\u26A0") + ` ${_wLsp.summary}`);
|
|
1799
2612
|
}
|
|
1800
2613
|
}
|
|
1801
2614
|
}
|
|
@@ -1807,7 +2620,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1807
2620
|
case "APPEND_FILE":
|
|
1808
2621
|
try {
|
|
1809
2622
|
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1810
|
-
if (!
|
|
2623
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
1811
2624
|
response.error = "Access denied: Path outside project";
|
|
1812
2625
|
} else {
|
|
1813
2626
|
const isCssFile = filePath.endsWith(".css");
|
|
@@ -1856,7 +2669,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1856
2669
|
bodyContent
|
|
1857
2670
|
].join("\n");
|
|
1858
2671
|
fs4.writeFileSync(filePath, merged, "utf-8");
|
|
1859
|
-
console.log(
|
|
2672
|
+
console.log(chalk2.blue("\u2139") + ` CSS-safe append: hoisted ${dedupedImports.length} @import(s) to top of ${message.filePath}`);
|
|
1860
2673
|
} else {
|
|
1861
2674
|
fs4.appendFileSync(filePath, "\n\n" + newContent, "utf-8");
|
|
1862
2675
|
}
|
|
@@ -1865,7 +2678,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1865
2678
|
}
|
|
1866
2679
|
const formatter = await autoFormat(filePath, projectPath);
|
|
1867
2680
|
response.data = { success: true, path: filePath, formatted: formatter, undoAvailable: true };
|
|
1868
|
-
console.log(
|
|
2681
|
+
console.log(chalk2.blue("\u2139") + ` Appended file: ${message.filePath}` + (formatter ? chalk2.dim(` (formatted with ${formatter})`) : "") + chalk2.dim(" [undo saved]"));
|
|
1869
2682
|
}
|
|
1870
2683
|
} catch (e) {
|
|
1871
2684
|
response.error = e.message;
|
|
@@ -1874,7 +2687,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1874
2687
|
case "EDIT_CLASSNAME": {
|
|
1875
2688
|
try {
|
|
1876
2689
|
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1877
|
-
if (!
|
|
2690
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
1878
2691
|
response.error = "Access denied: Path outside project";
|
|
1879
2692
|
break;
|
|
1880
2693
|
}
|
|
@@ -1916,7 +2729,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1916
2729
|
undoAvailable: true
|
|
1917
2730
|
};
|
|
1918
2731
|
console.log(
|
|
1919
|
-
|
|
2732
|
+
chalk2.blue("\u2139") + ` EDIT_CLASSNAME: ${message.filePath}` + chalk2.dim(` [${result.strategy} @ line ${result.matchedLine}]`) + (formatter ? chalk2.dim(` (${formatter})`) : "") + chalk2.dim(" [undo saved]")
|
|
1920
2733
|
);
|
|
1921
2734
|
});
|
|
1922
2735
|
const lsp = await checkDiagnostics(filePath, projectPath);
|
|
@@ -1933,7 +2746,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
1933
2746
|
case "EDIT_FILE":
|
|
1934
2747
|
try {
|
|
1935
2748
|
const filePath = path4.resolve(projectPath, message.filePath);
|
|
1936
|
-
if (!
|
|
2749
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
1937
2750
|
response.error = "Access denied: Path outside project";
|
|
1938
2751
|
} else if (!fs4.existsSync(filePath)) {
|
|
1939
2752
|
const requestedBase = path4.basename(message.filePath);
|
|
@@ -2006,9 +2819,9 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2006
2819
|
formatted: formatter,
|
|
2007
2820
|
undoAvailable: true
|
|
2008
2821
|
};
|
|
2009
|
-
const strategyLabel = result.strategy === "exact" ? "" :
|
|
2010
|
-
const formatLabel = formatter ?
|
|
2011
|
-
console.log(
|
|
2822
|
+
const strategyLabel = result.strategy === "exact" ? "" : chalk2.dim(` [${result.strategy}]`);
|
|
2823
|
+
const formatLabel = formatter ? chalk2.dim(` (${formatter})`) : "";
|
|
2824
|
+
console.log(chalk2.blue("\u2139") + ` Edited file: ${message.filePath}${strategyLabel}${formatLabel}` + chalk2.dim(" [undo saved]"));
|
|
2012
2825
|
});
|
|
2013
2826
|
const _eLsp = await checkDiagnostics(filePath, projectPath);
|
|
2014
2827
|
if (_eLsp.ran && response.data) {
|
|
@@ -2016,7 +2829,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2016
2829
|
if (_eLsp.summary) {
|
|
2017
2830
|
response.data.lspSummary = _eLsp.summary;
|
|
2018
2831
|
if (_eLsp.diagnostics.some((d) => d.severity === "error")) {
|
|
2019
|
-
console.log(
|
|
2832
|
+
console.log(chalk2.yellow("\u26A0") + ` ${_eLsp.summary}`);
|
|
2020
2833
|
}
|
|
2021
2834
|
}
|
|
2022
2835
|
}
|
|
@@ -2029,7 +2842,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2029
2842
|
case "REPLACE_LINES":
|
|
2030
2843
|
try {
|
|
2031
2844
|
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2032
|
-
if (!
|
|
2845
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
2033
2846
|
response.error = "Access denied: Path outside project";
|
|
2034
2847
|
} else if (!fs4.existsSync(filePath)) {
|
|
2035
2848
|
response.error = "File not found: " + message.filePath;
|
|
@@ -2124,8 +2937,8 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2124
2937
|
undoAvailable: true,
|
|
2125
2938
|
hint: lineDelta !== 0 ? `Line count changed by ${lineDelta > 0 ? "+" : ""}${lineDelta}. Adjust subsequent line numbers if making more edits.` : null
|
|
2126
2939
|
};
|
|
2127
|
-
const formatLabel = formatter ?
|
|
2128
|
-
console.log(
|
|
2940
|
+
const formatLabel = formatter ? chalk2.dim(` (${formatter})`) : "";
|
|
2941
|
+
console.log(chalk2.blue("\u2139") + ` Replaced lines ${startLine}-${endLine} in ${message.filePath} (${oldLineCount}\u2192${newLines.length} lines)${formatLabel}` + chalk2.dim(" [undo saved]"));
|
|
2129
2942
|
});
|
|
2130
2943
|
const _rLsp = await checkDiagnostics(filePath, projectPath);
|
|
2131
2944
|
if (_rLsp.ran && response.data) {
|
|
@@ -2133,7 +2946,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2133
2946
|
if (_rLsp.summary) {
|
|
2134
2947
|
response.data.lspSummary = _rLsp.summary;
|
|
2135
2948
|
if (_rLsp.diagnostics.some((d) => d.severity === "error")) {
|
|
2136
|
-
console.log(
|
|
2949
|
+
console.log(chalk2.yellow("\u26A0") + ` ${_rLsp.summary}`);
|
|
2137
2950
|
}
|
|
2138
2951
|
}
|
|
2139
2952
|
}
|
|
@@ -2159,7 +2972,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2159
2972
|
action: "deleted (was new file)",
|
|
2160
2973
|
remaining: undoStack.length
|
|
2161
2974
|
};
|
|
2162
|
-
console.log(
|
|
2975
|
+
console.log(chalk2.yellow("\u21A9") + ` Undo: deleted ${snapshot.relativePath} (was a new file)`);
|
|
2163
2976
|
} else {
|
|
2164
2977
|
response.data = {
|
|
2165
2978
|
success: true,
|
|
@@ -2178,7 +2991,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2178
2991
|
action: "restored",
|
|
2179
2992
|
remaining: undoStack.length
|
|
2180
2993
|
};
|
|
2181
|
-
console.log(
|
|
2994
|
+
console.log(chalk2.yellow("\u21A9") + ` Undo: restored ${snapshot.relativePath} (reverted ${snapshot.operation})`);
|
|
2182
2995
|
}
|
|
2183
2996
|
}
|
|
2184
2997
|
} catch (e) {
|
|
@@ -2188,7 +3001,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2188
3001
|
case "DELETE_FILE":
|
|
2189
3002
|
try {
|
|
2190
3003
|
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2191
|
-
if (!
|
|
3004
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
2192
3005
|
response.error = "Access denied: Path outside project";
|
|
2193
3006
|
break;
|
|
2194
3007
|
}
|
|
@@ -2278,7 +3091,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2278
3091
|
undoAvailable: true,
|
|
2279
3092
|
hint: "File moved to .trace-trash/ \u2014 recoverable even after CLI restart. Call UNDO_LAST to restore to original path."
|
|
2280
3093
|
};
|
|
2281
|
-
console.log(
|
|
3094
|
+
console.log(chalk2.yellow("\u{1F5D1}") + ` Soft-deleted: ${message.filePath} \u2192 .trace-trash/${trashName}` + chalk2.dim(" [recoverable]"));
|
|
2282
3095
|
} catch (e) {
|
|
2283
3096
|
response.error = e.message;
|
|
2284
3097
|
}
|
|
@@ -2287,7 +3100,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2287
3100
|
try {
|
|
2288
3101
|
const oldPath = path4.resolve(projectPath, message.oldPath);
|
|
2289
3102
|
const newPath = path4.resolve(projectPath, message.newPath);
|
|
2290
|
-
if (!
|
|
3103
|
+
if (!_isInsideProject(projectPath, oldPath) || !_isInsideProject(projectPath, newPath)) {
|
|
2291
3104
|
response.error = "Access denied: Path outside project";
|
|
2292
3105
|
} else if (!fs4.existsSync(oldPath)) {
|
|
2293
3106
|
response.error = "File not found: " + message.oldPath;
|
|
@@ -2308,7 +3121,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2308
3121
|
newPath: message.newPath,
|
|
2309
3122
|
undoAvailable: true
|
|
2310
3123
|
};
|
|
2311
|
-
console.log(
|
|
3124
|
+
console.log(chalk2.blue("\u2139") + ` Renamed: ${message.oldPath} \u2192 ${message.newPath}` + chalk2.dim(" [undo saved]"));
|
|
2312
3125
|
}
|
|
2313
3126
|
} catch (e) {
|
|
2314
3127
|
response.error = e.message;
|
|
@@ -2316,9 +3129,23 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2316
3129
|
break;
|
|
2317
3130
|
case "GIT_BLAME":
|
|
2318
3131
|
try {
|
|
2319
|
-
const filePath = message.filePath;
|
|
2320
|
-
|
|
2321
|
-
|
|
3132
|
+
const filePath = path4.resolve(projectPath, message.filePath || "");
|
|
3133
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
3134
|
+
response.error = "Access denied: Path outside project";
|
|
3135
|
+
break;
|
|
3136
|
+
}
|
|
3137
|
+
const lineNum = parseInt(message.line, 10);
|
|
3138
|
+
if (!Number.isFinite(lineNum) || lineNum < 1) {
|
|
3139
|
+
response.error = "Invalid line number";
|
|
3140
|
+
break;
|
|
3141
|
+
}
|
|
3142
|
+
const { stdout } = await execFileAsync3("git", [
|
|
3143
|
+
"blame",
|
|
3144
|
+
"-L",
|
|
3145
|
+
`${lineNum},${lineNum}`,
|
|
3146
|
+
"--porcelain",
|
|
3147
|
+
filePath
|
|
3148
|
+
], { cwd: projectPath });
|
|
2322
3149
|
const lines = stdout.split("\n");
|
|
2323
3150
|
const commitHash = lines[0].split(" ")[0];
|
|
2324
3151
|
const author = lines.find((l) => l.startsWith("author "))?.substring(7);
|
|
@@ -2338,9 +3165,23 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2338
3165
|
break;
|
|
2339
3166
|
case "GIT_RECENT_CHANGES":
|
|
2340
3167
|
try {
|
|
2341
|
-
const filePath = message.filePath;
|
|
2342
|
-
|
|
2343
|
-
|
|
3168
|
+
const filePath = path4.resolve(projectPath, message.filePath || "");
|
|
3169
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
3170
|
+
response.error = "Access denied: Path outside project";
|
|
3171
|
+
break;
|
|
3172
|
+
}
|
|
3173
|
+
const days = Number.isFinite(message.days) && message.days > 0 ? Math.floor(message.days) : 7;
|
|
3174
|
+
const { stdout } = await execFileAsync3("git", [
|
|
3175
|
+
"log",
|
|
3176
|
+
"-n",
|
|
3177
|
+
"10",
|
|
3178
|
+
"--since",
|
|
3179
|
+
`${days} days ago`,
|
|
3180
|
+
"--pretty=format:%h|%an|%ad|%s",
|
|
3181
|
+
"--date=short",
|
|
3182
|
+
"--",
|
|
3183
|
+
filePath
|
|
3184
|
+
], { cwd: projectPath });
|
|
2344
3185
|
response.data = {
|
|
2345
3186
|
history: stdout.split("\n").filter(Boolean).map((line) => {
|
|
2346
3187
|
const [hash, author, date, message2] = line.split("|");
|
|
@@ -2353,8 +3194,10 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2353
3194
|
break;
|
|
2354
3195
|
case "GET_IMPORTS":
|
|
2355
3196
|
try {
|
|
2356
|
-
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2357
|
-
if (
|
|
3197
|
+
const filePath = path4.resolve(projectPath, message.filePath || "");
|
|
3198
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
3199
|
+
response.error = "Access denied: Path outside project";
|
|
3200
|
+
} else if (fs4.existsSync(filePath)) {
|
|
2358
3201
|
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2359
3202
|
const importRegex = /import\s+(?:[\w*\s{},]*)\s+from\s+['"]([^'"]+)['"]/g;
|
|
2360
3203
|
const imports = [];
|
|
@@ -2372,8 +3215,17 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2372
3215
|
break;
|
|
2373
3216
|
case "FIND_USAGES":
|
|
2374
3217
|
try {
|
|
2375
|
-
const query = message.query;
|
|
2376
|
-
|
|
3218
|
+
const query = typeof message.query === "string" ? message.query : "";
|
|
3219
|
+
if (!query) {
|
|
3220
|
+
response.error = "Empty query";
|
|
3221
|
+
break;
|
|
3222
|
+
}
|
|
3223
|
+
const { stdout } = await execFileAsync3("git", [
|
|
3224
|
+
"grep",
|
|
3225
|
+
"-n",
|
|
3226
|
+
"--",
|
|
3227
|
+
query
|
|
3228
|
+
], { cwd: projectPath });
|
|
2377
3229
|
response.data = {
|
|
2378
3230
|
usages: stdout.split("\n").filter(Boolean).slice(0, 20).map((line) => {
|
|
2379
3231
|
const parts = line.split(":");
|
|
@@ -2393,8 +3245,10 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2393
3245
|
break;
|
|
2394
3246
|
case "GET_ENV_VARS":
|
|
2395
3247
|
try {
|
|
2396
|
-
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2397
|
-
if (
|
|
3248
|
+
const filePath = path4.resolve(projectPath, message.filePath || "");
|
|
3249
|
+
if (!_isInsideProject(projectPath, filePath)) {
|
|
3250
|
+
response.error = "Access denied: Path outside project";
|
|
3251
|
+
} else if (fs4.existsSync(filePath)) {
|
|
2398
3252
|
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2399
3253
|
const envRegex = /(?:process\.env\.|import\.meta\.env\.)([A-Z_][A-Z0-9_]*)/g;
|
|
2400
3254
|
const vars = /* @__PURE__ */ new Set();
|
|
@@ -2432,7 +3286,7 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2432
3286
|
}
|
|
2433
3287
|
ws.send(JSON.stringify(response));
|
|
2434
3288
|
} catch (e) {
|
|
2435
|
-
console.error(
|
|
3289
|
+
console.error(chalk2.red("Parse error:"), e.message);
|
|
2436
3290
|
}
|
|
2437
3291
|
});
|
|
2438
3292
|
ws.on("message", (rawData) => {
|
|
@@ -2442,6 +3296,14 @@ function attachMessageHandler(ws, projectPath) {
|
|
|
2442
3296
|
const { resolve: resolve3 } = globalBrowserPending.get(msg.id);
|
|
2443
3297
|
globalBrowserPending.delete(msg.id);
|
|
2444
3298
|
resolve3(msg.error ? { error: msg.error } : msg.data ?? {});
|
|
3299
|
+
} else if (msg.type === "AGENT_PROGRESS" && msg.id && globalBrowserPending.has(msg.id)) {
|
|
3300
|
+
const pending = globalBrowserPending.get(msg.id);
|
|
3301
|
+
if (pending.onProgress) {
|
|
3302
|
+
try {
|
|
3303
|
+
pending.onProgress(msg.event);
|
|
3304
|
+
} catch {
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
2445
3307
|
}
|
|
2446
3308
|
} catch {
|
|
2447
3309
|
}
|
|
@@ -2451,53 +3313,53 @@ program.command("connect", { isDefault: true }).alias("c").description('Start ID
|
|
|
2451
3313
|
const port = parseInt(options.port);
|
|
2452
3314
|
const projectPath = process.cwd();
|
|
2453
3315
|
console.log();
|
|
2454
|
-
console.log(
|
|
2455
|
-
console.log(
|
|
3316
|
+
console.log(chalk2.bold.cyan("\u{1F517} Trace IDE Bridge"));
|
|
3317
|
+
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
2456
3318
|
console.log();
|
|
2457
|
-
console.log(`Project: ${
|
|
2458
|
-
console.log(`Port: ${
|
|
3319
|
+
console.log(`Project: ${chalk2.green(projectPath)}`);
|
|
3320
|
+
console.log(`Port: ${chalk2.cyan(port)}`);
|
|
2459
3321
|
console.log();
|
|
2460
3322
|
try {
|
|
2461
3323
|
const pkgPath = path4.join(projectPath, "package.json");
|
|
2462
3324
|
if (fs4.existsSync(pkgPath)) {
|
|
2463
3325
|
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
2464
|
-
console.log(`\u{1F4E6} Package: ${
|
|
3326
|
+
console.log(`\u{1F4E6} Package: ${chalk2.yellow(pkg.name)} v${pkg.version}`);
|
|
2465
3327
|
}
|
|
2466
3328
|
} catch (_) {
|
|
2467
3329
|
}
|
|
2468
3330
|
const wss = new WebSocketServer({ port });
|
|
2469
3331
|
let clientCount = 0;
|
|
2470
3332
|
console.log();
|
|
2471
|
-
console.log(
|
|
2472
|
-
console.log(
|
|
3333
|
+
console.log(chalk2.green("\u2713") + " WebSocket server started");
|
|
3334
|
+
console.log(chalk2.dim("Waiting for extension to connect..."));
|
|
2473
3335
|
console.log();
|
|
2474
|
-
console.log(
|
|
2475
|
-
console.log(
|
|
3336
|
+
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
3337
|
+
console.log(chalk2.dim("Press Ctrl+C to stop"));
|
|
2476
3338
|
console.log();
|
|
2477
3339
|
wss.on("connection", (ws) => {
|
|
2478
3340
|
clientCount++;
|
|
2479
|
-
console.log(
|
|
3341
|
+
console.log(chalk2.green("\u25CF") + ` Extension connected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
2480
3342
|
attachMessageHandler(ws, projectPath);
|
|
2481
3343
|
ws.on("close", () => {
|
|
2482
3344
|
clientCount--;
|
|
2483
|
-
console.log(
|
|
3345
|
+
console.log(chalk2.yellow("\u25CF") + ` Extension disconnected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
2484
3346
|
});
|
|
2485
3347
|
ws.on("error", (error) => {
|
|
2486
|
-
console.error(
|
|
3348
|
+
console.error(chalk2.red("WebSocket error:"), error.message);
|
|
2487
3349
|
});
|
|
2488
3350
|
});
|
|
2489
3351
|
wss.on("error", (error) => {
|
|
2490
3352
|
if (error.code === "EADDRINUSE") {
|
|
2491
|
-
console.log(
|
|
2492
|
-
console.log(
|
|
3353
|
+
console.log(chalk2.red(`\u2717 Port ${port} is already in use`));
|
|
3354
|
+
console.log(chalk2.dim("Try: trace-connect --port 8766"));
|
|
2493
3355
|
} else {
|
|
2494
|
-
console.error(
|
|
3356
|
+
console.error(chalk2.red("Server error:"), error.message);
|
|
2495
3357
|
}
|
|
2496
3358
|
process.exit(1);
|
|
2497
3359
|
});
|
|
2498
3360
|
process.on("SIGINT", () => {
|
|
2499
3361
|
console.log();
|
|
2500
|
-
console.log(
|
|
3362
|
+
console.log(chalk2.dim("Stopping..."));
|
|
2501
3363
|
wss.close();
|
|
2502
3364
|
process.exit(0);
|
|
2503
3365
|
});
|