@gettrace/cli 1.4.24 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1039 -177
  2. package/package.json +2 -3
package/dist/index.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { program } from "commander";
5
- import chalk from "chalk";
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 exec4, spawn } from "child_process";
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 { exec } from "child_process";
34
+ import { execFile } from "child_process";
35
35
  import { promisify } from "util";
36
- var execAsync = promisify(exec);
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 execAsync(`"${prettierBin}" --write "${filePath}"`, { cwd: projectPath });
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 execAsync(`"${eslintBin}" --fix "${filePath}"`, { cwd: projectPath });
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 { exec as exec2 } from "child_process";
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) ? `"${localTsc}"` : "tsc";
149
- const cmd = `${bin} --noEmit --pretty false`;
148
+ const bin = fs2.existsSync(localTsc) ? localTsc : "tsc";
150
149
  try {
151
- const raw = await spawnWithTimeout(cmd, projectPath);
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(cmd, projectPath);
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(cmd, projectPath);
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(cmd, cwd) {
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
- proc.kill("SIGTERM");
254
- reject(new Error(`Checker timed out: ${cmd.slice(0, 60)}`));
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
- const proc = exec2(cmd, { cwd }, (_err, stdout, stderr) => {
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 { exec as exec3 } from "child_process";
293
+ import { execFile as execFile2 } from "child_process";
278
294
  import { promisify as promisify2 } from "util";
279
- var execAsync2 = promisify2(exec3);
295
+ var execFileAsync2 = promisify2(execFile2);
280
296
  async function searchWithRipgrep(projectPath, query, opts) {
281
- const flags = [
297
+ const args = [
282
298
  "--json",
283
299
  "--line-number",
284
- opts.isRegex ? "" : "--fixed-strings",
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
- ].filter(Boolean);
305
- const escapedQuery = query.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
306
- const cmd = `rg ${flags.join(" ")} "${escapedQuery}"`;
307
- const { stdout } = await execAsync2(cmd, {
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 execAsync3 = promisify3(exec4);
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
- 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();
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
- console.log(chalk.dim("Starting dev server..."));
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
- 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
- });
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
- console.log();
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)));
1012
- console.log();
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
- console.log(chalk.green("\u25CF") + ` Extension connected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
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
- console.log(chalk.yellow("\u25CF") + ` Extension disconnected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
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(chalk.red("WebSocket error:"), err.message);
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(chalk.red(`\u2717 Port ${port} is already in use. Try: trace dev --port 8766`));
1596
+ console.error(chalk2.red(`\u2717 Port ${port} is already in use. Try: trace dev --port 8766`));
1036
1597
  } else {
1037
- console.error(chalk.red("Bridge error:"), error.message);
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
- try {
1061
- let body = {};
1062
- if (req.method === "POST") {
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
- 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"));
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(chalk.yellow("\u26A0 Browser HTTP server error:"), e.message);
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
- console.log(chalk.magenta("\u2699") + " Dev Mode: Using local trace agent at " + chalk.dim(agentPath));
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
- 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"));
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
- console.error(chalk.yellow("\u26A0") + " Failed to start Trace Agent Server:", e.message);
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(chalk.yellow("\nShutting down..."));
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 (!filePath.startsWith(projectPath)) {
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 (!filePath.startsWith(projectPath)) {
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 (!filePath.startsWith(projectPath)) {
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(chalk.blue("\u2139") + ` Project detected: ${chalk.yellow(framework)} + ${chalk.cyan(styling)}${typescript ? chalk.dim(" (TS)") : ""}${router ? chalk.dim(` [${router} router]`) : ""}`);
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(chalk.blue("\u2139") + ` Class index built: ${chalk.yellow(classesIndexed)} classes across ${chalk.cyan(filesScanned)} files`);
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 (!filePath.startsWith(projectPath)) {
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(chalk.blue("\u2139") + ` Wrote file: ${message.filePath}` + (formatter ? chalk.dim(` (formatted with ${formatter})`) : "") + chalk.dim(" [undo saved]"));
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(chalk.yellow("\u26A0") + ` ${_wLsp.summary}`);
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 (!filePath.startsWith(projectPath)) {
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(chalk.blue("\u2139") + ` CSS-safe append: hoisted ${dedupedImports.length} @import(s) to top of ${message.filePath}`);
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(chalk.blue("\u2139") + ` Appended file: ${message.filePath}` + (formatter ? chalk.dim(` (formatted with ${formatter})`) : "") + chalk.dim(" [undo saved]"));
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 (!filePath.startsWith(projectPath)) {
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
- chalk.blue("\u2139") + ` EDIT_CLASSNAME: ${message.filePath}` + chalk.dim(` [${result.strategy} @ line ${result.matchedLine}]`) + (formatter ? chalk.dim(` (${formatter})`) : "") + chalk.dim(" [undo saved]")
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 (!filePath.startsWith(projectPath)) {
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" ? "" : 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]"));
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(chalk.yellow("\u26A0") + ` ${_eLsp.summary}`);
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 (!filePath.startsWith(projectPath)) {
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 ? 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]"));
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(chalk.yellow("\u26A0") + ` ${_rLsp.summary}`);
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(chalk.yellow("\u21A9") + ` Undo: deleted ${snapshot.relativePath} (was a new file)`);
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(chalk.yellow("\u21A9") + ` Undo: restored ${snapshot.relativePath} (reverted ${snapshot.operation})`);
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 (!filePath.startsWith(projectPath)) {
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(chalk.yellow("\u{1F5D1}") + ` Soft-deleted: ${message.filePath} \u2192 .trace-trash/${trashName}` + chalk.dim(" [recoverable]"));
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 (!oldPath.startsWith(projectPath) || !newPath.startsWith(projectPath)) {
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(chalk.blue("\u2139") + ` Renamed: ${message.oldPath} \u2192 ${message.newPath}` + chalk.dim(" [undo saved]"));
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
- const line = message.line;
2321
- const { stdout } = await execAsync3(`git blame -L ${line},${line} --porcelain "${filePath}"`, { cwd: projectPath });
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
- 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 });
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 (fs4.existsSync(filePath)) {
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
- const { stdout } = await execAsync3(`git grep -n "${query}"`, { cwd: projectPath });
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 (fs4.existsSync(filePath)) {
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(chalk.red("Parse error:"), e.message);
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(chalk.bold.cyan("\u{1F517} Trace IDE Bridge"));
2455
- console.log(chalk.gray("\u2500".repeat(55)));
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: ${chalk.green(projectPath)}`);
2458
- console.log(`Port: ${chalk.cyan(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: ${chalk.yellow(pkg.name)} v${pkg.version}`);
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(chalk.green("\u2713") + " WebSocket server started");
2472
- console.log(chalk.dim("Waiting for extension to connect..."));
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(chalk.gray("\u2500".repeat(55)));
2475
- console.log(chalk.dim("Press Ctrl+C to stop"));
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(chalk.green("\u25CF") + ` Extension connected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
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(chalk.yellow("\u25CF") + ` Extension disconnected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
3345
+ console.log(chalk2.yellow("\u25CF") + ` Extension disconnected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
2484
3346
  });
2485
3347
  ws.on("error", (error) => {
2486
- console.error(chalk.red("WebSocket error:"), error.message);
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(chalk.red(`\u2717 Port ${port} is already in use`));
2492
- console.log(chalk.dim("Try: trace-connect --port 8766"));
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(chalk.red("Server error:"), error.message);
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(chalk.dim("Stopping..."));
3362
+ console.log(chalk2.dim("Stopping..."));
2501
3363
  wss.close();
2502
3364
  process.exit(0);
2503
3365
  });