@gettrace/cli 2.0.5 → 2.0.7
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 +74 -3414
- package/instrument/trace-instrument.cjs +474 -0
- package/package.json +9 -5
- package/scripts/postinstall.js +14 -3
- package/dist/ast.d.ts +0 -48
- package/dist/ast.js +0 -203
- package/dist/ast.js.map +0 -1
- package/dist/file-lock.d.ts +0 -23
- package/dist/file-lock.js +0 -47
- package/dist/file-lock.js.map +0 -1
- package/dist/format.d.ts +0 -20
- package/dist/format.js +0 -68
- package/dist/format.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js.map +0 -1
- package/dist/lsp.d.ts +0 -46
- package/dist/lsp.js +0 -267
- package/dist/lsp.js.map +0 -1
- package/dist/search.d.ts +0 -31
- package/dist/search.js +0 -169
- package/dist/search.js.map +0 -1
- package/native-host/.homedir +0 -1
- package/native-host/native-host-debug.log +0 -4
package/dist/index.js
CHANGED
|
@@ -1,3421 +1,81 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import{program as ye}from"commander";import m from"chalk";import{WebSocketServer as Me,WebSocket as xt}from"ws";import*as h from"fs";import*as b from"path";import{exec as Tt,execFile as Et,spawn as _t}from"child_process";import{promisify as We}from"util";import{fileURLToPath as Rt}from"url";var ve=new Map;async function ge(o,e){let s=ve.get(o)??Promise.resolve(),r,u=new Promise(a=>r=a);ve.set(o,u);try{return await s,await e()}finally{r(),ve.get(o)===u&&ve.delete(o)}}import*as xe from"fs";import*as pe from"path";import{execFile as Xe}from"child_process";import{promisify as Ue}from"util";var $e=Ue(Xe),Le=new Set([".js",".jsx",".ts",".tsx",".mjs",".cjs",".mts",".cts"]),ze=new Set([".css",".scss",".less"]),Ke=new Set([".html",".htm",".vue",".svelte"]),qe=new Set([...Le,...ze,...Ke,".json",".md",".yaml",".yml"]);async function de(o,e){let s=pe.extname(o).toLowerCase();if(!qe.has(s))return null;let r=pe.join(e,"node_modules",".bin","prettier");if(xe.existsSync(r))try{return await $e(r,["--write",o],{cwd:e}),"prettier"}catch{}if(Le.has(s)){let u=pe.join(e,"node_modules",".bin","eslint");if(xe.existsSync(u))try{return await $e(u,["--fix",o],{cwd:e}),"eslint"}catch{}}return null}import*as ae from"fs";import*as U from"path";import{spawn as Qe}from"child_process";var Te=new Set([".ts",".tsx",".mts",".cts"]),Ee=new Set([".js",".jsx",".mjs",".cjs"]),Ye=new Set([".css",".scss",".less",".sass"]),Ze=6e3,et=20;async function he(o,e){let s={diagnostics:[],summary:null,checkers:[],ran:!1},r=U.extname(o).toLowerCase(),u=[],a=[],n=[];if(Te.has(r)||Ee.has(r)){let p=U.join(e,"tsconfig.json");if(ae.existsSync(p)){let T=Ee.has(r)&&at(p);(Te.has(r)||T)&&(a.push("tsc"),n.push(tt(e)))}}if(Te.has(r)||Ee.has(r)){let p=U.join(e,"node_modules",".bin","eslint");ae.existsSync(p)&&(a.push("eslint"),n.push(nt(p,o,e)))}if(Ye.has(r)){let p=U.join(e,"node_modules",".bin","stylelint");ae.existsSync(p)&&(a.push("stylelint"),n.push(it(p,o,e)))}if(n.length===0)return s;let y=await Promise.allSettled(n);for(let p of y)p.status==="fulfilled"&&u.push(...p.value);let c=new Set,i=u.filter(p=>{let T=`${p.file}:${p.line}:${p.col}:${p.message}`;return c.has(T)?!1:(c.add(T),!0)}),t=U.relative(e,o),f=[...i.filter(p=>p.file===t&&p.severity==="error"),...i.filter(p=>p.file!==t&&p.severity==="error"),...i.filter(p=>p.severity==="warning")].slice(0,et),l=f.filter(p=>p.severity==="error").length,d=f.filter(p=>p.severity==="warning").length,g=null;return l>0?g=`\u26A0 ${l} error${l>1?"s":""}`+(d>0?` + ${d} warning${d>1?"s":""}`:"")+` detected after write [${a.join("+")}]. Fix these before making more edits.`:d>0?g=`\u2139 ${d} warning${d>1?"s":""} detected [${a.join("+")}]. No errors.`:i.length===0&&a.length>0&&(g=`\u2713 No errors detected [${a.join("+")}].`),{diagnostics:f,summary:g,checkers:a,ran:!0}}async function tt(o){let e=U.join(o,"node_modules",".bin","tsc"),s=ae.existsSync(e)?e:"tsc";try{let r=await _e(s,["--noEmit","--pretty","false"],o);return st(r,o)}catch{return[]}}function st(o,e){let s=[],r=/^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/gm,u;for(;(u=r.exec(o))!==null;){let[,a,n,y,c,i,t]=u,f=U.isAbsolute(a)?a:U.resolve(e,a);s.push({file:U.relative(e,f),line:parseInt(n,10),col:parseInt(y,10),severity:c,code:i,message:t.trim(),source:"tsc"})}return s}async function nt(o,e,s){try{let r=await _e(o,["--format","json",e],s);return rt(r,s)}catch{return[]}}function rt(o,e){let s=[],r=o.indexOf("[");if(r===-1)return s;try{let u=JSON.parse(o.slice(r));for(let a of u){let n=U.relative(e,a.filePath);for(let y of a.messages)y.severity!==0&&s.push({file:n,line:y.line??1,col:y.column??1,severity:y.severity>=2?"error":"warning",code:y.ruleId??"eslint",message:y.message,source:"eslint"})}}catch{}return s}async function it(o,e,s){try{let r=await _e(o,["--formatter","json",e],s);return ot(r,s)}catch{return[]}}function ot(o,e){let s=[],r=o.indexOf("[");if(r===-1)return s;try{let u=JSON.parse(o.slice(r));for(let a of u){let n=U.relative(e,a.source);for(let y of a.warnings)s.push({file:n,line:y.line??1,col:y.column??1,severity:y.severity==="error"?"error":"warning",code:y.rule??"stylelint",message:y.text,source:"stylelint"})}}catch{}return s}function _e(o,e,s){return new Promise((r,u)=>{let a=!1,n="",y="",c=Qe(o,e,{cwd:s,shell:!1}),i=setTimeout(()=>{if(!a){a=!0;try{c.kill("SIGTERM")}catch{}u(new Error(`Checker timed out: ${o} ${e.slice(0,2).join(" ")}`))}},Ze);c.stdout?.on("data",t=>{n+=t.toString("utf-8")}),c.stderr?.on("data",t=>{y+=t.toString("utf-8")}),c.on("error",()=>{a||(a=!0,clearTimeout(i),r(""))}),c.on("close",()=>{a||(a=!0,clearTimeout(i),r(n+y))})})}function at(o){try{let e=ae.readFileSync(o,"utf-8");return/"allowJs"\s*:\s*true/.test(e)}catch{return!1}}import*as ue from"fs";import*as ce from"path";import{execFile as ct}from"child_process";import{promisify as lt}from"util";var dt=lt(ct);async function ut(o,e,s){let r=["--json","--line-number",...s.isRegex?[]:["--fixed-strings"],s.caseSensitive?"--case-sensitive":"--ignore-case",`--max-count=${s.maxResults}`,"--glob","!node_modules/**","--glob","!.git/**","--glob","!dist/**","--glob","!build/**","--glob","!.next/**","--glob","!coverage/**","--glob","!.cache/**","--glob","!.turbo/**","--",e],{stdout:u}=await dt("rg",r,{cwd:o,maxBuffer:10*1024*1024}),a=[];for(let n of u.split(`
|
|
3
|
+
`)){let y=n.trim();if(!(!y||!y.startsWith("{")))try{let c=JSON.parse(y);if(c.type!=="match")continue;if(a.push({file:ce.relative(o,c.data.path.text),line:c.data.line_number,content:c.data.lines.text.trimEnd().substring(0,200)}),a.length>=s.maxResults)break}catch{}}return a}var ft=new Set([".js",".ts",".jsx",".tsx",".vue",".svelte",".css",".scss",".sass",".less",".html",".htm",".json",".md"]),gt=new Set(["node_modules",".git","dist","build",".next",".nuxt",".svelte-kit",".cache","coverage",".turbo",".vercel",".output"]);function pt(o,e){if(e.isRegex)try{let r=new RegExp(o,e.caseSensitive?"":"i");return u=>r.test(u)}catch{}if(e.caseSensitive)return r=>r.includes(o);let s=o.toLowerCase();return r=>r.toLowerCase().includes(s)}function ht(o,e,s){let r=[],u=pt(e,s);function a(n){if(r.length>=s.maxResults)return;let y;try{y=ue.readdirSync(n)}catch{return}for(let c of y){if(r.length>=s.maxResults)return;if(gt.has(c)||c.startsWith("."))continue;let i=ce.join(n,c),t;try{t=ue.statSync(i)}catch{continue}if(t.isDirectory())a(i);else if(ft.has(ce.extname(c).toLowerCase())){let f;try{f=ue.readFileSync(i,"utf-8")}catch{continue}let l=f.split(`
|
|
4
|
+
`);for(let d=0;d<l.length&&r.length<s.maxResults;d++)u(l[d])&&r.push({file:ce.relative(o,i),line:d+1,content:l[d].trim().substring(0,200)})}}}return a(o),r}async function Ce(o,e,s={}){let r={isRegex:s.isRegex??!1,caseSensitive:s.caseSensitive??!0,maxResults:s.maxResults??20};try{return{matches:await ut(o,e,r),engine:"ripgrep"}}catch{return{matches:ht(o,e,r),engine:"fs"}}}import{parse as mt}from"@babel/parser";import{createRequire as yt}from"module";import*as ee from"@babel/types";var Ne=yt(import.meta.url),Ae=Ne("@babel/traverse"),Ie=Ne("@babel/generator"),St=Ae.default??Ae,vt=Ie.default??Ie;function De(o,e){let s;try{s=mt(o,{sourceType:"module",plugins:["jsx","typescript"],errorRecovery:!0})}catch(y){return{success:!1,error:`Parse error: ${y instanceof Error?y.message:String(y)}`}}let r=[];if(St(s,{JSXOpeningElement(y){for(let c of y.node.attributes){if(!ee.isJSXAttribute(c)||!ee.isJSXIdentifier(c.name,{name:"className"}))continue;let i=c.loc?.start.line??0,t=0;if(e.lineHint&&i>0){let l=Math.abs(i-e.lineHint);l<=1?t+=100:l<=5?t+=50:l<=15&&(t+=15)}let f=bt(c.value);e.oldValue?f===e.oldValue?t+=80:f?.includes(e.oldValue)?t+=40:f===null&&e.lineHint&&(t+=20):t+=20,t>0&&r.push({nodePath:y,attrNode:c,score:t,line:i})}}}),r.length===0)return{success:!1,error:"No className attribute found matching the given hints."};r.sort((y,c)=>c.score-y.score);let u=r[0],a;return u.score>=180?a="line_exact":u.score>=100?a="line_proximity":u.score>=80?a="value_exact":a="value_partial",u.attrNode.value=ee.stringLiteral(e.newValue),{success:!0,code:vt(s,{retainLines:!0,jsescOption:{minimal:!0}},o).code,strategy:a,matchedLine:u.line}}function bt(o){if(!o)return"";if(ee.isStringLiteral(o))return o.value;if(ee.isJSXExpressionContainer(o)){let e=o.expression;if(ee.isStringLiteral(e))return e.value;if(ee.isTemplateLiteral(e)&&e.expressions.length===0)return e.quasis[0]?.value.cooked??null}return null}import Y from"chalk";import*as Pe from"readline";var H="\x1B[",wt=[/service=tool\.registry/,/service=permission .* (evaluate|evaluated)$/,/service=permission permission=skill/,/service=bus type=.* (subscribing|publishing)/,/service=server method=GET path=\/(session|global\/event|event)/,/service=server status=(started|completed) .* path=\/(session|global\/event|event)/,/service=server status=(started|completed) duration=\d+ method=GET/,/service=lsp\.client .* path=/,/service=lsp\.server tsserver=/,/service=lsp\.server (downloading|removing|building)/,/service=lsp file=/,/service=snapshot hash=.* tracking/,/service=session\.prompt .* resolveTools/,/service=file\.time/,/service=session\.compaction/,/service=session\.processor/,/service=plugin name=.* loading internal plugin/,/service=provider .* (using bundled provider|getSDK)/,/service=db count=\d+ mode=bundled applying migrations/,/service=db path=.* opening database/,/service=format /,/service=bash-tool /,/service=server-proxy/,/service=project .* fromDirectory/,/service=file init/,/service=lsp serverIds=/,/service=vcs branch=.* initialized/,/service=mcp key=.* found$/,/service=mcp .* startup failed/],be=class{currentView="all";logs=[];status;originalStdoutWrite=null;originalStderrWrite=null;headerHeight=0;tabBarRow=0;scrollTop=0;terminalRows=process.stdout.rows||30;terminalCols=process.stdout.columns||100;resizeHandler=null;keyHandler=null;rl=null;isShutdown=!1;MAX_LOGS=5e3;agentRateBucket=0;agentRateResetAt=0;agentDroppedThisSec=0;AGENT_RATE_PER_SEC=60;partialBuffer=new Map;constructor(e){this.status=e}start(){this.installInterceptors(),this.installResizeHandler(),this.installKeyHandler(),this.render()}shutdown(){if(!this.isShutdown){for(let[e,s]of this.partialBuffer)s&&this.pushLine(e,s);if(this.partialBuffer.clear(),this.isShutdown=!0,this.originalStdoutWrite&&(process.stdout.write=this.originalStdoutWrite,this.originalStdoutWrite=null),this.originalStderrWrite&&(process.stderr.write=this.originalStderrWrite,this.originalStderrWrite=null),this.resizeHandler&&(process.stdout.removeListener("resize",this.resizeHandler),this.resizeHandler=null),this.rl){try{this.rl.close()}catch{}this.rl=null}if(process.stdin.isTTY&&process.stdin.setRawMode){try{process.stdin.setRawMode(!1)}catch{}process.stdin.pause()}process.stdout.write(`${H}r${H}?25h${H}${this.terminalRows};1H
|
|
5
|
+
`)}}pushApp(e){this.pushChunk("app",e)}pushTrace(e){let s=e.replace(/\n+$/,"");for(let r of s.split(`
|
|
6
|
+
`))this.pushLine("trace",r)}setStatus(e){Object.assign(this.status,e),this.isShutdown||this.drawHeader()}installKeyHandler(){process.stdin.isTTY&&(Pe.emitKeypressEvents(process.stdin),process.stdin.setRawMode&&process.stdin.setRawMode(!0),process.stdin.resume(),this.keyHandler=(e,s)=>{if(s){if(s.ctrl&&s.name==="c"){process.kill(process.pid,"SIGINT");return}if(s.name==="q"&&!s.ctrl&&!s.meta){process.kill(process.pid,"SIGINT");return}if(s.name==="1")return this.setView("app");if(s.name==="2")return this.setView("agent");if(s.name==="3")return this.setView("all");if(s.name==="c"&&!s.ctrl&&!s.meta){this.clearLogArea();return}}},process.stdin.on("keypress",this.keyHandler))}installResizeHandler(){this.resizeHandler=()=>{this.terminalRows=process.stdout.rows||30,this.terminalCols=process.stdout.columns||100,this.render()},process.stdout.on("resize",this.resizeHandler)}setView(e){this.currentView!==e&&(this.currentView=e,this.drawTabBar(),this.replayLogs())}installInterceptors(){this.originalStdoutWrite=process.stdout.write.bind(process.stdout),this.originalStderrWrite=process.stderr.write.bind(process.stderr);let e=s=>(r,u,a)=>{let n=typeof r=="string"?r:Buffer.isBuffer(r)?r.toString(typeof u=="string"?u:"utf8"):String(r),y=n.trimStart(),i=/^(INFO |WARN |ERROR|DEBUG)\s+\d{4}-\d{2}-\d{2}T/.test(y)?"agent":"trace";return this.pushChunk(i,n),typeof u=="function"?u():typeof a=="function"&&a(),!0};process.stdout.write=e(this.originalStdoutWrite),process.stderr.write=e(this.originalStderrWrite)}pushChunk(e,s){let a=((this.partialBuffer.get(e)??"")+s).split(`
|
|
7
|
+
`),n=a.pop()??"";this.partialBuffer.set(e,n);for(let y of a)this.pushLine(e,y)}pushLine(e,s){if(e==="agent"&&this.isNoise(s))return;if(e==="agent"){let u=Date.now();if(u>this.agentRateResetAt&&(this.agentDroppedThisSec>0&&(this.appendDisplayLine(this.formatLine({source:"trace",time:u,text:Y.dim(`(suppressed ${this.agentDroppedThisSec} agent lines this second)`)})),this.agentDroppedThisSec=0),this.agentRateBucket=0,this.agentRateResetAt=u+1e3),++this.agentRateBucket>this.AGENT_RATE_PER_SEC){this.agentDroppedThisSec++;return}}let r={source:e,text:s,time:Date.now()};this.logs.push(r),this.logs.length>this.MAX_LOGS&&this.logs.shift(),this.matchesView(e)&&this.appendDisplayLine(this.formatLine(r))}isNoise(e){return e?wt.some(s=>s.test(e)):!0}matchesView(e){return this.currentView==="all"?!0:this.currentView==="app"?e==="app":this.currentView==="agent"?e!=="app":!0}formatLine(e){let s=(()=>{switch(e.source){case"app":return Y.cyan("app ");case"agent":return Y.magenta("agent ");case"trace":return Y.yellow("trace ")}})(),r=Y.dim("\u2502");return` ${s}${r} ${e.text}`}render(){this.isShutdown||(this.write(`${H}2J${H}H${H}?25l`),this.drawHeader(),this.drawTabBar(),this.installScrollRegion(),this.replayLogs())}drawHeader(){this.write(`${H}s`),this.write(`${H}H`);let e=Y.cyan,s=Y.bold.cyan,r=Y.dim,u=Y.green,a=Y.red,n=Y.yellow,y=[" \u2572 \u2571 "," \u2572 \u2571 "," \u2572___ ___\u2571 "," \u25CF "," \u2503 \u2503 "," \u2503 \u2503 "],c="T R A C E",i=[];i.push("");for(let D of y)i.push(" "+e(D));i.push(""),i.push(" "+s(c)+" "+r("v"+this.status.cliVersion)),i.push(""),i.push(" "+r("\u2500".repeat(Math.max(40,this.terminalCols-4)))),i.push("");let t=D=>D?u("\u25CF"):a("\u25CF"),f=9,l=(D,A)=>" "+r(D.padEnd(f))+A,d=this.status.extConnected,g=this.status.bridgeReady?d?`${u("\u25CF")} connected ${r("\xB7")} ${this.status.clientCount} `+r(`client${this.status.clientCount===1?"":"s"}`):`${r("\u25CC")} ${r("ready \xB7 waiting for extension")}`:`${n("\u25CB")} starting`,p=this.status.agentReady?d?`${u("\u25CF")} connected`+(this.status.agentExtras?r(` ${this.status.agentExtras}`):""):`${r("\u25CC")} ${r("ready \xB7 waiting for extension")}`+(this.status.agentExtras?r(` ${this.status.agentExtras}`):""):`${n("\u25CB")} starting`,T=this.status.devReady?`${u("\u25CF")} ready`:`${n("\u25CB")} starting`,L=this.truncate(this.status.project,this.terminalCols-18),w=`${this.status.devCommand} ${T}`,R=`ws://localhost:${this.status.bridgePort} ${g}`,k=`http://localhost:${this.status.agentPort} ${p}`,I=`http://localhost:${this.status.browserPort} ${r("for coding-agent CDP queries")}`;i.push(l("project",L)),i.push(l("command",w)),i.push(l("bridge",R)),i.push(l("agent",k)),i.push(l("browser",I)),i.push(""),i.push(" "+r("\u2500".repeat(Math.max(40,this.terminalCols-4))));for(let D of i)this.write(this.padToWidth(D)+`
|
|
8
|
+
`);this.headerHeight=i.length,this.tabBarRow=this.headerHeight+1,this.write(`${H}u`)}drawTabBar(){if(this.isShutdown)return;this.write(`${H}s`),this.write(`${H}${this.tabBarRow};1H`);let e=Y.dim,s=(f,l)=>this.currentView===l?Y.bold.cyan(f):Y.dim(f),r=" "+s("1 App","app")+" "+s("2 Agent","agent")+" "+s("3 All","all"),u=e("c clear \xB7 q quit")+" ",a=f=>f.replace(/\x1B\[[0-9;]*m/g,""),n=a(r).length,y=a(u).length,c=Math.max(2,this.terminalCols-n-y),i=r+" ".repeat(c)+u,t=" "+e("\u2500".repeat(Math.max(40,this.terminalCols-4)));this.write(this.padToWidth(i)+`
|
|
9
|
+
`),this.write(this.padToWidth(t)+`
|
|
10
|
+
`),this.scrollTop=this.tabBarRow+2,this.write(`${H}u`)}installScrollRegion(){this.write(`${H}${this.scrollTop};${this.terminalRows}r`),this.write(`${H}${this.terminalRows};1H`)}clearLogArea(){this.write(`${H}s`);for(let e=this.scrollTop;e<=this.terminalRows;e++)this.write(`${H}${e};1H${H}2K`);this.write(`${H}${this.terminalRows};1H`),this.write(`${H}u`)}replayLogs(){this.clearLogArea(),this.write(`${H}${this.terminalRows};1H`);let e=this.terminalRows-this.scrollTop+1,s=Math.max(0,this.logs.length-e);for(let r=s;r<this.logs.length;r++){let u=this.logs[r];this.matchesView(u.source)&&this.appendDisplayLine(this.formatLine(u))}}appendDisplayLine(e){this.isShutdown||this.write(e+`
|
|
11
|
+
`)}write(e){this.originalStdoutWrite?this.originalStdoutWrite(e):process.stdout.write(e)}truncate(e,s){if(s<=3)return e.slice(0,Math.max(0,s));if(e.length<=s)return e;let r=Math.floor((s-3)*.4),u=s-3-r;return e.slice(0,r)+"..."+e.slice(e.length-u)}padToWidth(e){let s=e.replace(/\x1B\[[0-9;]*m/g,""),r=Math.max(0,this.terminalCols-s.length);return e+" ".repeat(r)}};import{createRequire as $t}from"module";var kt=Rt(import.meta.url),Oe=b.dirname(kt),bs=We(Tt),Re=We(Et);function Q(o,e){return e===o||e.startsWith(o+b.sep)}var ke=class{lines=[];MAX_LINES=500;push(e){let r=e.replace(/\x1B\[[0-9;]*[mGKHFABCDEJsu]/g,"").split(`
|
|
12
|
+
`);for(let u of r)u.trim()!==""&&(this.lines.push(u),this.lines.length>this.MAX_LINES&&this.lines.shift())}getLast(e=100){return this.lines.slice(-e)}clear(){this.lines=[]}get length(){return this.lines.length}},le=new ke,z=null;function Fe(o,e){if(e)return{command:e,pm:"custom",script:e};let s="npm";h.existsSync(b.join(o,"bun.lockb"))?s="bun":h.existsSync(b.join(o,"pnpm-lock.yaml"))?s="pnpm":h.existsSync(b.join(o,"yarn.lock"))&&(s="yarn");let r=b.join(o,"package.json"),u={};if(h.existsSync(r))try{u=JSON.parse(h.readFileSync(r,"utf-8")).scripts||{}}catch{}let n=["dev","start","serve","preview"].find(c=>!!u[c])||"dev";return{command:{npm:`npm run ${n}`,pnpm:`pnpm run ${n}`,yarn:`yarn ${n}`,bun:`bun run ${n}`}[s]??`npm run ${n}`,pm:s,script:n}}var Lt=$t(import.meta.url),Be=Lt("../package.json").version;function Ct(o,e){if(o===""||e==="")return Math.max(o.length,e.length);let s=Array.from({length:o.length+1},(r,u)=>Array.from({length:e.length+1},(a,n)=>u===0?n:n===0?u:0));for(let r=1;r<=o.length;r++)for(let u=1;u<=e.length;u++){let a=o[r-1]===e[u-1]?0:1;s[r][u]=Math.min(s[r-1][u]+1,s[r][u-1]+1,s[r-1][u-1]+a)}return s[o.length][e.length]}var At=function*(o,e){yield e},It=function*(o,e){let s=o.split(`
|
|
13
|
+
`),r=e.split(`
|
|
14
|
+
`);r[r.length-1]===""&&r.pop();for(let u=0;u<=s.length-r.length;u++){let a=!0;for(let n=0;n<r.length;n++)if(s[u+n].trim()!==r[n].trim()){a=!1;break}if(a){let n=0;for(let c=0;c<u;c++)n+=s[c].length+1;let y=n;for(let c=0;c<r.length;c++)y+=s[u+c].length,c<r.length-1&&(y+=1);yield o.substring(n,y)}}},Nt=function*(o,e){let s=o.split(`
|
|
15
|
+
`),r=e.split(`
|
|
16
|
+
`);if(r.length<3)return;r[r.length-1]===""&&r.pop();let u=r[0].trim(),a=r[r.length-1].trim(),n=r.length,y=[];for(let f=0;f<s.length;f++)if(s[f].trim()===u){for(let l=f+2;l<s.length;l++)if(s[l].trim()===a){y.push({startLine:f,endLine:l});break}}if(y.length===0)return;let c=null,i=-1,t=y.length===1?0:.3;for(let f of y){let l=f.endLine-f.startLine+1,d=0,g=Math.min(n-2,l-2);if(g>0)for(let p=1;p<n-1&&p<l-1;p++){let T=s[f.startLine+p].trim(),L=r[p].trim(),w=Math.max(T.length,L.length);w!==0&&(d+=(1-Ct(T,L)/w)/g)}else d=1;d>i&&(i=d,c=f)}if(i>=t&&c){let f=0;for(let d=0;d<c.startLine;d++)f+=s[d].length+1;let l=f;for(let d=c.startLine;d<=c.endLine;d++)l+=s[d].length,d<c.endLine&&(l+=1);yield o.substring(f,l)}},Dt=function*(o,e){let s=n=>n.replace(/\s+/g," ").trim(),r=s(e),u=o.split(`
|
|
17
|
+
`);for(let n of u)s(n)===r&&(yield n);let a=e.split(`
|
|
18
|
+
`);if(a.length>1)for(let n=0;n<=u.length-a.length;n++){let y=u.slice(n,n+a.length);s(y.join(`
|
|
19
|
+
`))===r&&(yield y.join(`
|
|
20
|
+
`))}},Pt=function*(o,e){let s=n=>{let y=n.split(`
|
|
21
|
+
`),c=y.filter(t=>t.trim().length>0);if(c.length===0)return n;let i=Math.min(...c.map(t=>{let f=t.match(/^(\s*)/);return f?f[1].length:0}));return y.map(t=>t.trim().length===0?t:t.slice(i)).join(`
|
|
22
|
+
`)},r=s(e),u=o.split(`
|
|
23
|
+
`),a=e.split(`
|
|
24
|
+
`);for(let n=0;n<=u.length-a.length;n++){let y=u.slice(n,n+a.length).join(`
|
|
25
|
+
`);s(y)===r&&(yield y)}},Ot=function*(o,e){let s=n=>n.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g,(y,c)=>{switch(c){case"n":return`
|
|
26
|
+
`;case"t":return" ";case"r":return"\r";case"'":return"'";case'"':return'"';case"`":return"`";case"\\":return"\\";case`
|
|
27
|
+
`:return`
|
|
28
|
+
`;case"$":return"$";default:return y}}),r=s(e);o.includes(r)&&(yield r);let u=o.split(`
|
|
29
|
+
`),a=r.split(`
|
|
30
|
+
`);for(let n=0;n<=u.length-a.length;n++){let y=u.slice(n,n+a.length).join(`
|
|
31
|
+
`);s(y)===r&&(yield y)}},Ft=function*(o,e){let s=e.trim();if(s===e)return;o.includes(s)&&(yield s);let r=o.split(`
|
|
32
|
+
`),u=e.split(`
|
|
33
|
+
`);for(let a=0;a<=r.length-u.length;a++){let n=r.slice(a,a+u.length).join(`
|
|
34
|
+
`);n.trim()===s&&(yield n)}},jt=function*(o,e){let s=e.split(`
|
|
35
|
+
`);if(s.length<3)return;s[s.length-1]===""&&s.pop();let r=o.split(`
|
|
36
|
+
`),u=s[0].trim(),a=s[s.length-1].trim();for(let n=0;n<r.length;n++)if(r[n].trim()===u){for(let y=n+2;y<r.length;y++)if(r[y].trim()===a){let c=r.slice(n,y+1);if(c.length===s.length){let i=0,t=0;for(let f=1;f<c.length-1;f++){let l=c[f].trim(),d=s[f].trim();(l.length>0||d.length>0)&&(t++,l===d&&i++)}if(t===0||i/t>=.5){yield c.join(`
|
|
37
|
+
`);return}}break}}},Mt=function*(o,e){let s=0;for(;;){let r=o.indexOf(e,s);if(r===-1)break;yield e,s=r+e.length}},Wt=function*(o,e){let s=a=>a.replace(/\n\s*\n/g,`
|
|
38
|
+
`).trim(),r=s(e);if(!r)return;if(s(o).includes(r)){let a=o.split(`
|
|
39
|
+
`),n=e.split(`
|
|
40
|
+
`).filter(i=>i.trim().length>0);if(n.length===0)return;let y=n[0].trim(),c=n[n.length-1].trim();for(let i=0;i<a.length;i++){if(a[i].trim()!==y)continue;let t=-1,f=0;for(let l=i;l<a.length;l++){if(a[l].trim().length>0&&f++,a[l].trim()===c&&f>=n.length){t=l;break}if(l-i>n.length*2+5)break}if(t>=0){let l=a.slice(i,t+1),d=l.filter(p=>p.trim().length>0),g=d.length===n.length;if(g){for(let p=0;p<n.length;p++)if(d[p].trim()!==n[p].trim()){g=!1;break}}if(g){yield l.join(`
|
|
41
|
+
`);return}}}}};function Bt(o,e,s,r=!1){if(e===s)return{error:"No changes: oldString and newString are identical."};let u=[["exact",At],["line-trimmed",It],["block-anchor",Nt],["whitespace-normalized",Dt],["indentation-flexible",Pt],["escape-normalized",Ot],["trimmed-boundary",Ft],["context-aware",jt],["multi-occurrence",Mt],["blank-line-tolerant",Wt]],a=!0;for(let[n,y]of u)for(let c of y(o,e)){let i=o.indexOf(c);if(i===-1)continue;if(a=!1,r)return{result:o.replaceAll(c,s),strategy:n};let t=o.lastIndexOf(c);if(i===t)return{result:o.substring(0,i)+s+o.substring(i+c.length),strategy:n}}return a?{error:"Could not find oldString in the file. Tried 10 matching strategies including fuzzy whitespace, indentation, blank-line-tolerant, and block-anchor matching."}:{error:"Found multiple matches for oldString. Provide more surrounding context to make the match unique."}}ye.name("trace").description("Trace IDE Bridge \u2014 connect your codebase and dev server to the Trace extension").version(Be);ye.command("install-native").description("Register the Trace native messaging host so the Chrome extension can auto-launch trace dev").action(async()=>{let{execFileSync:o}=await import("child_process"),{fileURLToPath:e}=await import("url"),{dirname:s,join:r}=await import("path"),u=s(e(import.meta.url)),a=r(u,"..","scripts","postinstall.js");try{o(process.execPath,[a],{stdio:"inherit"})}catch(n){console.error(m.red("\u2717 Native host registration failed:"),n.message),process.exit(1)}});ye.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(o,e)=>{let s=parseInt(e.port),r=process.cwd(),{command:u,pm:a,script:n}=Fe(r,o),c=e.ui!==!1&&process.stdout.isTTY?new be({project:r,pm:a,devCommand:u,bridgePort:s,agentPort:8766,browserPort:8767,bridgeReady:!1,agentReady:!1,devReady:!1,extConnected:!1,clientCount:0,agentExtras:"",cliVersion:Be}):null;c&&c.start();let i=S=>{c?c.pushTrace(S):console.log(S)},t=S=>{c?c.pushTrace(m.red(S)):console.error(S)};c||(console.log(),console.log(m.bold.cyan("\u26A1 Trace Dev")),console.log(m.gray("\u2500".repeat(55))),console.log(),console.log(`\u{1F4C1} Project: ${m.green(r)}`),console.log(`\u{1F4E6} Package Mgr: ${m.yellow(a)}`),console.log(`\u{1F680} Dev Command: ${m.cyan(u)}`),console.log(`\u{1F310} Bridge Port: ${m.cyan(s)}`),console.log(),console.log(m.gray("\u2500".repeat(55))),console.log());let f=new Set,l=S=>{let v=JSON.stringify(S);for(let x of f)x.readyState===xt.OPEN&&x.send(v)};i(m.dim("Starting dev server..."));let d={...process.env,FORCE_COLOR:"1"};delete d.MallocNanoZone,delete d.MallocStackLogging,delete d.MallocScribble,delete d.MallocGuardEdges,delete d.MallocErrorAbort;function g(){let S=p(r,o);if(S.ok)return S;if(!r.replace(/\/+$/,"").endsWith("/Trace/greenfield"))try{let x=h.readdirSync(r,{withFileTypes:!0});for(let $ of x){if(!$.isDirectory()||$.name.startsWith(".")||$.name==="node_modules")continue;let M=b.join(r,$.name),Z=p(M,void 0);if(Z.ok)return Z}}catch{}return{ok:!1}}function p(S,v){let x=b.join(S,"package.json");if(!h.existsSync(x))return{ok:!1};try{let M=JSON.parse(h.readFileSync(x,"utf-8")).scripts||{};if(!["dev","start","serve","preview"].find(E=>!!M[E]))return{ok:!1};let _=Fe(S,v);return{ok:!0,cmd:_.command,script:_.script,cwd:S}}catch{return{ok:!1}}}function T(S,v=r){if(z&&!z.killed){i(m.dim(`[Trace] Dev process already running \u2014 skipping duplicate spawn for ${v}`));return}z=_t(S,[],{cwd:v,shell:!0,env:d}),z.stdout?.on("data",x=>{let $=x.toString();c?c.pushApp($):process.stdout.write($),le.push($),l({type:"STREAM_CHUNK",stream:"stdout",chunk:$}),c&&/\b(Ready in|ready in|listening on|Local:)\b/i.test($)&&c.setStatus({devReady:!0})}),z.stderr?.on("data",x=>{let $=x.toString();c?c.pushApp($):process.stderr.write($),le.push($),l({type:"STREAM_CHUNK",stream:"stderr",chunk:$})}),z.on("close",x=>{let $=`[Trace] Dev server exited with code ${x}`;c?c.pushTrace(m.yellow($)):process.stdout.write(m.yellow($)+`
|
|
42
|
+
`),le.push($),l({type:"STREAM_END",exitCode:x}),c&&c.setStatus({devReady:!1}),z=null}),z.on("error",x=>{let $=`[Trace] Failed to start dev server: ${x.message}`;t($),t(`\u2717 Could not start dev server. Command: ${S}`)})}let L=new Me({port:s}),w=0;L.on("listening",()=>{c?(c.setStatus({bridgeReady:!0}),c.pushTrace(m.green("\u2713")+` IDE Bridge listening on port ${s}`),c.pushTrace(m.dim("Waiting for Trace extension to connect..."))):(console.log(),console.log(m.gray("\u2500".repeat(55))),console.log(m.green("\u2713")+" IDE Bridge listening on port "+m.cyan(s)),console.log(m.dim("Waiting for Trace extension to connect...")),console.log(m.dim("Press Ctrl+C to stop both")),console.log(m.gray("\u2500".repeat(55))),console.log())}),L.on("connection",S=>{w++,f.add(S),c?(c.setStatus({extConnected:!0,clientCount:w}),c.pushTrace(m.green("\u25CF")+` Extension connected (${w} client${w>1?"s":""})`)):console.log(m.green("\u25CF")+` Extension connected (${w} client${w>1?"s":""})`),Ge(S,r);let v=le.getLast(100);v.length>0&&S.send(JSON.stringify({type:"TERMINAL_CATCHUP",lines:v})),S.on("close",()=>{w--,f.delete(S),c?(c.setStatus({extConnected:w>0,clientCount:w}),c.pushTrace(m.yellow("\u25CF")+` Extension disconnected (${w} client${w>1?"s":""})`)):console.log(m.yellow("\u25CF")+` Extension disconnected (${w} client${w>1?"s":""})`)}),S.on("error",x=>{console.error(m.red("WebSocket error:"),x.message),f.delete(S)})}),L.on("error",S=>{S.code==="EADDRINUSE"?console.error(m.red(`\u2717 Port ${s} is already in use. Try: trace dev --port 8766`)):console.error(m.red("Bridge error:"),S.message)});let k=(await import("http")).createServer(async(S,v)=>{if(v.setHeader("Access-Control-Allow-Origin","*"),v.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),v.setHeader("Access-Control-Allow-Headers","Content-Type"),S.method==="OPTIONS"){v.writeHead(204),v.end();return}let x=new URL(S.url,"http://localhost");if(x.pathname.startsWith("/__trace/")){if(x.pathname==="/__trace/ingest"&&S.method==="POST"){let E="",G=!1;S.on("data",W=>{E+=W.toString(),E.length>8e6&&(G=!0,S.destroy())}),S.on("end",()=>{if(!G)try{let W=JSON.parse(E||"{}");Jt(W.service,Array.isArray(W.spans)?W.spans:[])}catch{}try{v.writeHead(200,{"Content-Type":"application/json"}),v.end('{"ok":true}')}catch{}}),S.on("error",()=>{try{v.writeHead(400),v.end()}catch{}});return}if(x.pathname==="/__trace/traces"){let E=me.slice().reverse().map(G=>{let W=fe.get(G);return W?He(W):null}).filter(Boolean);v.writeHead(200,{"Content-Type":"application/json"}),v.end(JSON.stringify({traces:E}));return}let _=x.pathname.match(/^\/__trace\/traces\/([A-Za-z0-9]+)$/);if(_){let E=fe.get(_[1]);if(!E){v.writeHead(404,{"Content-Type":"application/json"}),v.end('{"error":"not found"}');return}v.writeHead(200,{"Content-Type":"application/json"}),v.end(JSON.stringify(Je(E)));return}if(x.pathname==="/__trace/stream"){v.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache, no-transform",Connection:"keep-alive","X-Accel-Buffering":"no"});try{v.write(`data: ${JSON.stringify({type:"hello"})}
|
|
2
43
|
|
|
3
|
-
|
|
4
|
-
import { program } from "commander";
|
|
5
|
-
import chalk2 from "chalk";
|
|
6
|
-
import { WebSocketServer, WebSocket } from "ws";
|
|
7
|
-
import * as fs4 from "fs";
|
|
8
|
-
import * as path4 from "path";
|
|
9
|
-
import { exec, execFile as execFile3, spawn as spawn2 } from "child_process";
|
|
10
|
-
import { promisify as promisify3 } from "util";
|
|
11
|
-
import { fileURLToPath } from "url";
|
|
44
|
+
`)}catch{}we.add(v);let E=setInterval(()=>{try{v.writableEnded||v.write(`: ka
|
|
12
45
|
|
|
13
|
-
|
|
14
|
-
var _locks = /* @__PURE__ */ new Map();
|
|
15
|
-
async function withFileLock(filePath, fn) {
|
|
16
|
-
const prior = _locks.get(filePath) ?? Promise.resolve();
|
|
17
|
-
let release;
|
|
18
|
-
const hold = new Promise((resolve3) => release = resolve3);
|
|
19
|
-
_locks.set(filePath, hold);
|
|
20
|
-
try {
|
|
21
|
-
await prior;
|
|
22
|
-
return await fn();
|
|
23
|
-
} finally {
|
|
24
|
-
release();
|
|
25
|
-
if (_locks.get(filePath) === hold) {
|
|
26
|
-
_locks.delete(filePath);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
46
|
+
`)}catch{}},15e3);E.unref&&E.unref(),S.on("close",()=>{clearInterval(E),we.delete(v)});return}if(x.pathname==="/__trace/clear"){fe.clear(),me.length=0,v.writeHead(200,{"Content-Type":"application/json"}),v.end('{"ok":true}');return}v.writeHead(404,{"Content-Type":"application/json"}),v.end('{"error":"unknown trace route"}');return}let $=x.pathname.match(/^\/agent\/([a-z_-]+)$/);if($){let _=$[1],E=[...f].find(O=>O.readyState===1);if(!E){v.writeHead(503,{"Content-Type":"application/json"}),v.end(JSON.stringify({error:"Trace extension not connected."}));return}let G={};if(S.method==="POST")try{G=await new Promise((O,V)=>{let re="";S.on("data",oe=>re+=oe.toString()),S.on("end",()=>{try{O(JSON.parse(re||"{}"))}catch{O({})}}),S.on("error",V)})}catch{v.writeHead(400,{"Content-Type":"application/json"}),v.end(JSON.stringify({error:"Invalid request body"}));return}v.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache, no-transform",Connection:"keep-alive","X-Accel-Buffering":"no"});let W=O=>{try{v.writableEnded||v.write(`data: ${JSON.stringify(O)}
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
import * as path from "path";
|
|
34
|
-
import { execFile } from "child_process";
|
|
35
|
-
import { promisify } from "util";
|
|
36
|
-
var execFileAsync = promisify(execFile);
|
|
37
|
-
var JS_EXTS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
38
|
-
var CSS_EXTS = /* @__PURE__ */ new Set([".css", ".scss", ".less"]);
|
|
39
|
-
var HTML_EXTS = /* @__PURE__ */ new Set([".html", ".htm", ".vue", ".svelte"]);
|
|
40
|
-
var ALL_FORMATTABLE = /* @__PURE__ */ new Set([
|
|
41
|
-
...JS_EXTS,
|
|
42
|
-
...CSS_EXTS,
|
|
43
|
-
...HTML_EXTS,
|
|
44
|
-
".json",
|
|
45
|
-
".md",
|
|
46
|
-
".yaml",
|
|
47
|
-
".yml"
|
|
48
|
-
]);
|
|
49
|
-
async function autoFormat(filePath, projectPath) {
|
|
50
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
51
|
-
if (!ALL_FORMATTABLE.has(ext))
|
|
52
|
-
return null;
|
|
53
|
-
const prettierBin = path.join(projectPath, "node_modules", ".bin", "prettier");
|
|
54
|
-
if (fs.existsSync(prettierBin)) {
|
|
55
|
-
try {
|
|
56
|
-
await execFileAsync(prettierBin, ["--write", filePath], { cwd: projectPath });
|
|
57
|
-
return "prettier";
|
|
58
|
-
} catch {
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if (JS_EXTS.has(ext)) {
|
|
62
|
-
const eslintBin = path.join(projectPath, "node_modules", ".bin", "eslint");
|
|
63
|
-
if (fs.existsSync(eslintBin)) {
|
|
64
|
-
try {
|
|
65
|
-
await execFileAsync(eslintBin, ["--fix", filePath], { cwd: projectPath });
|
|
66
|
-
return "eslint";
|
|
67
|
-
} catch {
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
48
|
+
`)}catch{}};W({type:"agent_start",agent:_});let P=++je,N=setTimeout(()=>{if(ne.get(P)){ne.delete(P),W({type:"error",error:"Agent timeout (65s)"});try{v.end()}catch{}}},65e3);S.on("close",()=>{ne.has(P)&&(clearTimeout(N),ne.delete(P))}),ne.set(P,{resolve:O=>{clearTimeout(N),W({type:"result",data:O});try{v.end()}catch{}},reject:O=>{clearTimeout(N),W({type:"error",error:O?.message||String(O)});try{v.end()}catch{}},timer:N,onProgress:O=>W(O)}),E.send(JSON.stringify({id:P,type:"AGENT_INVOKE",agent:_,query:G.query||""}));return}let M={"/browser/console":"BROWSER_GET_CONSOLE","/browser/network":"BROWSER_GET_NETWORK","/browser/dom":"BROWSER_GET_DOM","/browser/screenshot":"BROWSER_SCREENSHOT","/browser/verify-build":"BROWSER_VERIFY_BUILD","/browser/find":"BROWSER_FIND","/browser/click":"BROWSER_CLICK","/browser/type":"BROWSER_TYPE","/browser/wait-for":"BROWSER_WAIT_FOR"};if(!(x.pathname in M||x.pathname==="/browser/eval")){v.writeHead(404,{"Content-Type":"application/json"}),v.end(JSON.stringify({error:"Not found. Available: /browser/{console,network,dom,eval,screenshot,verify-build,find,click,type,wait-for}"}));return}let C=[...f].find(_=>_.readyState===1);if(!C){v.writeHead(503,{"Content-Type":"application/json"}),v.end(JSON.stringify({error:"Trace extension not connected. Open the extension and attach to a tab."}));return}try{let _={};S.method==="POST"&&(_=await new Promise((V,re)=>{let oe="";S.on("data",Se=>oe+=Se.toString()),S.on("end",()=>{try{V(JSON.parse(oe||"{}"))}catch{V({})}}),S.on("error",re)}));let E=++je,G=M[x.pathname]||"BROWSER_EVAL",W={id:E,type:G},P={..._||{}};for(let[V,re]of x.searchParams.entries())V in P||(P[V]=re);for(let V of Object.keys(P))V==="id"||V==="type"||(W[V]=P[V]);C.send(JSON.stringify(W));let N=x.pathname==="/browser/screenshot"?15e3:x.pathname==="/browser/wait-for"?Math.min(Number(P.timeout)||8e3,3e4)+4e3:5e3,O=await new Promise((V,re)=>{let oe=setTimeout(()=>{ne.delete(E),re(new Error("Browser query timeout ("+Math.round(N/1e3)+"s). Extension may not be attached to a tab."))},N);ne.set(E,{resolve:V,reject:re,timer:oe})});v.writeHead(200,{"Content-Type":"application/json"}),v.end(JSON.stringify(O,null,2))}catch(_){v.writeHead(500,{"Content-Type":"application/json"}),v.end(JSON.stringify({error:_.message}))}}),I=8767;k.listen(I,"127.0.0.1",()=>{c?c.pushTrace(m.green("\u2713")+` Browser Query HTTP server on port ${I}`):(console.log(m.green("\u2713")+" Browser Query HTTP server on port "+m.cyan(I)),console.log(m.dim(" Coding agents can query: curl http://localhost:8767/browser/console")))}),k.on("error",S=>{S.code!=="EADDRINUSE"&&console.warn(m.yellow("\u26A0 Browser HTTP server error:"),S.message)});let D=null;try{let S="@gettrace/agent/dist/node/index.js";process.env.TRACE_DEV_MODE&&(S=b.resolve(Oe,"../../packages/trace-agent/dist/node/index.js"),c?c.pushTrace(m.magenta("\u2699")+" Dev Mode: Using local trace agent at "+m.dim(S)):console.log(m.magenta("\u2699")+" Dev Mode: Using local trace agent at "+m.dim(S)));let{Server:v}=await import(S);try{let x=b.resolve(Oe,"..","instrument","trace-instrument.cjs");h.existsSync(x)&&(process.env.TRACE_INGEST_URL||(process.env.TRACE_INGEST_URL=`http://127.0.0.1:${I}/__trace/ingest`),process.env.TRACE_INSTRUMENT_PATH=x)}catch{}process.env.OPENCODE_EXPERIMENTAL="1",process.env.OPENCODE_ENABLE_EXA="1",process.env.OPENCODE_EXPERIMENTAL_LSP_TOOL="1",process.env.OPENCODE_ENABLE_QUESTION_TOOL="1",process.env.OPENCODE_CLIENT="app",process.env.OPENCODE_DISABLE_AUTOUPDATE="1",process.env.OPENCODE_DISABLE_TERMINAL_TITLE="1",D=await v.listen({port:8766,hostname:"127.0.0.1",cors:["*"]}),c?(c.setStatus({agentReady:!0,agentExtras:"Sonnet 4.5 \xB7 LSP \xB7 Exa \xB7 Plan"}),c.pushTrace(m.green("\u2713")+" Trace Agent Server listening on port 8766")):(console.log(m.green("\u2713")+" Trace Agent Server listening on port "+m.cyan(8766)),console.log(m.dim(" LSP \xB7 Exa search \xB7 Plan mode \xB7 All tools unlocked")))}catch(S){let v="\u26A0 Failed to start Trace Agent Server: "+S.message;c?c.pushTrace(m.yellow(v)):console.error(m.yellow(v))}let A=null,F=0,B=null,j=null;function J(S){if(!(!Array.isArray(S)||!S.length)){F>=S.length&&(F=Math.max(0,S.length-100));for(let v of S.slice(F)){let x=v+`
|
|
49
|
+
`;c?c.pushApp(x):process.stdout.write(x),le.push(x),l({type:"STREAM_CHUNK",stream:"stdout",chunk:x})}F=S.length}}function te(){B&&(clearInterval(B),B=null)}function se(S){te(),B=setInterval(async()=>{try{let v=await fetch(`http://127.0.0.1:8766/dev-server/${encodeURIComponent(S)}`);if(!v.ok)return;let x=await v.json().catch(()=>null);if(!x)return;J(x.output||[]);let $=x.state?.status;$==="exited"||$==="stopped"||$==="failed"?(te(),c&&c.setStatus({devReady:!1})):$==="ready"&&c&&c.setStatus({devReady:!0})}catch{}},500)}async function ie(){let S=j;if(S)for(;!S.signal.aborted;){try{let v=await fetch("http://127.0.0.1:8766/global/event",{signal:S.signal,headers:{Accept:"text/event-stream"}});if(!v.ok||!v.body){await new Promise(Z=>setTimeout(Z,1e3));continue}let x=v.body.getReader(),$=new TextDecoder,M="";for(;!S.signal.aborted;){let{value:Z,done:C}=await x.read();if(C)break;M+=$.decode(Z,{stream:!0});let _;for(;(_=M.indexOf(`
|
|
73
50
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
import * as path2 from "path";
|
|
77
|
-
import { spawn } from "child_process";
|
|
78
|
-
var TS_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".cts"]);
|
|
79
|
-
var JS_EXTS2 = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs"]);
|
|
80
|
-
var CSS_EXTS2 = /* @__PURE__ */ new Set([".css", ".scss", ".less", ".sass"]);
|
|
81
|
-
var CHECKER_TIMEOUT_MS = 6e3;
|
|
82
|
-
var MAX_DIAGNOSTICS = 20;
|
|
83
|
-
async function checkDiagnostics(filePath, projectPath) {
|
|
84
|
-
const SKIP = { diagnostics: [], summary: null, checkers: [], ran: false };
|
|
85
|
-
const ext = path2.extname(filePath).toLowerCase();
|
|
86
|
-
const all = [];
|
|
87
|
-
const checkers = [];
|
|
88
|
-
const pending = [];
|
|
89
|
-
if (TS_EXTS.has(ext) || JS_EXTS2.has(ext)) {
|
|
90
|
-
const tsconfig = path2.join(projectPath, "tsconfig.json");
|
|
91
|
-
if (fs2.existsSync(tsconfig)) {
|
|
92
|
-
const shouldRunForJs = JS_EXTS2.has(ext) && tsconfigAllowsJs(tsconfig);
|
|
93
|
-
if (TS_EXTS.has(ext) || shouldRunForJs) {
|
|
94
|
-
checkers.push("tsc");
|
|
95
|
-
pending.push(runTsc(projectPath));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (TS_EXTS.has(ext) || JS_EXTS2.has(ext)) {
|
|
100
|
-
const eslintBin = path2.join(projectPath, "node_modules", ".bin", "eslint");
|
|
101
|
-
if (fs2.existsSync(eslintBin)) {
|
|
102
|
-
checkers.push("eslint");
|
|
103
|
-
pending.push(runEslint(eslintBin, filePath, projectPath));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (CSS_EXTS2.has(ext)) {
|
|
107
|
-
const stylelintBin = path2.join(projectPath, "node_modules", ".bin", "stylelint");
|
|
108
|
-
if (fs2.existsSync(stylelintBin)) {
|
|
109
|
-
checkers.push("stylelint");
|
|
110
|
-
pending.push(runStylelint(stylelintBin, filePath, projectPath));
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (pending.length === 0)
|
|
114
|
-
return SKIP;
|
|
115
|
-
const results = await Promise.allSettled(pending);
|
|
116
|
-
for (const r of results) {
|
|
117
|
-
if (r.status === "fulfilled")
|
|
118
|
-
all.push(...r.value);
|
|
119
|
-
}
|
|
120
|
-
const seen = /* @__PURE__ */ new Set();
|
|
121
|
-
const unique = all.filter((d) => {
|
|
122
|
-
const key = `${d.file}:${d.line}:${d.col}:${d.message}`;
|
|
123
|
-
if (seen.has(key))
|
|
124
|
-
return false;
|
|
125
|
-
seen.add(key);
|
|
126
|
-
return true;
|
|
127
|
-
});
|
|
128
|
-
const rel = path2.relative(projectPath, filePath);
|
|
129
|
-
const sorted = [
|
|
130
|
-
...unique.filter((d) => d.file === rel && d.severity === "error"),
|
|
131
|
-
...unique.filter((d) => d.file !== rel && d.severity === "error"),
|
|
132
|
-
...unique.filter((d) => d.severity === "warning")
|
|
133
|
-
].slice(0, MAX_DIAGNOSTICS);
|
|
134
|
-
const errCount = sorted.filter((d) => d.severity === "error").length;
|
|
135
|
-
const warnCount = sorted.filter((d) => d.severity === "warning").length;
|
|
136
|
-
let summary = null;
|
|
137
|
-
if (errCount > 0) {
|
|
138
|
-
summary = `\u26A0 ${errCount} error${errCount > 1 ? "s" : ""}` + (warnCount > 0 ? ` + ${warnCount} warning${warnCount > 1 ? "s" : ""}` : "") + ` detected after write [${checkers.join("+")}]. Fix these before making more edits.`;
|
|
139
|
-
} else if (warnCount > 0) {
|
|
140
|
-
summary = `\u2139 ${warnCount} warning${warnCount > 1 ? "s" : ""} detected [${checkers.join("+")}]. No errors.`;
|
|
141
|
-
} else if (unique.length === 0 && checkers.length > 0) {
|
|
142
|
-
summary = `\u2713 No errors detected [${checkers.join("+")}].`;
|
|
143
|
-
}
|
|
144
|
-
return { diagnostics: sorted, summary, checkers, ran: true };
|
|
145
|
-
}
|
|
146
|
-
async function runTsc(projectPath) {
|
|
147
|
-
const localTsc = path2.join(projectPath, "node_modules", ".bin", "tsc");
|
|
148
|
-
const bin = fs2.existsSync(localTsc) ? localTsc : "tsc";
|
|
149
|
-
try {
|
|
150
|
-
const raw = await spawnWithTimeout(bin, ["--noEmit", "--pretty", "false"], projectPath);
|
|
151
|
-
return parseTscOutput(raw, projectPath);
|
|
152
|
-
} catch {
|
|
153
|
-
return [];
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
function parseTscOutput(output, projectPath) {
|
|
157
|
-
const diagnostics = [];
|
|
158
|
-
const pattern = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/gm;
|
|
159
|
-
let m;
|
|
160
|
-
while ((m = pattern.exec(output)) !== null) {
|
|
161
|
-
const [, rawFile, lineStr, colStr, severity, code, message] = m;
|
|
162
|
-
const absFile = path2.isAbsolute(rawFile) ? rawFile : path2.resolve(projectPath, rawFile);
|
|
163
|
-
diagnostics.push({
|
|
164
|
-
file: path2.relative(projectPath, absFile),
|
|
165
|
-
line: parseInt(lineStr, 10),
|
|
166
|
-
col: parseInt(colStr, 10),
|
|
167
|
-
severity,
|
|
168
|
-
code,
|
|
169
|
-
message: message.trim(),
|
|
170
|
-
source: "tsc"
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
return diagnostics;
|
|
174
|
-
}
|
|
175
|
-
async function runEslint(eslintBin, filePath, projectPath) {
|
|
176
|
-
try {
|
|
177
|
-
const raw = await spawnWithTimeout(eslintBin, ["--format", "json", filePath], projectPath);
|
|
178
|
-
return parseEslintOutput(raw, projectPath);
|
|
179
|
-
} catch {
|
|
180
|
-
return [];
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
function parseEslintOutput(raw, projectPath) {
|
|
184
|
-
const diagnostics = [];
|
|
185
|
-
const jsonStart = raw.indexOf("[");
|
|
186
|
-
if (jsonStart === -1)
|
|
187
|
-
return diagnostics;
|
|
188
|
-
try {
|
|
189
|
-
const results = JSON.parse(raw.slice(jsonStart));
|
|
190
|
-
for (const fileResult of results) {
|
|
191
|
-
const relFile = path2.relative(projectPath, fileResult.filePath);
|
|
192
|
-
for (const msg of fileResult.messages) {
|
|
193
|
-
if (msg.severity === 0)
|
|
194
|
-
continue;
|
|
195
|
-
diagnostics.push({
|
|
196
|
-
file: relFile,
|
|
197
|
-
line: msg.line ?? 1,
|
|
198
|
-
col: msg.column ?? 1,
|
|
199
|
-
severity: msg.severity >= 2 ? "error" : "warning",
|
|
200
|
-
code: msg.ruleId ?? "eslint",
|
|
201
|
-
message: msg.message,
|
|
202
|
-
source: "eslint"
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
} catch {
|
|
207
|
-
}
|
|
208
|
-
return diagnostics;
|
|
209
|
-
}
|
|
210
|
-
async function runStylelint(stylelintBin, filePath, projectPath) {
|
|
211
|
-
try {
|
|
212
|
-
const raw = await spawnWithTimeout(stylelintBin, ["--formatter", "json", filePath], projectPath);
|
|
213
|
-
return parseStylelintOutput(raw, projectPath);
|
|
214
|
-
} catch {
|
|
215
|
-
return [];
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
function parseStylelintOutput(raw, projectPath) {
|
|
219
|
-
const diagnostics = [];
|
|
220
|
-
const jsonStart = raw.indexOf("[");
|
|
221
|
-
if (jsonStart === -1)
|
|
222
|
-
return diagnostics;
|
|
223
|
-
try {
|
|
224
|
-
const results = JSON.parse(raw.slice(jsonStart));
|
|
225
|
-
for (const fileResult of results) {
|
|
226
|
-
const relFile = path2.relative(projectPath, fileResult.source);
|
|
227
|
-
for (const w of fileResult.warnings) {
|
|
228
|
-
diagnostics.push({
|
|
229
|
-
file: relFile,
|
|
230
|
-
line: w.line ?? 1,
|
|
231
|
-
col: w.column ?? 1,
|
|
232
|
-
severity: w.severity === "error" ? "error" : "warning",
|
|
233
|
-
code: w.rule ?? "stylelint",
|
|
234
|
-
message: w.text,
|
|
235
|
-
source: "stylelint"
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
} catch {
|
|
240
|
-
}
|
|
241
|
-
return diagnostics;
|
|
242
|
-
}
|
|
243
|
-
function spawnWithTimeout(bin, args, cwd) {
|
|
244
|
-
return new Promise((resolve3, reject) => {
|
|
245
|
-
let settled = false;
|
|
246
|
-
let stdout = "";
|
|
247
|
-
let stderr = "";
|
|
248
|
-
const proc = spawn(bin, args, { cwd, shell: false });
|
|
249
|
-
const timer = setTimeout(() => {
|
|
250
|
-
if (settled)
|
|
251
|
-
return;
|
|
252
|
-
settled = true;
|
|
253
|
-
try {
|
|
254
|
-
proc.kill("SIGTERM");
|
|
255
|
-
} catch {
|
|
256
|
-
}
|
|
257
|
-
reject(new Error(`Checker timed out: ${bin} ${args.slice(0, 2).join(" ")}`));
|
|
258
|
-
}, CHECKER_TIMEOUT_MS);
|
|
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", () => {
|
|
273
|
-
if (settled)
|
|
274
|
-
return;
|
|
275
|
-
settled = true;
|
|
276
|
-
clearTimeout(timer);
|
|
277
|
-
resolve3(stdout + stderr);
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
function tsconfigAllowsJs(tsconfigPath) {
|
|
282
|
-
try {
|
|
283
|
-
const raw = fs2.readFileSync(tsconfigPath, "utf-8");
|
|
284
|
-
return /"allowJs"\s*:\s*true/.test(raw);
|
|
285
|
-
} catch {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
51
|
+
`))!==-1;){let E=M.slice(0,_);M=M.slice(_+2);let G=E.split(`
|
|
52
|
+
`).find(P=>P.startsWith("data:"));if(!G)continue;let W=G.replace(/^data:\s*/,"");try{let P=JSON.parse(W),N=P?.payload?.type,O=P?.payload?.properties;if(!N||!O||O.id&&A&&O.id!==A)continue;N==="dev_server.starting"?!A&&O.id&&(A=O.id,se(O.id)):N==="dev_server.ready"?c&&c.setStatus({devReady:!0}):(N==="dev_server.failed"||N==="dev_server.exited")&&c&&c.setStatus({devReady:!1})}catch{}}}}catch{if(S.signal.aborted)return}await new Promise(v=>setTimeout(v,1e3))}}async function K(S,v){if(A)return i(m.dim(`[Trace] Dev server already managed by agent (id=${A}) \u2014 skipping duplicate`)),{ok:!0};j=new AbortController,ie();let x;try{x=await fetch("http://127.0.0.1:8766/dev-server",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({command:S,cwd:v,timeoutMs:9e4})})}catch(M){return{ok:!1,reason:M?.message??String(M)}}if(!x.ok){let M=await x.text().catch(()=>"");return{ok:!1,reason:`HTTP ${x.status}: ${M||x.statusText}`}}let $=await x.json().catch(()=>null);return $?.id?(A=$.id,J($.output||[]),se($.id),$.state?.status==="ready"?(c&&c.setStatus({devReady:!0}),{ok:!0}):{ok:!1,reason:$.state?.error||`state: ${$.state?.status??"unknown"}`}):{ok:!1,reason:"no_id_returned"}}let q=o?{ok:!0,cmd:u,script:"override",cwd:r}:g();if(q.ok&&q.cmd)if(D){i(m.dim(`Starting dev server via agent: ${q.cmd}`));let S=await K(q.cmd,q.cwd??r);S.ok||(t(`\u26A0 Agent-routed dev_server failed: ${S.reason}. Falling back to direct spawn.`),T(q.cmd,q.cwd??r))}else i(m.yellow("\u26A0 Agent server unavailable \u2014 using direct dev spawn (no cross-process dedup)")),T(q.cmd,q.cwd??r);else i(m.dim("No dev script at startup \u2014 the agent will start one via dev_server when it scaffolds."));let X=async()=>{if(c&&c.shutdown(),console.log(),console.log(m.yellow("Shutting down...")),j){try{j.abort()}catch{}j=null}if(te(),A){try{await fetch(`http://127.0.0.1:8766/dev-server/${encodeURIComponent(A)}/stop`,{method:"POST",signal:AbortSignal.timeout(3e3)})}catch{}A=null}z&&!z.killed&&z.kill("SIGTERM"),D&&await D.stop(),L.close(),process.exit(0)};process.on("SIGINT",X),process.on("SIGTERM",X)});var ne=new Map,je=0,Ht=500,fe=new Map,me=[],we=new Set;function Jt(o,e){for(let s of e){if(!s||typeof s.traceId!="string")continue;let r=fe.get(s.traceId);if(!r)for(r={traceId:s.traceId,service:o,spans:[],firstSeen:Date.now(),lastSeen:Date.now(),complete:!1},fe.set(s.traceId,r),me.push(s.traceId);me.length>Ht;){let u=me.shift();u&&fe.delete(u)}r.spans.push(s),r.lastSeen=Date.now(),s.kind==="server"&&!s.parentSpanId&&(r.root=s,r.complete=!0,Gt(r))}}function He(o){let e=o.root,s=e&&e.attributes||{};return{traceId:o.traceId,service:o.service,name:e?e.name:void 0,method:s["http.method"],route:s["http.route"]||s["http.target"],statusCode:s["http.status_code"],startTime:e?e.startTime:o.firstSeen,duration:e?e.duration:void 0,spanCount:o.spans.length,hasError:o.spans.some(r=>!!r.error),complete:o.complete}}function Je(o){let e=o.spans.slice().sort((s,r)=>s.startTime-r.startTime);return Object.assign({},He(o),{spans:e})}function Gt(o){if(we.size===0)return;let e=`data: ${JSON.stringify({type:"trace",trace:Je(o)})}
|
|
289
53
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
"
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
// Exclude directories that are never source code
|
|
304
|
-
"--glob",
|
|
305
|
-
"!node_modules/**",
|
|
306
|
-
"--glob",
|
|
307
|
-
"!.git/**",
|
|
308
|
-
"--glob",
|
|
309
|
-
"!dist/**",
|
|
310
|
-
"--glob",
|
|
311
|
-
"!build/**",
|
|
312
|
-
"--glob",
|
|
313
|
-
"!.next/**",
|
|
314
|
-
"--glob",
|
|
315
|
-
"!coverage/**",
|
|
316
|
-
"--glob",
|
|
317
|
-
"!.cache/**",
|
|
318
|
-
"--glob",
|
|
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, {
|
|
326
|
-
cwd: projectPath,
|
|
327
|
-
maxBuffer: 10 * 1024 * 1024
|
|
328
|
-
// 10 MB — generous for large codebases
|
|
329
|
-
});
|
|
330
|
-
const matches = [];
|
|
331
|
-
for (const line of stdout.split("\n")) {
|
|
332
|
-
const trimmed = line.trim();
|
|
333
|
-
if (!trimmed || !trimmed.startsWith("{"))
|
|
334
|
-
continue;
|
|
335
|
-
try {
|
|
336
|
-
const obj = JSON.parse(trimmed);
|
|
337
|
-
if (obj.type !== "match")
|
|
338
|
-
continue;
|
|
339
|
-
matches.push({
|
|
340
|
-
file: path3.relative(projectPath, obj.data.path.text),
|
|
341
|
-
line: obj.data.line_number,
|
|
342
|
-
content: obj.data.lines.text.trimEnd().substring(0, 200)
|
|
343
|
-
});
|
|
344
|
-
if (matches.length >= opts.maxResults)
|
|
345
|
-
break;
|
|
346
|
-
} catch {
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return matches;
|
|
350
|
-
}
|
|
351
|
-
var FS_SEARCH_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
352
|
-
".js",
|
|
353
|
-
".ts",
|
|
354
|
-
".jsx",
|
|
355
|
-
".tsx",
|
|
356
|
-
".vue",
|
|
357
|
-
".svelte",
|
|
358
|
-
".css",
|
|
359
|
-
".scss",
|
|
360
|
-
".sass",
|
|
361
|
-
".less",
|
|
362
|
-
".html",
|
|
363
|
-
".htm",
|
|
364
|
-
".json",
|
|
365
|
-
".md"
|
|
366
|
-
]);
|
|
367
|
-
var FS_IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
368
|
-
"node_modules",
|
|
369
|
-
".git",
|
|
370
|
-
"dist",
|
|
371
|
-
"build",
|
|
372
|
-
".next",
|
|
373
|
-
".nuxt",
|
|
374
|
-
".svelte-kit",
|
|
375
|
-
".cache",
|
|
376
|
-
"coverage",
|
|
377
|
-
".turbo",
|
|
378
|
-
".vercel",
|
|
379
|
-
".output"
|
|
380
|
-
]);
|
|
381
|
-
function buildMatcher(query, opts) {
|
|
382
|
-
if (opts.isRegex) {
|
|
383
|
-
try {
|
|
384
|
-
const re = new RegExp(query, opts.caseSensitive ? "" : "i");
|
|
385
|
-
return (line) => re.test(line);
|
|
386
|
-
} catch {
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
if (opts.caseSensitive) {
|
|
390
|
-
return (line) => line.includes(query);
|
|
391
|
-
}
|
|
392
|
-
const lowerQuery = query.toLowerCase();
|
|
393
|
-
return (line) => line.toLowerCase().includes(lowerQuery);
|
|
394
|
-
}
|
|
395
|
-
function searchWithFs(projectPath, query, opts) {
|
|
396
|
-
const results = [];
|
|
397
|
-
const matcher = buildMatcher(query, opts);
|
|
398
|
-
function walkDir(dir) {
|
|
399
|
-
if (results.length >= opts.maxResults)
|
|
400
|
-
return;
|
|
401
|
-
let entries;
|
|
402
|
-
try {
|
|
403
|
-
entries = fs3.readdirSync(dir);
|
|
404
|
-
} catch {
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
for (const entry of entries) {
|
|
408
|
-
if (results.length >= opts.maxResults)
|
|
409
|
-
return;
|
|
410
|
-
if (FS_IGNORE_DIRS.has(entry) || entry.startsWith("."))
|
|
411
|
-
continue;
|
|
412
|
-
const fullPath = path3.join(dir, entry);
|
|
413
|
-
let stat;
|
|
414
|
-
try {
|
|
415
|
-
stat = fs3.statSync(fullPath);
|
|
416
|
-
} catch {
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
if (stat.isDirectory()) {
|
|
420
|
-
walkDir(fullPath);
|
|
421
|
-
} else if (FS_SEARCH_EXTENSIONS.has(path3.extname(entry).toLowerCase())) {
|
|
422
|
-
let content;
|
|
423
|
-
try {
|
|
424
|
-
content = fs3.readFileSync(fullPath, "utf-8");
|
|
425
|
-
} catch {
|
|
426
|
-
continue;
|
|
427
|
-
}
|
|
428
|
-
const lines = content.split("\n");
|
|
429
|
-
for (let i = 0; i < lines.length && results.length < opts.maxResults; i++) {
|
|
430
|
-
if (matcher(lines[i])) {
|
|
431
|
-
results.push({
|
|
432
|
-
file: path3.relative(projectPath, fullPath),
|
|
433
|
-
line: i + 1,
|
|
434
|
-
content: lines[i].trim().substring(0, 200)
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
walkDir(projectPath);
|
|
442
|
-
return results;
|
|
443
|
-
}
|
|
444
|
-
async function searchCode(projectPath, query, opts = {}) {
|
|
445
|
-
const resolved = {
|
|
446
|
-
isRegex: opts.isRegex ?? false,
|
|
447
|
-
caseSensitive: opts.caseSensitive ?? true,
|
|
448
|
-
maxResults: opts.maxResults ?? 20
|
|
449
|
-
};
|
|
450
|
-
try {
|
|
451
|
-
const matches = await searchWithRipgrep(projectPath, query, resolved);
|
|
452
|
-
return { matches, engine: "ripgrep" };
|
|
453
|
-
} catch {
|
|
454
|
-
const matches = searchWithFs(projectPath, query, resolved);
|
|
455
|
-
return { matches, engine: "fs" };
|
|
456
|
-
}
|
|
457
|
-
}
|
|
54
|
+
`;for(let s of we)try{s.writableEnded||s.write(e)}catch{}}function Ge(o,e){let s=[],r={projectPath:e};try{let u=b.join(e,"package.json");if(h.existsSync(u)){let a=JSON.parse(h.readFileSync(u,"utf-8"));r={...r,name:a.name,version:a.version,description:a.description}}}catch{}o.on("message",async u=>{try{let n=JSON.parse(u.toString()),{id:y,type:c}=n,i={id:y};switch(c){case"GET_PROJECT_INFO":i.data=r;break;case"READ_FILE":try{let t=b.resolve(e,n.filePath);if(!Q(e,t))i.error="Access denied";else if(h.existsSync(t)){let f=h.statSync(t);if(f.size>0){let A=h.openSync(t,"r"),F=Buffer.alloc(Math.min(4096,f.size));if(h.readSync(A,F,0,F.length,0),h.closeSync(A),F.includes(0)){i.error=`Cannot read binary file: ${n.filePath}`;break}}let d=h.readFileSync(t,"utf-8").split(`
|
|
55
|
+
`),g=n.offset||1,p=n.limit||2e3,T=Math.max(0,g-1),L=Math.min(d.length,T+p),R=d.slice(T,L).map((A,F)=>`${T+F+1}: ${A}`).join(`
|
|
56
|
+
`),k=L<d.length,I=h.statSync(t),D=`${Math.round(I.mtimeMs)}-${I.size}`;i.data={content:R,exists:!0,path:n.filePath,totalLines:d.length,showing:{from:g,to:L,truncated:k},readToken:D}}else{let f=b.dirname(t),l=b.basename(t),d=[];try{d=h.readdirSync(f).filter(g=>g.toLowerCase().includes(l.toLowerCase())||l.toLowerCase().includes(g.toLowerCase())).slice(0,3).map(g=>b.relative(e,b.join(f,g)))}catch{}i.data={exists:!1,suggestions:d}}}catch(t){i.error=t.message}break;case"GET_SOURCE":try{let t=b.resolve(e,n.filePath);if(!Q(e,t))i.error="Access denied";else if(h.existsSync(t)){let l=h.readFileSync(t,"utf-8").split(`
|
|
57
|
+
`),d=Math.max(0,(n.lineStart||1)-1),g=n.lineEnd?Math.min(l.length,n.lineEnd):l.length;i.data={lines:l.slice(d,g),startLine:d+1,endLine:g,totalLines:l.length}}else i.error="File not found"}catch(t){i.error=t.message}break;case"GET_ERROR_CONTEXT":try{let t=b.resolve(e,n.filePath);if(!Q(e,t))i.error="Access denied";else if(h.existsSync(t)){let l=h.readFileSync(t,"utf-8").split(`
|
|
58
|
+
`),d=n.line||1,g=n.contextLines||5,p=Math.max(0,d-g-1),T=Math.min(l.length,d+g);i.data={lines:l.slice(p,T).map((L,w)=>({number:p+w+1,content:L,isError:p+w+1===d})),errorLine:d,filePath:n.filePath}}else i.error="File not found"}catch(t){i.error=t.message}break;case"GET_FILE_TREE":try{let t=n.depth||3,f=Ve(e,t);i.data={tree:f}}catch(t){i.error=t.message}break;case"SEARCH_CODE":try{let{matches:t,engine:f}=await Ce(e,n.query,{isRegex:n.isRegex||!1,caseSensitive:n.caseSensitive!==!1,maxResults:n.maxResults||20});i.data={matches:t,engine:f}}catch(t){i.error=t.message}break;case"FIND_FILES":try{let p=function(T){if(!(g.length>=l))try{let L=h.readdirSync(T);for(let w of L){if(g.length>=l)break;if(d.includes(w)||w.startsWith("."))continue;let R=b.join(T,w);try{h.statSync(R).isDirectory()?p(R):(!f||w===f||t.includes("*")&&w.endsWith(f))&&g.push(b.relative(e,R))}catch{}}}catch{}};var a=p;let t=n.pattern||"",f=t.split("/").pop().replace(/\*\*/g,"").replace(/\*/g,""),l=n.maxResults||20,d=["node_modules",".git","dist","build",".next",".cache"],g=[];p(e),i.data=g}catch(t){i.error=t.message}case"DETECT_PROJECT":try{let t=b.join(e,"package.json"),f={},l={},d="";if(h.existsSync(t)){let C=JSON.parse(h.readFileSync(t,"utf-8"));f=C.dependencies||{},l=C.devDependencies||{},d=C.name||""}let g={...f,...l},p="html";g.next?p="next.js":g.nuxt||g.nuxt3?p="nuxt":g.gatsby?p="gatsby":g["@remix-run/react"]?p="remix":g.svelte||g["@sveltejs/kit"]?p="svelte":g.vue?p="vue":g["@angular/core"]?p="angular":g.react&&(p="react");let T=!!g.typescript||h.existsSync(b.join(e,"tsconfig.json")),L=T?".tsx":".jsx",w="plain-css";g.tailwindcss?w="tailwind":g["styled-components"]?w="styled-components":g["@emotion/react"]||g["@emotion/styled"]?w="emotion":(g.sass||g["node-sass"])&&(w="sass");let R=null;p==="next.js"&&(h.existsSync(b.join(e,"src/app"))||h.existsSync(b.join(e,"app"))?R="app":(h.existsSync(b.join(e,"src/pages"))||h.existsSync(b.join(e,"pages")))&&(R="pages"));let k=C=>{for(let _ of C)if(h.existsSync(b.join(e,_)))return _;return null},I=k(["src/app/layout.tsx","src/app/layout.jsx","src/app/layout.js","app/layout.tsx","app/layout.jsx","app/layout.js","src/pages/_app.tsx","src/pages/_app.jsx","pages/_app.tsx","pages/_app.jsx","src/App.tsx","src/App.jsx","src/App.js","src/main.tsx","src/main.jsx","src/main.js","index.html"]),D=k(["src/app/globals.css","src/app/global.css","app/globals.css","app/global.css","src/index.css","src/styles/globals.css","src/styles/global.css","src/App.css","src/styles.css","styles/globals.css","styles/global.css","css/style.css","css/main.css","style.css","styles.css"]),A=k(["src/components","src/app/components","components","src/ui","src/app/ui"]),F=null,B=null;w==="tailwind"&&(F=k(["tailwind.config.ts","tailwind.config.js","tailwind.config.mjs","tailwind.config.cjs"]),B=g.tailwindcss||null);let j="arrow-function",J="default",te=null,se=null;if(A){let C=b.join(e,A);try{let _=h.readdirSync(C).filter(E=>E.endsWith(".tsx")||E.endsWith(".jsx")||E.endsWith(".js")||E.endsWith(".vue")||E.endsWith(".svelte"));if(_.length>0){te=b.join(A,_[0]);let E=h.readFileSync(b.join(C,_[0]),"utf-8");se=E.split(`
|
|
59
|
+
`).slice(0,80).join(`
|
|
60
|
+
`),/^export default function /m.test(E)?(j="function-declaration",J="default"):/^export function /m.test(E)?(j="function-declaration",J="named"):(/const \w+ = \(/.test(E)||/const \w+ = \(\) =>/.test(E))&&(j="arrow-function"),/^export default /m.test(E)?J="default":(/^export \{/m.test(E)||/^export const/m.test(E))&&(J="named"),/import\s+\w+\s+from\s+['"][^'"]+\.module\.(css|scss|sass)['"]/.test(E)&&(w="css-modules")}}catch{}}if(w==="plain-css"&&I)try{let C=h.readFileSync(b.join(e,I),"utf-8");/import\s+\w+\s+from\s+['"][^'"]+\.module\.(css|scss|sass)['"]/.test(C)&&(w="css-modules")}catch{}let ie=null;if(D)try{ie=h.readFileSync(b.join(e,D),"utf-8").split(`
|
|
61
|
+
`).slice(0,40).join(`
|
|
62
|
+
`)}catch{}let K=new Set(["node_modules",".git",".next",".nuxt",".svelte-kit","dist","build",".cache",".turbo","coverage","__pycache__",".vercel",".output",".parcel-cache"]),q=new Set([".tsx",".jsx",".js",".ts",".vue",".svelte",".css",".scss",".sass",".less",".module.css",".module.scss",".html",".astro"]),X=[],S=120,v=new Map,x=(C,_,E)=>{if(!(E>4||X.length>=S))try{let W=h.readdirSync(b.join(e,C),{withFileTypes:!0}).filter(P=>!P.name.startsWith(".")||P.name===".env").sort((P,N)=>P.isDirectory()&&!N.isDirectory()?-1:!P.isDirectory()&&N.isDirectory()?1:P.name.localeCompare(N.name));for(let P=0;P<W.length&&X.length<S;P++){let N=W[P],O=P===W.length-1,V=O?"\u2514\u2500\u2500 ":"\u251C\u2500\u2500 ",re=O?" ":"\u2502 ",oe=C?`${C}/${N.name}`:N.name;if(N.isDirectory()){if(K.has(N.name))continue;X.push(`${_}${V}${N.name}/`),x(oe,_+re,E+1)}else{let Se=b.extname(N.name).toLowerCase();new Set([".tsx",".jsx",".vue",".svelte"]).has(Se)&&/^[A-Z]/.test(N.name)&&v.set(C,(v.get(C)??0)+1),(q.has(Se)||E<=1&&(N.name==="package.json"||N.name==="tsconfig.json"||N.name.startsWith("tailwind.config")||N.name.startsWith("next.config")||N.name.startsWith("vite.config")))&&X.push(`${_}${V}${N.name}`)}}}catch{}},$=["src","app","pages","components","styles","public","lib","utils"],M=[];for(let C of $)h.existsSync(b.join(e,C))&&M.push(C);try{let C=h.readdirSync(e,{withFileTypes:!0});for(let _ of C)!_.isDirectory()&&(_.name==="package.json"||_.name==="tsconfig.json"||_.name.startsWith("tailwind.config")||_.name.startsWith("next.config")||_.name.startsWith("vite.config")||_.name==="index.html")&&X.push(_.name)}catch{}for(let C of M)X.push(`${C}/`),x(C,"",1);let Z=X.length>0?X.join(`
|
|
63
|
+
`):null;if(v.size>0){let C="",_=0;for(let[E,G]of v.entries())G>_&&(_=G,C=E);_>0&&(A=C)}i.data={framework:p,typescript:T,styling:w,router:R,ext:L,pkgName:d,layoutFile:I,globalStyleFile:D,componentsDir:A,tailwindConfig:F,tailwindVersion:B,componentPattern:j,exportPattern:J,sampleComponentFile:te,sampleComponentCode:se,globalStylePreview:ie,projectTree:Z},console.log(m.blue("\u2139")+` Project detected: ${m.yellow(p)} + ${m.cyan(w)}${T?m.dim(" (TS)"):""}${R?m.dim(` [${R} router]`):""}`)}catch(t){i.error=t.message}break;case"BUILD_CLASS_INDEX":try{let t=new Set([".tsx",".jsx",".vue",".svelte",".js",".ts"]),f=new Set(["node_modules",".git",".next",".nuxt","dist","build",".cache"]),l={},d=0,g=0,p=T=>{try{let L=h.readdirSync(T,{withFileTypes:!0});for(let w of L){if(w.name.startsWith("."))continue;let R=b.join(T,w.name),k=b.relative(e,R).replace(/\\/g,"/");if(w.isDirectory()){if(f.has(w.name))continue;p(R)}else{let I=b.extname(w.name).toLowerCase();if(!t.has(I))continue;try{let A=h.readFileSync(R,"utf-8").split(`
|
|
64
|
+
`);d++;for(let F=0;F<A.length;F++){let j=A[F].matchAll(/(?:className|class)\s*=\s*(?:"([^"]+)"|'([^']+)'|\{(?:`([^`]+)`|"([^"]+)"|'([^']+)')\})/g);for(let J of j){let te=J[1]||J[2]||J[3]||J[4]||J[5];if(!te)continue;let se=te.split(/\s+/).filter(K=>K.length>2&&!K.startsWith("$")&&!K.includes("{")&&!K.includes("}"));if(se.length===0)continue;let ie=se.slice(0,3).sort().join(" ");ie&&!l[ie]&&(l[ie]={file:k,line:F+1},g++);for(let K of se)/^(flex|grid|block|hidden|relative|absolute|fixed|sticky|inline|w-|h-|p-|m-|text-|bg-|border|rounded|flex-|grid-|col-|row-)/.test(K)||K.length>5&&!l[K]&&(l[K]={file:k,line:F+1},g++)}}}catch{}}}}catch{}};p(e),i.data={index:l,filesScanned:d,classesIndexed:g},console.log(m.blue("\u2139")+` Class index built: ${m.yellow(g)} classes across ${m.cyan(d)} files`)}catch(t){i.error=t.message}break;case"WRITE_FILE":try{let t=b.resolve(e,n.filePath);if(!Q(e,t))i.error="Access denied: Path outside project";else{let f=h.existsSync(t)?h.readFileSync(t,"utf-8"):null;if(s.push({filePath:t,prevContent:f,operation:"WRITE_FILE",timestamp:Date.now(),relativePath:n.filePath}),s.length>20&&s.shift(),t.endsWith(".css")){let g=n.content||"",p=(g.match(/\{/g)||[]).length,T=(g.match(/\}/g)||[]).length;if(p!==T){s.pop(),i.error=`CSS validation failed: unbalanced braces (${p} opening vs ${T} closing). Fix the CSS before writing.`;break}}let l=b.dirname(t);h.existsSync(l)||h.mkdirSync(l,{recursive:!0}),await ge(t,async()=>{h.writeFileSync(t,n.content,"utf-8");let g=await de(t,e);i.data={success:!0,path:t,formatted:g,undoAvailable:!0},console.log(m.blue("\u2139")+` Wrote file: ${n.filePath}`+(g?m.dim(` (formatted with ${g})`):"")+m.dim(" [undo saved]"))});let d=await he(t,e);d.ran&&i.data&&(i.data.lspDiagnostics=d.diagnostics,d.summary&&(i.data.lspSummary=d.summary,d.diagnostics.some(g=>g.severity==="error")&&console.log(m.yellow("\u26A0")+` ${d.summary}`)))}}catch(t){i.error=t.message}break;case"APPEND_FILE":try{let t=b.resolve(e,n.filePath);if(!Q(e,t))i.error="Access denied: Path outside project";else{let f=t.endsWith(".css"),l=n.content||"";if(f){let p=(l.match(/\{/g)||[]).length,T=(l.match(/\}/g)||[]).length;if(p!==T){i.error=`CSS validation failed: unbalanced braces (${p} opening vs ${T} closing). Fix the CSS before appending.`;break}}let d=h.existsSync(t)?h.readFileSync(t,"utf-8"):null;if(s.push({filePath:t,prevContent:d,operation:"APPEND_FILE",timestamp:Date.now(),relativePath:n.filePath}),s.length>20&&s.shift(),f&&d!==null){let p=/^@(?:import|charset)\s+[^;]+;\s*$/gm,T=[],L=l.replace(p,w=>(T.push(w.trim()),"")).trim();if(T.length>0){let w=d.split(`
|
|
65
|
+
`),R=-1;for(let B=0;B<w.length;B++){let j=w[B].trim();if((j.startsWith("@import")||j.startsWith("@charset"))&&(R=B),j&&!j.startsWith("@import")&&!j.startsWith("@charset")&&!j.startsWith("/*")&&!j.startsWith("*")&&!j.startsWith("//"))break}let k=T.filter(B=>!d.includes(B)),I=R+1,D=w.slice(0,I),A=w.slice(I),F=[...D,...k,...A,"",L].join(`
|
|
66
|
+
`);h.writeFileSync(t,F,"utf-8"),console.log(m.blue("\u2139")+` CSS-safe append: hoisted ${k.length} @import(s) to top of ${n.filePath}`)}else h.appendFileSync(t,`
|
|
458
67
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
plugins: ["jsx", "typescript"],
|
|
474
|
-
errorRecovery: true
|
|
475
|
-
// tolerate minor syntax errors in source file
|
|
476
|
-
});
|
|
477
|
-
} catch (err) {
|
|
478
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
479
|
-
return { success: false, error: `Parse error: ${msg}` };
|
|
480
|
-
}
|
|
481
|
-
const candidates = [];
|
|
482
|
-
traverse(ast, {
|
|
483
|
-
JSXOpeningElement(nodePath) {
|
|
484
|
-
for (const attr of nodePath.node.attributes) {
|
|
485
|
-
if (!t.isJSXAttribute(attr))
|
|
486
|
-
continue;
|
|
487
|
-
if (!t.isJSXIdentifier(attr.name, { name: "className" }))
|
|
488
|
-
continue;
|
|
489
|
-
const line = attr.loc?.start.line ?? 0;
|
|
490
|
-
let score = 0;
|
|
491
|
-
if (edit.lineHint && line > 0) {
|
|
492
|
-
const dist = Math.abs(line - edit.lineHint);
|
|
493
|
-
if (dist <= 1)
|
|
494
|
-
score += 100;
|
|
495
|
-
else if (dist <= 5)
|
|
496
|
-
score += 50;
|
|
497
|
-
else if (dist <= 15)
|
|
498
|
-
score += 15;
|
|
499
|
-
}
|
|
500
|
-
const currentValue = _extractClassNameString(attr.value);
|
|
501
|
-
if (edit.oldValue) {
|
|
502
|
-
if (currentValue === edit.oldValue)
|
|
503
|
-
score += 80;
|
|
504
|
-
else if (currentValue?.includes(edit.oldValue))
|
|
505
|
-
score += 40;
|
|
506
|
-
else if (currentValue === null && edit.lineHint)
|
|
507
|
-
score += 20;
|
|
508
|
-
} else {
|
|
509
|
-
score += 20;
|
|
510
|
-
}
|
|
511
|
-
if (score > 0) {
|
|
512
|
-
candidates.push({ nodePath, attrNode: attr, score, line });
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
if (candidates.length === 0) {
|
|
518
|
-
return { success: false, error: "No className attribute found matching the given hints." };
|
|
519
|
-
}
|
|
520
|
-
candidates.sort((a, b) => b.score - a.score);
|
|
521
|
-
const best = candidates[0];
|
|
522
|
-
let strategy;
|
|
523
|
-
if (best.score >= 180)
|
|
524
|
-
strategy = "line_exact";
|
|
525
|
-
else if (best.score >= 100)
|
|
526
|
-
strategy = "line_proximity";
|
|
527
|
-
else if (best.score >= 80)
|
|
528
|
-
strategy = "value_exact";
|
|
529
|
-
else
|
|
530
|
-
strategy = "value_partial";
|
|
531
|
-
best.attrNode.value = t.stringLiteral(edit.newValue);
|
|
532
|
-
const output = generate(
|
|
533
|
-
ast,
|
|
534
|
-
{ retainLines: true, jsescOption: { minimal: true } },
|
|
535
|
-
source
|
|
536
|
-
);
|
|
537
|
-
return {
|
|
538
|
-
success: true,
|
|
539
|
-
code: output.code,
|
|
540
|
-
strategy,
|
|
541
|
-
matchedLine: best.line
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
function _extractClassNameString(value) {
|
|
545
|
-
if (!value)
|
|
546
|
-
return "";
|
|
547
|
-
if (t.isStringLiteral(value))
|
|
548
|
-
return value.value;
|
|
549
|
-
if (t.isJSXExpressionContainer(value)) {
|
|
550
|
-
const expr = value.expression;
|
|
551
|
-
if (t.isStringLiteral(expr))
|
|
552
|
-
return expr.value;
|
|
553
|
-
if (t.isTemplateLiteral(expr) && expr.expressions.length === 0) {
|
|
554
|
-
return expr.quasis[0]?.value.cooked ?? null;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
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
|
-
|
|
1000
|
-
// src/index.ts
|
|
1001
|
-
import { createRequire as _createRequire } from "module";
|
|
1002
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
1003
|
-
var __dirname = path4.dirname(__filename);
|
|
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
|
-
}
|
|
1009
|
-
var TerminalBuffer = class {
|
|
1010
|
-
lines = [];
|
|
1011
|
-
MAX_LINES = 500;
|
|
1012
|
-
push(chunk) {
|
|
1013
|
-
const clean = chunk.replace(/\x1B\[[0-9;]*[mGKHFABCDEJsu]/g, "");
|
|
1014
|
-
const newLines = clean.split("\n");
|
|
1015
|
-
for (const line of newLines) {
|
|
1016
|
-
if (line.trim() === "")
|
|
1017
|
-
continue;
|
|
1018
|
-
this.lines.push(line);
|
|
1019
|
-
if (this.lines.length > this.MAX_LINES) {
|
|
1020
|
-
this.lines.shift();
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
getLast(n = 100) {
|
|
1025
|
-
return this.lines.slice(-n);
|
|
1026
|
-
}
|
|
1027
|
-
clear() {
|
|
1028
|
-
this.lines = [];
|
|
1029
|
-
}
|
|
1030
|
-
get length() {
|
|
1031
|
-
return this.lines.length;
|
|
1032
|
-
}
|
|
1033
|
-
};
|
|
1034
|
-
var globalTerminalBuffer = new TerminalBuffer();
|
|
1035
|
-
var devProcess = null;
|
|
1036
|
-
function detectDevCommand(projectPath, override) {
|
|
1037
|
-
if (override) {
|
|
1038
|
-
return { command: override, pm: "custom", script: override };
|
|
1039
|
-
}
|
|
1040
|
-
let pm = "npm";
|
|
1041
|
-
if (fs4.existsSync(path4.join(projectPath, "bun.lockb")))
|
|
1042
|
-
pm = "bun";
|
|
1043
|
-
else if (fs4.existsSync(path4.join(projectPath, "pnpm-lock.yaml")))
|
|
1044
|
-
pm = "pnpm";
|
|
1045
|
-
else if (fs4.existsSync(path4.join(projectPath, "yarn.lock")))
|
|
1046
|
-
pm = "yarn";
|
|
1047
|
-
const pkgPath = path4.join(projectPath, "package.json");
|
|
1048
|
-
let scripts = {};
|
|
1049
|
-
if (fs4.existsSync(pkgPath)) {
|
|
1050
|
-
try {
|
|
1051
|
-
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
1052
|
-
scripts = pkg.scripts || {};
|
|
1053
|
-
} catch (e) {
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
const candidates = ["dev", "start", "serve", "preview"];
|
|
1057
|
-
const script = candidates.find((c) => !!scripts[c]) || "dev";
|
|
1058
|
-
const runCmd = {
|
|
1059
|
-
npm: `npm run ${script}`,
|
|
1060
|
-
pnpm: `pnpm run ${script}`,
|
|
1061
|
-
yarn: `yarn ${script}`,
|
|
1062
|
-
bun: `bun run ${script}`
|
|
1063
|
-
};
|
|
1064
|
-
return { command: runCmd[pm] ?? `npm run ${script}`, pm, script };
|
|
1065
|
-
}
|
|
1066
|
-
var _pkgRequire = _createRequire(import.meta.url);
|
|
1067
|
-
var VERSION = _pkgRequire("../package.json").version;
|
|
1068
|
-
function levenshtein(a, b) {
|
|
1069
|
-
if (a === "" || b === "")
|
|
1070
|
-
return Math.max(a.length, b.length);
|
|
1071
|
-
const matrix = Array.from(
|
|
1072
|
-
{ length: a.length + 1 },
|
|
1073
|
-
(_, i) => Array.from({ length: b.length + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0)
|
|
1074
|
-
);
|
|
1075
|
-
for (let i = 1; i <= a.length; i++) {
|
|
1076
|
-
for (let j = 1; j <= b.length; j++) {
|
|
1077
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1078
|
-
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
return matrix[a.length][b.length];
|
|
1082
|
-
}
|
|
1083
|
-
var SimpleReplacer = function* (_content, find) {
|
|
1084
|
-
yield find;
|
|
1085
|
-
};
|
|
1086
|
-
var LineTrimmedReplacer = function* (content, find) {
|
|
1087
|
-
const originalLines = content.split("\n");
|
|
1088
|
-
const searchLines = find.split("\n");
|
|
1089
|
-
if (searchLines[searchLines.length - 1] === "")
|
|
1090
|
-
searchLines.pop();
|
|
1091
|
-
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
|
1092
|
-
let matches = true;
|
|
1093
|
-
for (let j = 0; j < searchLines.length; j++) {
|
|
1094
|
-
if (originalLines[i + j].trim() !== searchLines[j].trim()) {
|
|
1095
|
-
matches = false;
|
|
1096
|
-
break;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
if (matches) {
|
|
1100
|
-
let start = 0;
|
|
1101
|
-
for (let k = 0; k < i; k++)
|
|
1102
|
-
start += originalLines[k].length + 1;
|
|
1103
|
-
let end = start;
|
|
1104
|
-
for (let k = 0; k < searchLines.length; k++) {
|
|
1105
|
-
end += originalLines[i + k].length;
|
|
1106
|
-
if (k < searchLines.length - 1)
|
|
1107
|
-
end += 1;
|
|
1108
|
-
}
|
|
1109
|
-
yield content.substring(start, end);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
};
|
|
1113
|
-
var BlockAnchorReplacer = function* (content, find) {
|
|
1114
|
-
const originalLines = content.split("\n");
|
|
1115
|
-
const searchLines = find.split("\n");
|
|
1116
|
-
if (searchLines.length < 3)
|
|
1117
|
-
return;
|
|
1118
|
-
if (searchLines[searchLines.length - 1] === "")
|
|
1119
|
-
searchLines.pop();
|
|
1120
|
-
const firstSearch = searchLines[0].trim();
|
|
1121
|
-
const lastSearch = searchLines[searchLines.length - 1].trim();
|
|
1122
|
-
const searchBlockSize = searchLines.length;
|
|
1123
|
-
const candidates = [];
|
|
1124
|
-
for (let i = 0; i < originalLines.length; i++) {
|
|
1125
|
-
if (originalLines[i].trim() !== firstSearch)
|
|
1126
|
-
continue;
|
|
1127
|
-
for (let j = i + 2; j < originalLines.length; j++) {
|
|
1128
|
-
if (originalLines[j].trim() === lastSearch) {
|
|
1129
|
-
candidates.push({ startLine: i, endLine: j });
|
|
1130
|
-
break;
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
if (candidates.length === 0)
|
|
1135
|
-
return;
|
|
1136
|
-
let bestMatch = null;
|
|
1137
|
-
let maxSimilarity = -1;
|
|
1138
|
-
const threshold = candidates.length === 1 ? 0 : 0.3;
|
|
1139
|
-
for (const cand of candidates) {
|
|
1140
|
-
const actualSize = cand.endLine - cand.startLine + 1;
|
|
1141
|
-
let similarity = 0;
|
|
1142
|
-
const linesToCheck = Math.min(searchBlockSize - 2, actualSize - 2);
|
|
1143
|
-
if (linesToCheck > 0) {
|
|
1144
|
-
for (let j = 1; j < searchBlockSize - 1 && j < actualSize - 1; j++) {
|
|
1145
|
-
const orig = originalLines[cand.startLine + j].trim();
|
|
1146
|
-
const srch = searchLines[j].trim();
|
|
1147
|
-
const maxLen = Math.max(orig.length, srch.length);
|
|
1148
|
-
if (maxLen === 0)
|
|
1149
|
-
continue;
|
|
1150
|
-
similarity += (1 - levenshtein(orig, srch) / maxLen) / linesToCheck;
|
|
1151
|
-
}
|
|
1152
|
-
} else {
|
|
1153
|
-
similarity = 1;
|
|
1154
|
-
}
|
|
1155
|
-
if (similarity > maxSimilarity) {
|
|
1156
|
-
maxSimilarity = similarity;
|
|
1157
|
-
bestMatch = cand;
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
if (maxSimilarity >= threshold && bestMatch) {
|
|
1161
|
-
let start = 0;
|
|
1162
|
-
for (let k = 0; k < bestMatch.startLine; k++)
|
|
1163
|
-
start += originalLines[k].length + 1;
|
|
1164
|
-
let end = start;
|
|
1165
|
-
for (let k = bestMatch.startLine; k <= bestMatch.endLine; k++) {
|
|
1166
|
-
end += originalLines[k].length;
|
|
1167
|
-
if (k < bestMatch.endLine)
|
|
1168
|
-
end += 1;
|
|
1169
|
-
}
|
|
1170
|
-
yield content.substring(start, end);
|
|
1171
|
-
}
|
|
1172
|
-
};
|
|
1173
|
-
var WhitespaceNormalizedReplacer = function* (content, find) {
|
|
1174
|
-
const normalize = (text) => text.replace(/\s+/g, " ").trim();
|
|
1175
|
-
const normalizedFind = normalize(find);
|
|
1176
|
-
const lines = content.split("\n");
|
|
1177
|
-
for (const line of lines) {
|
|
1178
|
-
if (normalize(line) === normalizedFind) {
|
|
1179
|
-
yield line;
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
const findLines = find.split("\n");
|
|
1183
|
-
if (findLines.length > 1) {
|
|
1184
|
-
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
1185
|
-
const block = lines.slice(i, i + findLines.length);
|
|
1186
|
-
if (normalize(block.join("\n")) === normalizedFind) {
|
|
1187
|
-
yield block.join("\n");
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
};
|
|
1192
|
-
var IndentationFlexibleReplacer = function* (content, find) {
|
|
1193
|
-
const removeIndent = (text) => {
|
|
1194
|
-
const lines = text.split("\n");
|
|
1195
|
-
const nonEmpty = lines.filter((l) => l.trim().length > 0);
|
|
1196
|
-
if (nonEmpty.length === 0)
|
|
1197
|
-
return text;
|
|
1198
|
-
const minIndent = Math.min(...nonEmpty.map((l) => {
|
|
1199
|
-
const m = l.match(/^(\s*)/);
|
|
1200
|
-
return m ? m[1].length : 0;
|
|
1201
|
-
}));
|
|
1202
|
-
return lines.map((l) => l.trim().length === 0 ? l : l.slice(minIndent)).join("\n");
|
|
1203
|
-
};
|
|
1204
|
-
const normalizedFind = removeIndent(find);
|
|
1205
|
-
const contentLines = content.split("\n");
|
|
1206
|
-
const findLines = find.split("\n");
|
|
1207
|
-
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
|
|
1208
|
-
const block = contentLines.slice(i, i + findLines.length).join("\n");
|
|
1209
|
-
if (removeIndent(block) === normalizedFind) {
|
|
1210
|
-
yield block;
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
};
|
|
1214
|
-
var EscapeNormalizedReplacer = function* (content, find) {
|
|
1215
|
-
const unescape = (str) => str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, c) => {
|
|
1216
|
-
switch (c) {
|
|
1217
|
-
case "n":
|
|
1218
|
-
return "\n";
|
|
1219
|
-
case "t":
|
|
1220
|
-
return " ";
|
|
1221
|
-
case "r":
|
|
1222
|
-
return "\r";
|
|
1223
|
-
case "'":
|
|
1224
|
-
return "'";
|
|
1225
|
-
case '"':
|
|
1226
|
-
return '"';
|
|
1227
|
-
case "`":
|
|
1228
|
-
return "`";
|
|
1229
|
-
case "\\":
|
|
1230
|
-
return "\\";
|
|
1231
|
-
case "\n":
|
|
1232
|
-
return "\n";
|
|
1233
|
-
case "$":
|
|
1234
|
-
return "$";
|
|
1235
|
-
default:
|
|
1236
|
-
return match;
|
|
1237
|
-
}
|
|
1238
|
-
});
|
|
1239
|
-
const unescapedFind = unescape(find);
|
|
1240
|
-
if (content.includes(unescapedFind))
|
|
1241
|
-
yield unescapedFind;
|
|
1242
|
-
const lines = content.split("\n");
|
|
1243
|
-
const findLines = unescapedFind.split("\n");
|
|
1244
|
-
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
1245
|
-
const block = lines.slice(i, i + findLines.length).join("\n");
|
|
1246
|
-
if (unescape(block) === unescapedFind)
|
|
1247
|
-
yield block;
|
|
1248
|
-
}
|
|
1249
|
-
};
|
|
1250
|
-
var TrimmedBoundaryReplacer = function* (content, find) {
|
|
1251
|
-
const trimmed = find.trim();
|
|
1252
|
-
if (trimmed === find)
|
|
1253
|
-
return;
|
|
1254
|
-
if (content.includes(trimmed))
|
|
1255
|
-
yield trimmed;
|
|
1256
|
-
const lines = content.split("\n");
|
|
1257
|
-
const findLines = find.split("\n");
|
|
1258
|
-
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
|
1259
|
-
const block = lines.slice(i, i + findLines.length).join("\n");
|
|
1260
|
-
if (block.trim() === trimmed)
|
|
1261
|
-
yield block;
|
|
1262
|
-
}
|
|
1263
|
-
};
|
|
1264
|
-
var ContextAwareReplacer = function* (content, find) {
|
|
1265
|
-
const findLines = find.split("\n");
|
|
1266
|
-
if (findLines.length < 3)
|
|
1267
|
-
return;
|
|
1268
|
-
if (findLines[findLines.length - 1] === "")
|
|
1269
|
-
findLines.pop();
|
|
1270
|
-
const contentLines = content.split("\n");
|
|
1271
|
-
const firstLine = findLines[0].trim();
|
|
1272
|
-
const lastLine = findLines[findLines.length - 1].trim();
|
|
1273
|
-
for (let i = 0; i < contentLines.length; i++) {
|
|
1274
|
-
if (contentLines[i].trim() !== firstLine)
|
|
1275
|
-
continue;
|
|
1276
|
-
for (let j = i + 2; j < contentLines.length; j++) {
|
|
1277
|
-
if (contentLines[j].trim() === lastLine) {
|
|
1278
|
-
const blockLines = contentLines.slice(i, j + 1);
|
|
1279
|
-
if (blockLines.length === findLines.length) {
|
|
1280
|
-
let matching = 0, total = 0;
|
|
1281
|
-
for (let k = 1; k < blockLines.length - 1; k++) {
|
|
1282
|
-
const bl = blockLines[k].trim(), fl = findLines[k].trim();
|
|
1283
|
-
if (bl.length > 0 || fl.length > 0) {
|
|
1284
|
-
total++;
|
|
1285
|
-
if (bl === fl)
|
|
1286
|
-
matching++;
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
if (total === 0 || matching / total >= 0.5) {
|
|
1290
|
-
yield blockLines.join("\n");
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
break;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
};
|
|
1299
|
-
var MultiOccurrenceReplacer = function* (content, find) {
|
|
1300
|
-
let start = 0;
|
|
1301
|
-
while (true) {
|
|
1302
|
-
const idx = content.indexOf(find, start);
|
|
1303
|
-
if (idx === -1)
|
|
1304
|
-
break;
|
|
1305
|
-
yield find;
|
|
1306
|
-
start = idx + find.length;
|
|
1307
|
-
}
|
|
1308
|
-
};
|
|
1309
|
-
var BlankLineTolerantReplacer = function* (content, find) {
|
|
1310
|
-
const collapseBlankLines = (text) => text.replace(/\n\s*\n/g, "\n").trim();
|
|
1311
|
-
const collapsedFind = collapseBlankLines(find);
|
|
1312
|
-
if (!collapsedFind)
|
|
1313
|
-
return;
|
|
1314
|
-
const collapsedContent = collapseBlankLines(content);
|
|
1315
|
-
if (collapsedContent.includes(collapsedFind)) {
|
|
1316
|
-
const contentLines = content.split("\n");
|
|
1317
|
-
const findLines = find.split("\n").filter((l) => l.trim().length > 0);
|
|
1318
|
-
if (findLines.length === 0)
|
|
1319
|
-
return;
|
|
1320
|
-
const firstNonEmpty = findLines[0].trim();
|
|
1321
|
-
const lastNonEmpty = findLines[findLines.length - 1].trim();
|
|
1322
|
-
for (let i = 0; i < contentLines.length; i++) {
|
|
1323
|
-
if (contentLines[i].trim() !== firstNonEmpty)
|
|
1324
|
-
continue;
|
|
1325
|
-
let foundEnd = -1;
|
|
1326
|
-
let nonEmptyCount = 0;
|
|
1327
|
-
for (let j = i; j < contentLines.length; j++) {
|
|
1328
|
-
if (contentLines[j].trim().length > 0) {
|
|
1329
|
-
nonEmptyCount++;
|
|
1330
|
-
}
|
|
1331
|
-
if (contentLines[j].trim() === lastNonEmpty && nonEmptyCount >= findLines.length) {
|
|
1332
|
-
foundEnd = j;
|
|
1333
|
-
break;
|
|
1334
|
-
}
|
|
1335
|
-
if (j - i > findLines.length * 2 + 5)
|
|
1336
|
-
break;
|
|
1337
|
-
}
|
|
1338
|
-
if (foundEnd >= 0) {
|
|
1339
|
-
const candidateLines = contentLines.slice(i, foundEnd + 1);
|
|
1340
|
-
const candidateNonEmpty = candidateLines.filter((l) => l.trim().length > 0);
|
|
1341
|
-
let allMatch = candidateNonEmpty.length === findLines.length;
|
|
1342
|
-
if (allMatch) {
|
|
1343
|
-
for (let m = 0; m < findLines.length; m++) {
|
|
1344
|
-
if (candidateNonEmpty[m].trim() !== findLines[m].trim()) {
|
|
1345
|
-
allMatch = false;
|
|
1346
|
-
break;
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
}
|
|
1350
|
-
if (allMatch) {
|
|
1351
|
-
yield candidateLines.join("\n");
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
};
|
|
1358
|
-
function fuzzyReplace(content, oldString, newString, replaceAll = false) {
|
|
1359
|
-
if (oldString === newString) {
|
|
1360
|
-
return { error: "No changes: oldString and newString are identical." };
|
|
1361
|
-
}
|
|
1362
|
-
const replacers = [
|
|
1363
|
-
["exact", SimpleReplacer],
|
|
1364
|
-
["line-trimmed", LineTrimmedReplacer],
|
|
1365
|
-
["block-anchor", BlockAnchorReplacer],
|
|
1366
|
-
["whitespace-normalized", WhitespaceNormalizedReplacer],
|
|
1367
|
-
["indentation-flexible", IndentationFlexibleReplacer],
|
|
1368
|
-
["escape-normalized", EscapeNormalizedReplacer],
|
|
1369
|
-
["trimmed-boundary", TrimmedBoundaryReplacer],
|
|
1370
|
-
["context-aware", ContextAwareReplacer],
|
|
1371
|
-
["multi-occurrence", MultiOccurrenceReplacer],
|
|
1372
|
-
["blank-line-tolerant", BlankLineTolerantReplacer]
|
|
1373
|
-
];
|
|
1374
|
-
let notFound = true;
|
|
1375
|
-
for (const [name, replacer] of replacers) {
|
|
1376
|
-
for (const search of replacer(content, oldString)) {
|
|
1377
|
-
const index = content.indexOf(search);
|
|
1378
|
-
if (index === -1)
|
|
1379
|
-
continue;
|
|
1380
|
-
notFound = false;
|
|
1381
|
-
if (replaceAll) {
|
|
1382
|
-
return { result: content.replaceAll(search, newString), strategy: name };
|
|
1383
|
-
}
|
|
1384
|
-
const lastIndex = content.lastIndexOf(search);
|
|
1385
|
-
if (index !== lastIndex)
|
|
1386
|
-
continue;
|
|
1387
|
-
return {
|
|
1388
|
-
result: content.substring(0, index) + newString + content.substring(index + search.length),
|
|
1389
|
-
strategy: name
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
if (notFound) {
|
|
1394
|
-
return { error: "Could not find oldString in the file. Tried 10 matching strategies including fuzzy whitespace, indentation, blank-line-tolerant, and block-anchor matching." };
|
|
1395
|
-
}
|
|
1396
|
-
return { error: "Found multiple matches for oldString. Provide more surrounding context to make the match unique." };
|
|
1397
|
-
}
|
|
1398
|
-
program.name("trace").description("Trace IDE Bridge \u2014 connect your codebase and dev server to the Trace extension").version(VERSION);
|
|
1399
|
-
program.command("install-native").description("Register the Trace native messaging host so the Chrome extension can auto-launch trace dev").action(async () => {
|
|
1400
|
-
const { execFileSync } = await import("child_process");
|
|
1401
|
-
const { fileURLToPath: fileURLToPath2 } = await import("url");
|
|
1402
|
-
const { dirname: dirname2, join: join5 } = await import("path");
|
|
1403
|
-
const __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
1404
|
-
const script = join5(__dirname2, "..", "scripts", "postinstall.js");
|
|
1405
|
-
try {
|
|
1406
|
-
execFileSync(process.execPath, [script], { stdio: "inherit" });
|
|
1407
|
-
} catch (e) {
|
|
1408
|
-
console.error(chalk2.red("\u2717 Native host registration failed:"), e.message);
|
|
1409
|
-
process.exit(1);
|
|
1410
|
-
}
|
|
1411
|
-
});
|
|
1412
|
-
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) => {
|
|
1413
|
-
const port = parseInt(options.port);
|
|
1414
|
-
const projectPath = process.cwd();
|
|
1415
|
-
const { command, pm, script } = detectDevCommand(projectPath, commandOverride);
|
|
1416
|
-
const useTui = options.ui !== false && process.stdout.isTTY;
|
|
1417
|
-
const tui = useTui ? new TraceTUI({
|
|
1418
|
-
project: projectPath,
|
|
1419
|
-
pm,
|
|
1420
|
-
devCommand: command,
|
|
1421
|
-
bridgePort: port,
|
|
1422
|
-
agentPort: 8766,
|
|
1423
|
-
browserPort: 8767,
|
|
1424
|
-
bridgeReady: false,
|
|
1425
|
-
agentReady: false,
|
|
1426
|
-
devReady: false,
|
|
1427
|
-
extConnected: false,
|
|
1428
|
-
clientCount: 0,
|
|
1429
|
-
agentExtras: "",
|
|
1430
|
-
cliVersion: VERSION
|
|
1431
|
-
}) : null;
|
|
1432
|
-
if (tui)
|
|
1433
|
-
tui.start();
|
|
1434
|
-
const logTrace = (msg) => {
|
|
1435
|
-
if (tui)
|
|
1436
|
-
tui.pushTrace(msg);
|
|
1437
|
-
else
|
|
1438
|
-
console.log(msg);
|
|
1439
|
-
};
|
|
1440
|
-
const logTraceErr = (msg) => {
|
|
1441
|
-
if (tui)
|
|
1442
|
-
tui.pushTrace(chalk2.red(msg));
|
|
1443
|
-
else
|
|
1444
|
-
console.error(msg);
|
|
1445
|
-
};
|
|
1446
|
-
if (!tui) {
|
|
1447
|
-
console.log();
|
|
1448
|
-
console.log(chalk2.bold.cyan("\u26A1 Trace Dev"));
|
|
1449
|
-
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
1450
|
-
console.log();
|
|
1451
|
-
console.log(`\u{1F4C1} Project: ${chalk2.green(projectPath)}`);
|
|
1452
|
-
console.log(`\u{1F4E6} Package Mgr: ${chalk2.yellow(pm)}`);
|
|
1453
|
-
console.log(`\u{1F680} Dev Command: ${chalk2.cyan(command)}`);
|
|
1454
|
-
console.log(`\u{1F310} Bridge Port: ${chalk2.cyan(port)}`);
|
|
1455
|
-
console.log();
|
|
1456
|
-
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
1457
|
-
console.log();
|
|
1458
|
-
}
|
|
1459
|
-
const connectedClients = /* @__PURE__ */ new Set();
|
|
1460
|
-
const broadcast = (payload) => {
|
|
1461
|
-
const msg = JSON.stringify(payload);
|
|
1462
|
-
for (const client of connectedClients) {
|
|
1463
|
-
if (client.readyState === WebSocket.OPEN) {
|
|
1464
|
-
client.send(msg);
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
};
|
|
1468
|
-
logTrace(chalk2.dim("Starting dev server..."));
|
|
1469
|
-
const childEnv = { ...process.env, FORCE_COLOR: "1" };
|
|
1470
|
-
delete childEnv.MallocNanoZone;
|
|
1471
|
-
delete childEnv.MallocStackLogging;
|
|
1472
|
-
delete childEnv.MallocScribble;
|
|
1473
|
-
delete childEnv.MallocGuardEdges;
|
|
1474
|
-
delete childEnv.MallocErrorAbort;
|
|
1475
|
-
function hasRunnableDevScript() {
|
|
1476
|
-
const result = _checkPkg(projectPath, commandOverride);
|
|
1477
|
-
if (result.ok)
|
|
1478
|
-
return result;
|
|
1479
|
-
const isGreenfield = projectPath.replace(/\/+$/, "").endsWith("/Trace/greenfield");
|
|
1480
|
-
if (!isGreenfield) {
|
|
1481
|
-
try {
|
|
1482
|
-
const entries = fs4.readdirSync(projectPath, { withFileTypes: true });
|
|
1483
|
-
for (const entry of entries) {
|
|
1484
|
-
if (!entry.isDirectory())
|
|
1485
|
-
continue;
|
|
1486
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
1487
|
-
continue;
|
|
1488
|
-
const sub = path4.join(projectPath, entry.name);
|
|
1489
|
-
const r = _checkPkg(sub, void 0);
|
|
1490
|
-
if (r.ok)
|
|
1491
|
-
return r;
|
|
1492
|
-
}
|
|
1493
|
-
} catch {
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
return { ok: false };
|
|
1497
|
-
}
|
|
1498
|
-
function _checkPkg(cwd, override) {
|
|
1499
|
-
const pkgPath = path4.join(cwd, "package.json");
|
|
1500
|
-
if (!fs4.existsSync(pkgPath))
|
|
1501
|
-
return { ok: false };
|
|
1502
|
-
try {
|
|
1503
|
-
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
1504
|
-
const scripts = pkg.scripts || {};
|
|
1505
|
-
const candidates = ["dev", "start", "serve", "preview"];
|
|
1506
|
-
const script2 = candidates.find((c) => !!scripts[c]);
|
|
1507
|
-
if (!script2)
|
|
1508
|
-
return { ok: false };
|
|
1509
|
-
const detected = detectDevCommand(cwd, override);
|
|
1510
|
-
return { ok: true, cmd: detected.command, script: detected.script, cwd };
|
|
1511
|
-
} catch {
|
|
1512
|
-
return { ok: false };
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
function spawnDevProcess(cmd, cwd = projectPath) {
|
|
1516
|
-
if (devProcess && !devProcess.killed) {
|
|
1517
|
-
logTrace(chalk2.dim(`[Trace] Dev process already running \u2014 skipping duplicate spawn for ${cwd}`));
|
|
1518
|
-
return;
|
|
1519
|
-
}
|
|
1520
|
-
devProcess = spawn2(cmd, [], {
|
|
1521
|
-
cwd,
|
|
1522
|
-
shell: true,
|
|
1523
|
-
env: childEnv
|
|
1524
|
-
});
|
|
1525
|
-
devProcess.stdout?.on("data", (chunk) => {
|
|
1526
|
-
const text = chunk.toString();
|
|
1527
|
-
if (tui)
|
|
1528
|
-
tui.pushApp(text);
|
|
1529
|
-
else
|
|
1530
|
-
process.stdout.write(text);
|
|
1531
|
-
globalTerminalBuffer.push(text);
|
|
1532
|
-
broadcast({ type: "STREAM_CHUNK", stream: "stdout", chunk: text });
|
|
1533
|
-
if (tui && /\b(Ready in|ready in|listening on|Local:)\b/i.test(text)) {
|
|
1534
|
-
tui.setStatus({ devReady: true });
|
|
1535
|
-
}
|
|
1536
|
-
});
|
|
1537
|
-
devProcess.stderr?.on("data", (chunk) => {
|
|
1538
|
-
const text = chunk.toString();
|
|
1539
|
-
if (tui)
|
|
1540
|
-
tui.pushApp(text);
|
|
1541
|
-
else
|
|
1542
|
-
process.stderr.write(text);
|
|
1543
|
-
globalTerminalBuffer.push(text);
|
|
1544
|
-
broadcast({ type: "STREAM_CHUNK", stream: "stderr", chunk: text });
|
|
1545
|
-
});
|
|
1546
|
-
devProcess.on("close", (code) => {
|
|
1547
|
-
const msg = `[Trace] Dev server exited with code ${code}`;
|
|
1548
|
-
if (tui)
|
|
1549
|
-
tui.pushTrace(chalk2.yellow(msg));
|
|
1550
|
-
else
|
|
1551
|
-
process.stdout.write(chalk2.yellow(msg) + "\n");
|
|
1552
|
-
globalTerminalBuffer.push(msg);
|
|
1553
|
-
broadcast({ type: "STREAM_END", exitCode: code });
|
|
1554
|
-
if (tui)
|
|
1555
|
-
tui.setStatus({ devReady: false });
|
|
1556
|
-
devProcess = null;
|
|
1557
|
-
});
|
|
1558
|
-
devProcess.on("error", (err) => {
|
|
1559
|
-
const msg = `[Trace] Failed to start dev server: ${err.message}`;
|
|
1560
|
-
logTraceErr(msg);
|
|
1561
|
-
logTraceErr(`\u2717 Could not start dev server. Command: ${cmd}`);
|
|
1562
|
-
});
|
|
1563
|
-
}
|
|
1564
|
-
const wss = new WebSocketServer({ port });
|
|
1565
|
-
let clientCount = 0;
|
|
1566
|
-
wss.on("listening", () => {
|
|
1567
|
-
if (tui) {
|
|
1568
|
-
tui.setStatus({ bridgeReady: true });
|
|
1569
|
-
tui.pushTrace(chalk2.green("\u2713") + ` IDE Bridge listening on port ${port}`);
|
|
1570
|
-
tui.pushTrace(chalk2.dim("Waiting for Trace extension to connect..."));
|
|
1571
|
-
} else {
|
|
1572
|
-
console.log();
|
|
1573
|
-
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
1574
|
-
console.log(chalk2.green("\u2713") + " IDE Bridge listening on port " + chalk2.cyan(port));
|
|
1575
|
-
console.log(chalk2.dim("Waiting for Trace extension to connect..."));
|
|
1576
|
-
console.log(chalk2.dim("Press Ctrl+C to stop both"));
|
|
1577
|
-
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
1578
|
-
console.log();
|
|
1579
|
-
}
|
|
1580
|
-
});
|
|
1581
|
-
wss.on("connection", (ws) => {
|
|
1582
|
-
clientCount++;
|
|
1583
|
-
connectedClients.add(ws);
|
|
1584
|
-
if (tui) {
|
|
1585
|
-
tui.setStatus({ extConnected: true, clientCount });
|
|
1586
|
-
tui.pushTrace(chalk2.green("\u25CF") + ` Extension connected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
1587
|
-
} else {
|
|
1588
|
-
console.log(chalk2.green("\u25CF") + ` Extension connected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
1589
|
-
}
|
|
1590
|
-
attachMessageHandler(ws, projectPath);
|
|
1591
|
-
const catchup = globalTerminalBuffer.getLast(100);
|
|
1592
|
-
if (catchup.length > 0) {
|
|
1593
|
-
ws.send(JSON.stringify({ type: "TERMINAL_CATCHUP", lines: catchup }));
|
|
1594
|
-
}
|
|
1595
|
-
ws.on("close", () => {
|
|
1596
|
-
clientCount--;
|
|
1597
|
-
connectedClients.delete(ws);
|
|
1598
|
-
if (tui) {
|
|
1599
|
-
tui.setStatus({ extConnected: clientCount > 0, clientCount });
|
|
1600
|
-
tui.pushTrace(chalk2.yellow("\u25CF") + ` Extension disconnected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
1601
|
-
} else {
|
|
1602
|
-
console.log(chalk2.yellow("\u25CF") + ` Extension disconnected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
1603
|
-
}
|
|
1604
|
-
});
|
|
1605
|
-
ws.on("error", (err) => {
|
|
1606
|
-
console.error(chalk2.red("WebSocket error:"), err.message);
|
|
1607
|
-
connectedClients.delete(ws);
|
|
1608
|
-
});
|
|
1609
|
-
});
|
|
1610
|
-
wss.on("error", (error) => {
|
|
1611
|
-
if (error.code === "EADDRINUSE") {
|
|
1612
|
-
console.error(chalk2.red(`\u2717 Port ${port} is already in use. Try: trace dev --port 8766`));
|
|
1613
|
-
} else {
|
|
1614
|
-
console.error(chalk2.red("Bridge error:"), error.message);
|
|
1615
|
-
}
|
|
1616
|
-
});
|
|
1617
|
-
const http = await import("http");
|
|
1618
|
-
const browserHttpServer = http.createServer(async (req, res) => {
|
|
1619
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1620
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1621
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1622
|
-
if (req.method === "OPTIONS") {
|
|
1623
|
-
res.writeHead(204);
|
|
1624
|
-
res.end();
|
|
1625
|
-
return;
|
|
1626
|
-
}
|
|
1627
|
-
const url = new URL(req.url, `http://localhost`);
|
|
1628
|
-
const agentRouteMatch = url.pathname.match(/^\/agent\/([a-z_-]+)$/);
|
|
1629
|
-
if (agentRouteMatch) {
|
|
1630
|
-
const agentName = agentRouteMatch[1];
|
|
1631
|
-
const client2 = [...connectedClients].find((c) => c.readyState === 1);
|
|
1632
|
-
if (!client2) {
|
|
1633
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1634
|
-
res.end(JSON.stringify({ error: "Trace extension not connected." }));
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
let body = {};
|
|
1638
|
-
if (req.method === "POST") {
|
|
1639
|
-
try {
|
|
1640
|
-
body = await new Promise((resolve3, reject) => {
|
|
1641
|
-
let raw = "";
|
|
1642
|
-
req.on("data", (chunk) => raw += chunk.toString());
|
|
1643
|
-
req.on("end", () => {
|
|
1644
|
-
try {
|
|
1645
|
-
resolve3(JSON.parse(raw || "{}"));
|
|
1646
|
-
} catch {
|
|
1647
|
-
resolve3({});
|
|
1648
|
-
}
|
|
1649
|
-
});
|
|
1650
|
-
req.on("error", reject);
|
|
1651
|
-
});
|
|
1652
|
-
} catch (e) {
|
|
1653
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1654
|
-
res.end(JSON.stringify({ error: "Invalid request body" }));
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
res.writeHead(200, {
|
|
1659
|
-
"Content-Type": "text/event-stream",
|
|
1660
|
-
"Cache-Control": "no-cache, no-transform",
|
|
1661
|
-
"Connection": "keep-alive",
|
|
1662
|
-
// Some HTTP layers (proxies, fetch in node) buffer responses
|
|
1663
|
-
// unless we tell them not to.
|
|
1664
|
-
"X-Accel-Buffering": "no"
|
|
1665
|
-
});
|
|
1666
|
-
const writeEvent = (payload) => {
|
|
1667
|
-
try {
|
|
1668
|
-
if (!res.writableEnded) {
|
|
1669
|
-
res.write(`data: ${JSON.stringify(payload)}
|
|
1670
|
-
|
|
1671
|
-
`);
|
|
1672
|
-
}
|
|
1673
|
-
} catch {
|
|
1674
|
-
}
|
|
1675
|
-
};
|
|
1676
|
-
writeEvent({ type: "agent_start", agent: agentName });
|
|
1677
|
-
const reqId = ++globalBrowserRequestId;
|
|
1678
|
-
const timer = setTimeout(() => {
|
|
1679
|
-
const pending = globalBrowserPending.get(reqId);
|
|
1680
|
-
if (pending) {
|
|
1681
|
-
globalBrowserPending.delete(reqId);
|
|
1682
|
-
writeEvent({ type: "error", error: "Agent timeout (65s)" });
|
|
1683
|
-
try {
|
|
1684
|
-
res.end();
|
|
1685
|
-
} catch {
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
}, 65e3);
|
|
1689
|
-
req.on("close", () => {
|
|
1690
|
-
if (globalBrowserPending.has(reqId)) {
|
|
1691
|
-
clearTimeout(timer);
|
|
1692
|
-
globalBrowserPending.delete(reqId);
|
|
1693
|
-
}
|
|
1694
|
-
});
|
|
1695
|
-
globalBrowserPending.set(reqId, {
|
|
1696
|
-
resolve: (data) => {
|
|
1697
|
-
clearTimeout(timer);
|
|
1698
|
-
writeEvent({ type: "result", data });
|
|
1699
|
-
try {
|
|
1700
|
-
res.end();
|
|
1701
|
-
} catch {
|
|
1702
|
-
}
|
|
1703
|
-
},
|
|
1704
|
-
reject: (e) => {
|
|
1705
|
-
clearTimeout(timer);
|
|
1706
|
-
writeEvent({ type: "error", error: e?.message || String(e) });
|
|
1707
|
-
try {
|
|
1708
|
-
res.end();
|
|
1709
|
-
} catch {
|
|
1710
|
-
}
|
|
1711
|
-
},
|
|
1712
|
-
timer,
|
|
1713
|
-
onProgress: (event) => writeEvent(event)
|
|
1714
|
-
});
|
|
1715
|
-
client2.send(JSON.stringify({
|
|
1716
|
-
id: reqId,
|
|
1717
|
-
type: "AGENT_INVOKE",
|
|
1718
|
-
agent: agentName,
|
|
1719
|
-
query: body.query || ""
|
|
1720
|
-
}));
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
const browserQueryRoutes = {
|
|
1724
|
-
"/browser/console": "BROWSER_GET_CONSOLE",
|
|
1725
|
-
"/browser/network": "BROWSER_GET_NETWORK",
|
|
1726
|
-
"/browser/dom": "BROWSER_GET_DOM",
|
|
1727
|
-
"/browser/screenshot": "BROWSER_SCREENSHOT",
|
|
1728
|
-
"/browser/verify-build": "BROWSER_VERIFY_BUILD",
|
|
1729
|
-
"/browser/find": "BROWSER_FIND",
|
|
1730
|
-
"/browser/click": "BROWSER_CLICK",
|
|
1731
|
-
"/browser/type": "BROWSER_TYPE",
|
|
1732
|
-
"/browser/wait-for": "BROWSER_WAIT_FOR"
|
|
1733
|
-
};
|
|
1734
|
-
const isBrowserRoute = url.pathname in browserQueryRoutes || url.pathname === "/browser/eval";
|
|
1735
|
-
if (!isBrowserRoute) {
|
|
1736
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1737
|
-
res.end(JSON.stringify({ error: "Not found. Available: /browser/{console,network,dom,eval,screenshot,verify-build,find,click,type,wait-for}" }));
|
|
1738
|
-
return;
|
|
1739
|
-
}
|
|
1740
|
-
const client = [...connectedClients].find(
|
|
1741
|
-
(c) => c.readyState === 1
|
|
1742
|
-
/* OPEN */
|
|
1743
|
-
);
|
|
1744
|
-
if (!client) {
|
|
1745
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1746
|
-
res.end(JSON.stringify({ error: "Trace extension not connected. Open the extension and attach to a tab." }));
|
|
1747
|
-
return;
|
|
1748
|
-
}
|
|
1749
|
-
try {
|
|
1750
|
-
let body = {};
|
|
1751
|
-
if (req.method === "POST") {
|
|
1752
|
-
body = await new Promise((resolve3, reject) => {
|
|
1753
|
-
let raw = "";
|
|
1754
|
-
req.on("data", (chunk) => raw += chunk.toString());
|
|
1755
|
-
req.on("end", () => {
|
|
1756
|
-
try {
|
|
1757
|
-
resolve3(JSON.parse(raw || "{}"));
|
|
1758
|
-
} catch {
|
|
1759
|
-
resolve3({});
|
|
1760
|
-
}
|
|
1761
|
-
});
|
|
1762
|
-
req.on("error", reject);
|
|
1763
|
-
});
|
|
1764
|
-
}
|
|
1765
|
-
const reqId = ++globalBrowserRequestId;
|
|
1766
|
-
const msgType = browserQueryRoutes[url.pathname] || "BROWSER_EVAL";
|
|
1767
|
-
const wsMsg = { id: reqId, type: msgType };
|
|
1768
|
-
const params = { ...body || {} };
|
|
1769
|
-
for (const [k, v] of url.searchParams.entries()) {
|
|
1770
|
-
if (!(k in params))
|
|
1771
|
-
params[k] = v;
|
|
1772
|
-
}
|
|
1773
|
-
for (const k of Object.keys(params)) {
|
|
1774
|
-
if (k === "id" || k === "type")
|
|
1775
|
-
continue;
|
|
1776
|
-
wsMsg[k] = params[k];
|
|
1777
|
-
}
|
|
1778
|
-
client.send(JSON.stringify(wsMsg));
|
|
1779
|
-
const browserQueryTimeoutMs = url.pathname === "/browser/screenshot" ? 15e3 : url.pathname === "/browser/wait-for" ? Math.min(Number(params.timeout) || 8e3, 3e4) + 4e3 : 5e3;
|
|
1780
|
-
const result = await new Promise((resolve3, reject) => {
|
|
1781
|
-
const timer = setTimeout(() => {
|
|
1782
|
-
globalBrowserPending.delete(reqId);
|
|
1783
|
-
reject(new Error("Browser query timeout (" + Math.round(browserQueryTimeoutMs / 1e3) + "s). Extension may not be attached to a tab."));
|
|
1784
|
-
}, browserQueryTimeoutMs);
|
|
1785
|
-
globalBrowserPending.set(reqId, { resolve: resolve3, reject, timer });
|
|
1786
|
-
});
|
|
1787
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1788
|
-
res.end(JSON.stringify(result, null, 2));
|
|
1789
|
-
} catch (e) {
|
|
1790
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1791
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
1792
|
-
}
|
|
1793
|
-
});
|
|
1794
|
-
const BROWSER_HTTP_PORT = 8767;
|
|
1795
|
-
browserHttpServer.listen(BROWSER_HTTP_PORT, "127.0.0.1", () => {
|
|
1796
|
-
if (tui) {
|
|
1797
|
-
tui.pushTrace(chalk2.green("\u2713") + ` Browser Query HTTP server on port ${BROWSER_HTTP_PORT}`);
|
|
1798
|
-
} else {
|
|
1799
|
-
console.log(chalk2.green("\u2713") + " Browser Query HTTP server on port " + chalk2.cyan(BROWSER_HTTP_PORT));
|
|
1800
|
-
console.log(chalk2.dim(" Coding agents can query: curl http://localhost:8767/browser/console"));
|
|
1801
|
-
}
|
|
1802
|
-
});
|
|
1803
|
-
browserHttpServer.on("error", (e) => {
|
|
1804
|
-
if (e.code !== "EADDRINUSE")
|
|
1805
|
-
console.warn(chalk2.yellow("\u26A0 Browser HTTP server error:"), e.message);
|
|
1806
|
-
});
|
|
1807
|
-
let agentServer = null;
|
|
1808
|
-
try {
|
|
1809
|
-
let agentPath = "@gettrace/agent/dist/node/index.js";
|
|
1810
|
-
if (process.env.TRACE_DEV_MODE) {
|
|
1811
|
-
agentPath = path4.resolve(__dirname, "../../packages/trace-agent/dist/node/index.js");
|
|
1812
|
-
if (tui)
|
|
1813
|
-
tui.pushTrace(chalk2.magenta("\u2699") + " Dev Mode: Using local trace agent at " + chalk2.dim(agentPath));
|
|
1814
|
-
else
|
|
1815
|
-
console.log(chalk2.magenta("\u2699") + " Dev Mode: Using local trace agent at " + chalk2.dim(agentPath));
|
|
1816
|
-
}
|
|
1817
|
-
const { Server } = await import(agentPath);
|
|
1818
|
-
process.env.OPENCODE_EXPERIMENTAL = "1";
|
|
1819
|
-
process.env.OPENCODE_ENABLE_EXA = "1";
|
|
1820
|
-
process.env.OPENCODE_EXPERIMENTAL_LSP_TOOL = "1";
|
|
1821
|
-
process.env.OPENCODE_ENABLE_QUESTION_TOOL = "1";
|
|
1822
|
-
process.env.OPENCODE_CLIENT = "app";
|
|
1823
|
-
process.env.OPENCODE_DISABLE_AUTOUPDATE = "1";
|
|
1824
|
-
process.env.OPENCODE_DISABLE_TERMINAL_TITLE = "1";
|
|
1825
|
-
agentServer = await Server.listen({
|
|
1826
|
-
port: 8766,
|
|
1827
|
-
hostname: "127.0.0.1",
|
|
1828
|
-
cors: ["*"]
|
|
1829
|
-
});
|
|
1830
|
-
if (tui) {
|
|
1831
|
-
tui.setStatus({
|
|
1832
|
-
agentReady: true,
|
|
1833
|
-
agentExtras: "Sonnet 4.5 \xB7 LSP \xB7 Exa \xB7 Plan"
|
|
1834
|
-
});
|
|
1835
|
-
tui.pushTrace(chalk2.green("\u2713") + " Trace Agent Server listening on port 8766");
|
|
1836
|
-
} else {
|
|
1837
|
-
console.log(chalk2.green("\u2713") + " Trace Agent Server listening on port " + chalk2.cyan(8766));
|
|
1838
|
-
console.log(chalk2.dim(" LSP \xB7 Exa search \xB7 Plan mode \xB7 All tools unlocked"));
|
|
1839
|
-
}
|
|
1840
|
-
} catch (e) {
|
|
1841
|
-
const msg = "\u26A0 Failed to start Trace Agent Server: " + e.message;
|
|
1842
|
-
if (tui)
|
|
1843
|
-
tui.pushTrace(chalk2.yellow(msg));
|
|
1844
|
-
else
|
|
1845
|
-
console.error(chalk2.yellow(msg));
|
|
1846
|
-
}
|
|
1847
|
-
let _devServerId = null;
|
|
1848
|
-
let _devServerOutputSeen = 0;
|
|
1849
|
-
let _devServerPollTimer = null;
|
|
1850
|
-
let _devServerSseAbort = null;
|
|
1851
|
-
function _drainAgentDevOutput(lines) {
|
|
1852
|
-
if (!Array.isArray(lines) || !lines.length)
|
|
1853
|
-
return;
|
|
1854
|
-
if (_devServerOutputSeen >= lines.length) {
|
|
1855
|
-
_devServerOutputSeen = Math.max(0, lines.length - 100);
|
|
1856
|
-
}
|
|
1857
|
-
for (const line of lines.slice(_devServerOutputSeen)) {
|
|
1858
|
-
const text = line + "\n";
|
|
1859
|
-
if (tui)
|
|
1860
|
-
tui.pushApp(text);
|
|
1861
|
-
else
|
|
1862
|
-
process.stdout.write(text);
|
|
1863
|
-
globalTerminalBuffer.push(text);
|
|
1864
|
-
broadcast({ type: "STREAM_CHUNK", stream: "stdout", chunk: text });
|
|
1865
|
-
}
|
|
1866
|
-
_devServerOutputSeen = lines.length;
|
|
1867
|
-
}
|
|
1868
|
-
function _stopAgentDevPoll() {
|
|
1869
|
-
if (_devServerPollTimer) {
|
|
1870
|
-
clearInterval(_devServerPollTimer);
|
|
1871
|
-
_devServerPollTimer = null;
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
function _startAgentDevPoll(id) {
|
|
1875
|
-
_stopAgentDevPoll();
|
|
1876
|
-
_devServerPollTimer = setInterval(async () => {
|
|
1877
|
-
try {
|
|
1878
|
-
const res = await fetch(`http://127.0.0.1:8766/dev-server/${encodeURIComponent(id)}`);
|
|
1879
|
-
if (!res.ok)
|
|
1880
|
-
return;
|
|
1881
|
-
const handle = await res.json().catch(() => null);
|
|
1882
|
-
if (!handle)
|
|
1883
|
-
return;
|
|
1884
|
-
_drainAgentDevOutput(handle.output || []);
|
|
1885
|
-
const status = handle.state?.status;
|
|
1886
|
-
if (status === "exited" || status === "stopped" || status === "failed") {
|
|
1887
|
-
_stopAgentDevPoll();
|
|
1888
|
-
if (tui)
|
|
1889
|
-
tui.setStatus({ devReady: false });
|
|
1890
|
-
} else if (status === "ready" && tui) {
|
|
1891
|
-
tui.setStatus({ devReady: true });
|
|
1892
|
-
}
|
|
1893
|
-
} catch {
|
|
1894
|
-
}
|
|
1895
|
-
}, 500);
|
|
1896
|
-
}
|
|
1897
|
-
async function _subscribeAgentDevEvents() {
|
|
1898
|
-
const ctrl = _devServerSseAbort;
|
|
1899
|
-
if (!ctrl)
|
|
1900
|
-
return;
|
|
1901
|
-
while (!ctrl.signal.aborted) {
|
|
1902
|
-
try {
|
|
1903
|
-
const res = await fetch("http://127.0.0.1:8766/global/event", {
|
|
1904
|
-
signal: ctrl.signal,
|
|
1905
|
-
headers: { Accept: "text/event-stream" }
|
|
1906
|
-
});
|
|
1907
|
-
if (!res.ok || !res.body) {
|
|
1908
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
1909
|
-
continue;
|
|
1910
|
-
}
|
|
1911
|
-
const reader = res.body.getReader();
|
|
1912
|
-
const decoder = new TextDecoder();
|
|
1913
|
-
let buf = "";
|
|
1914
|
-
while (!ctrl.signal.aborted) {
|
|
1915
|
-
const { value, done } = await reader.read();
|
|
1916
|
-
if (done)
|
|
1917
|
-
break;
|
|
1918
|
-
buf += decoder.decode(value, { stream: true });
|
|
1919
|
-
let idx;
|
|
1920
|
-
while ((idx = buf.indexOf("\n\n")) !== -1) {
|
|
1921
|
-
const frame = buf.slice(0, idx);
|
|
1922
|
-
buf = buf.slice(idx + 2);
|
|
1923
|
-
const dataLine = frame.split("\n").find((l) => l.startsWith("data:"));
|
|
1924
|
-
if (!dataLine)
|
|
1925
|
-
continue;
|
|
1926
|
-
const dataStr = dataLine.replace(/^data:\s*/, "");
|
|
1927
|
-
try {
|
|
1928
|
-
const parsed = JSON.parse(dataStr);
|
|
1929
|
-
const type = parsed?.payload?.type;
|
|
1930
|
-
const props = parsed?.payload?.properties;
|
|
1931
|
-
if (!type || !props)
|
|
1932
|
-
continue;
|
|
1933
|
-
if (props.id && _devServerId && props.id !== _devServerId)
|
|
1934
|
-
continue;
|
|
1935
|
-
if (type === "dev_server.starting") {
|
|
1936
|
-
if (!_devServerId && props.id) {
|
|
1937
|
-
_devServerId = props.id;
|
|
1938
|
-
_startAgentDevPoll(props.id);
|
|
1939
|
-
}
|
|
1940
|
-
} else if (type === "dev_server.ready") {
|
|
1941
|
-
if (tui)
|
|
1942
|
-
tui.setStatus({ devReady: true });
|
|
1943
|
-
} else if (type === "dev_server.failed" || type === "dev_server.exited") {
|
|
1944
|
-
if (tui)
|
|
1945
|
-
tui.setStatus({ devReady: false });
|
|
1946
|
-
}
|
|
1947
|
-
} catch {
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
} catch {
|
|
1952
|
-
if (ctrl.signal.aborted)
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
await new Promise((r) => setTimeout(r, 1e3));
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
async function spawnViaAgentServer(cmd, cwd) {
|
|
1959
|
-
if (_devServerId) {
|
|
1960
|
-
logTrace(chalk2.dim(`[Trace] Dev server already managed by agent (id=${_devServerId}) \u2014 skipping duplicate`));
|
|
1961
|
-
return { ok: true };
|
|
1962
|
-
}
|
|
1963
|
-
_devServerSseAbort = new AbortController();
|
|
1964
|
-
void _subscribeAgentDevEvents();
|
|
1965
|
-
let res;
|
|
1966
|
-
try {
|
|
1967
|
-
res = await fetch("http://127.0.0.1:8766/dev-server", {
|
|
1968
|
-
method: "POST",
|
|
1969
|
-
headers: { "Content-Type": "application/json" },
|
|
1970
|
-
body: JSON.stringify({ command: cmd, cwd, timeoutMs: 9e4 })
|
|
1971
|
-
});
|
|
1972
|
-
} catch (e) {
|
|
1973
|
-
return { ok: false, reason: e?.message ?? String(e) };
|
|
1974
|
-
}
|
|
1975
|
-
if (!res.ok) {
|
|
1976
|
-
const text = await res.text().catch(() => "");
|
|
1977
|
-
return { ok: false, reason: `HTTP ${res.status}: ${text || res.statusText}` };
|
|
1978
|
-
}
|
|
1979
|
-
const handle = await res.json().catch(() => null);
|
|
1980
|
-
if (!handle?.id)
|
|
1981
|
-
return { ok: false, reason: "no_id_returned" };
|
|
1982
|
-
_devServerId = handle.id;
|
|
1983
|
-
_drainAgentDevOutput(handle.output || []);
|
|
1984
|
-
_startAgentDevPoll(handle.id);
|
|
1985
|
-
if (handle.state?.status === "ready") {
|
|
1986
|
-
if (tui)
|
|
1987
|
-
tui.setStatus({ devReady: true });
|
|
1988
|
-
return { ok: true };
|
|
1989
|
-
}
|
|
1990
|
-
return {
|
|
1991
|
-
ok: false,
|
|
1992
|
-
reason: handle.state?.error || `state: ${handle.state?.status ?? "unknown"}`
|
|
1993
|
-
};
|
|
1994
|
-
}
|
|
1995
|
-
const initial = commandOverride ? { ok: true, cmd: command, script: "override", cwd: projectPath } : hasRunnableDevScript();
|
|
1996
|
-
if (initial.ok && initial.cmd) {
|
|
1997
|
-
if (agentServer) {
|
|
1998
|
-
logTrace(chalk2.dim(`Starting dev server via agent: ${initial.cmd}`));
|
|
1999
|
-
const result = await spawnViaAgentServer(initial.cmd, initial.cwd ?? projectPath);
|
|
2000
|
-
if (!result.ok) {
|
|
2001
|
-
logTraceErr(`\u26A0 Agent-routed dev_server failed: ${result.reason}. Falling back to direct spawn.`);
|
|
2002
|
-
spawnDevProcess(initial.cmd, initial.cwd ?? projectPath);
|
|
2003
|
-
}
|
|
2004
|
-
} else {
|
|
2005
|
-
logTrace(chalk2.yellow("\u26A0 Agent server unavailable \u2014 using direct dev spawn (no cross-process dedup)"));
|
|
2006
|
-
spawnDevProcess(initial.cmd, initial.cwd ?? projectPath);
|
|
2007
|
-
}
|
|
2008
|
-
} else {
|
|
2009
|
-
logTrace(chalk2.dim("No dev script at startup \u2014 the agent will start one via dev_server when it scaffolds."));
|
|
2010
|
-
}
|
|
2011
|
-
const shutdown = async () => {
|
|
2012
|
-
if (tui)
|
|
2013
|
-
tui.shutdown();
|
|
2014
|
-
console.log();
|
|
2015
|
-
console.log(chalk2.yellow("Shutting down..."));
|
|
2016
|
-
if (_devServerSseAbort) {
|
|
2017
|
-
try {
|
|
2018
|
-
_devServerSseAbort.abort();
|
|
2019
|
-
} catch {
|
|
2020
|
-
}
|
|
2021
|
-
_devServerSseAbort = null;
|
|
2022
|
-
}
|
|
2023
|
-
_stopAgentDevPoll();
|
|
2024
|
-
if (_devServerId) {
|
|
2025
|
-
try {
|
|
2026
|
-
await fetch(`http://127.0.0.1:8766/dev-server/${encodeURIComponent(_devServerId)}/stop`, {
|
|
2027
|
-
method: "POST",
|
|
2028
|
-
signal: AbortSignal.timeout(3e3)
|
|
2029
|
-
});
|
|
2030
|
-
} catch {
|
|
2031
|
-
}
|
|
2032
|
-
_devServerId = null;
|
|
2033
|
-
}
|
|
2034
|
-
if (devProcess && !devProcess.killed) {
|
|
2035
|
-
devProcess.kill("SIGTERM");
|
|
2036
|
-
}
|
|
2037
|
-
if (agentServer) {
|
|
2038
|
-
await agentServer.stop();
|
|
2039
|
-
}
|
|
2040
|
-
wss.close();
|
|
2041
|
-
process.exit(0);
|
|
2042
|
-
};
|
|
2043
|
-
process.on("SIGINT", shutdown);
|
|
2044
|
-
process.on("SIGTERM", shutdown);
|
|
2045
|
-
});
|
|
2046
|
-
var globalBrowserPending = /* @__PURE__ */ new Map();
|
|
2047
|
-
var globalBrowserRequestId = 0;
|
|
2048
|
-
function attachMessageHandler(ws, projectPath) {
|
|
2049
|
-
const undoStack = [];
|
|
2050
|
-
let projectInfo = { projectPath };
|
|
2051
|
-
try {
|
|
2052
|
-
const pkgPath = path4.join(projectPath, "package.json");
|
|
2053
|
-
if (fs4.existsSync(pkgPath)) {
|
|
2054
|
-
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
2055
|
-
projectInfo = { ...projectInfo, name: pkg.name, version: pkg.version, description: pkg.description };
|
|
2056
|
-
}
|
|
2057
|
-
} catch (_) {
|
|
2058
|
-
}
|
|
2059
|
-
ws.on("message", async (data) => {
|
|
2060
|
-
try {
|
|
2061
|
-
const message = JSON.parse(data.toString());
|
|
2062
|
-
const { id, type } = message;
|
|
2063
|
-
let response = { id };
|
|
2064
|
-
switch (type) {
|
|
2065
|
-
case "GET_PROJECT_INFO":
|
|
2066
|
-
response.data = projectInfo;
|
|
2067
|
-
break;
|
|
2068
|
-
case "READ_FILE":
|
|
2069
|
-
try {
|
|
2070
|
-
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2071
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
2072
|
-
response.error = "Access denied";
|
|
2073
|
-
} else if (fs4.existsSync(filePath)) {
|
|
2074
|
-
const stat = fs4.statSync(filePath);
|
|
2075
|
-
if (stat.size > 0) {
|
|
2076
|
-
const fd = fs4.openSync(filePath, "r");
|
|
2077
|
-
const sample = Buffer.alloc(Math.min(4096, stat.size));
|
|
2078
|
-
fs4.readSync(fd, sample, 0, sample.length, 0);
|
|
2079
|
-
fs4.closeSync(fd);
|
|
2080
|
-
if (sample.includes(0)) {
|
|
2081
|
-
response.error = `Cannot read binary file: ${message.filePath}`;
|
|
2082
|
-
break;
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
const rawContent = fs4.readFileSync(filePath, "utf-8");
|
|
2086
|
-
const allLines = rawContent.split("\n");
|
|
2087
|
-
const offset = message.offset || 1;
|
|
2088
|
-
const limit = message.limit || 2e3;
|
|
2089
|
-
const startIdx = Math.max(0, offset - 1);
|
|
2090
|
-
const endIdx = Math.min(allLines.length, startIdx + limit);
|
|
2091
|
-
const sliced = allLines.slice(startIdx, endIdx);
|
|
2092
|
-
const numbered = sliced.map((line, i) => `${startIdx + i + 1}: ${line}`).join("\n");
|
|
2093
|
-
const truncated = endIdx < allLines.length;
|
|
2094
|
-
const _rtStat = fs4.statSync(filePath);
|
|
2095
|
-
const _readToken = `${Math.round(_rtStat.mtimeMs)}-${_rtStat.size}`;
|
|
2096
|
-
response.data = {
|
|
2097
|
-
content: numbered,
|
|
2098
|
-
// rawContent intentionally omitted — agents use line-numbered
|
|
2099
|
-
// content for edits; sending raw doubles context size per read.
|
|
2100
|
-
exists: true,
|
|
2101
|
-
path: message.filePath,
|
|
2102
|
-
totalLines: allLines.length,
|
|
2103
|
-
showing: { from: offset, to: endIdx, truncated },
|
|
2104
|
-
readToken: _readToken
|
|
2105
|
-
};
|
|
2106
|
-
} else {
|
|
2107
|
-
const dir = path4.dirname(filePath);
|
|
2108
|
-
const base = path4.basename(filePath);
|
|
2109
|
-
let suggestions = [];
|
|
2110
|
-
try {
|
|
2111
|
-
suggestions = fs4.readdirSync(dir).filter((e) => e.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(e.toLowerCase())).slice(0, 3).map((e) => path4.relative(projectPath, path4.join(dir, e)));
|
|
2112
|
-
} catch (_) {
|
|
2113
|
-
}
|
|
2114
|
-
response.data = { exists: false, suggestions };
|
|
2115
|
-
}
|
|
2116
|
-
} catch (e) {
|
|
2117
|
-
response.error = e.message;
|
|
2118
|
-
}
|
|
2119
|
-
break;
|
|
2120
|
-
case "GET_SOURCE":
|
|
2121
|
-
try {
|
|
2122
|
-
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2123
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
2124
|
-
response.error = "Access denied";
|
|
2125
|
-
} else if (fs4.existsSync(filePath)) {
|
|
2126
|
-
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2127
|
-
const lines = content.split("\n");
|
|
2128
|
-
const start = Math.max(0, (message.lineStart || 1) - 1);
|
|
2129
|
-
const end = message.lineEnd ? Math.min(lines.length, message.lineEnd) : lines.length;
|
|
2130
|
-
response.data = {
|
|
2131
|
-
lines: lines.slice(start, end),
|
|
2132
|
-
startLine: start + 1,
|
|
2133
|
-
endLine: end,
|
|
2134
|
-
totalLines: lines.length
|
|
2135
|
-
};
|
|
2136
|
-
} else {
|
|
2137
|
-
response.error = "File not found";
|
|
2138
|
-
}
|
|
2139
|
-
} catch (e) {
|
|
2140
|
-
response.error = e.message;
|
|
2141
|
-
}
|
|
2142
|
-
break;
|
|
2143
|
-
case "GET_ERROR_CONTEXT":
|
|
2144
|
-
try {
|
|
2145
|
-
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2146
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
2147
|
-
response.error = "Access denied";
|
|
2148
|
-
} else if (fs4.existsSync(filePath)) {
|
|
2149
|
-
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2150
|
-
const lines = content.split("\n");
|
|
2151
|
-
const targetLine = message.line || 1;
|
|
2152
|
-
const contextLines = message.contextLines || 5;
|
|
2153
|
-
const start = Math.max(0, targetLine - contextLines - 1);
|
|
2154
|
-
const end = Math.min(lines.length, targetLine + contextLines);
|
|
2155
|
-
response.data = {
|
|
2156
|
-
lines: lines.slice(start, end).map((line, i) => ({
|
|
2157
|
-
number: start + i + 1,
|
|
2158
|
-
content: line,
|
|
2159
|
-
isError: start + i + 1 === targetLine
|
|
2160
|
-
})),
|
|
2161
|
-
errorLine: targetLine,
|
|
2162
|
-
filePath: message.filePath
|
|
2163
|
-
};
|
|
2164
|
-
} else {
|
|
2165
|
-
response.error = "File not found";
|
|
2166
|
-
}
|
|
2167
|
-
} catch (e) {
|
|
2168
|
-
response.error = e.message;
|
|
2169
|
-
}
|
|
2170
|
-
break;
|
|
2171
|
-
case "GET_FILE_TREE":
|
|
2172
|
-
try {
|
|
2173
|
-
const depth = message.depth || 3;
|
|
2174
|
-
const tree = getFileTree(projectPath, depth);
|
|
2175
|
-
response.data = { tree };
|
|
2176
|
-
} catch (e) {
|
|
2177
|
-
response.error = e.message;
|
|
2178
|
-
}
|
|
2179
|
-
break;
|
|
2180
|
-
case "SEARCH_CODE":
|
|
2181
|
-
try {
|
|
2182
|
-
const { matches, engine } = await searchCode(
|
|
2183
|
-
projectPath,
|
|
2184
|
-
message.query,
|
|
2185
|
-
{
|
|
2186
|
-
isRegex: message.isRegex || false,
|
|
2187
|
-
caseSensitive: message.caseSensitive !== false,
|
|
2188
|
-
maxResults: message.maxResults || 20
|
|
2189
|
-
}
|
|
2190
|
-
);
|
|
2191
|
-
response.data = { matches, engine };
|
|
2192
|
-
} catch (e) {
|
|
2193
|
-
response.error = e.message;
|
|
2194
|
-
}
|
|
2195
|
-
break;
|
|
2196
|
-
case "FIND_FILES":
|
|
2197
|
-
try {
|
|
2198
|
-
let findFiles2 = function(dir) {
|
|
2199
|
-
if (found.length >= maxResults)
|
|
2200
|
-
return;
|
|
2201
|
-
try {
|
|
2202
|
-
const items = fs4.readdirSync(dir);
|
|
2203
|
-
for (const item of items) {
|
|
2204
|
-
if (found.length >= maxResults)
|
|
2205
|
-
break;
|
|
2206
|
-
if (ignorePatterns.includes(item) || item.startsWith("."))
|
|
2207
|
-
continue;
|
|
2208
|
-
const fullPath = path4.join(dir, item);
|
|
2209
|
-
try {
|
|
2210
|
-
const stat = fs4.statSync(fullPath);
|
|
2211
|
-
if (stat.isDirectory()) {
|
|
2212
|
-
findFiles2(fullPath);
|
|
2213
|
-
} else {
|
|
2214
|
-
const matchByName = !fileName || item === fileName || pattern.includes("*") && item.endsWith(fileName);
|
|
2215
|
-
if (matchByName) {
|
|
2216
|
-
found.push(path4.relative(projectPath, fullPath));
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
} catch (e) {
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
} catch (e) {
|
|
2223
|
-
}
|
|
2224
|
-
};
|
|
2225
|
-
var findFiles = findFiles2;
|
|
2226
|
-
const pattern = message.pattern || "";
|
|
2227
|
-
const fileName = pattern.split("/").pop().replace(/\*\*/g, "").replace(/\*/g, "");
|
|
2228
|
-
const maxResults = message.maxResults || 20;
|
|
2229
|
-
const ignorePatterns = ["node_modules", ".git", "dist", "build", ".next", ".cache"];
|
|
2230
|
-
const found = [];
|
|
2231
|
-
findFiles2(projectPath);
|
|
2232
|
-
response.data = found;
|
|
2233
|
-
} catch (e) {
|
|
2234
|
-
response.error = e.message;
|
|
2235
|
-
}
|
|
2236
|
-
case "DETECT_PROJECT":
|
|
2237
|
-
try {
|
|
2238
|
-
const pkgPath = path4.join(projectPath, "package.json");
|
|
2239
|
-
let deps = {};
|
|
2240
|
-
let devDeps = {};
|
|
2241
|
-
let pkgName = "";
|
|
2242
|
-
if (fs4.existsSync(pkgPath)) {
|
|
2243
|
-
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
2244
|
-
deps = pkg.dependencies || {};
|
|
2245
|
-
devDeps = pkg.devDependencies || {};
|
|
2246
|
-
pkgName = pkg.name || "";
|
|
2247
|
-
}
|
|
2248
|
-
const allDeps = { ...deps, ...devDeps };
|
|
2249
|
-
let framework = "html";
|
|
2250
|
-
if (allDeps["next"])
|
|
2251
|
-
framework = "next.js";
|
|
2252
|
-
else if (allDeps["nuxt"] || allDeps["nuxt3"])
|
|
2253
|
-
framework = "nuxt";
|
|
2254
|
-
else if (allDeps["gatsby"])
|
|
2255
|
-
framework = "gatsby";
|
|
2256
|
-
else if (allDeps["@remix-run/react"])
|
|
2257
|
-
framework = "remix";
|
|
2258
|
-
else if (allDeps["svelte"] || allDeps["@sveltejs/kit"])
|
|
2259
|
-
framework = "svelte";
|
|
2260
|
-
else if (allDeps["vue"])
|
|
2261
|
-
framework = "vue";
|
|
2262
|
-
else if (allDeps["@angular/core"])
|
|
2263
|
-
framework = "angular";
|
|
2264
|
-
else if (allDeps["react"])
|
|
2265
|
-
framework = "react";
|
|
2266
|
-
const typescript = !!allDeps["typescript"] || fs4.existsSync(path4.join(projectPath, "tsconfig.json"));
|
|
2267
|
-
const ext = typescript ? ".tsx" : ".jsx";
|
|
2268
|
-
let styling = "plain-css";
|
|
2269
|
-
if (allDeps["tailwindcss"])
|
|
2270
|
-
styling = "tailwind";
|
|
2271
|
-
else if (allDeps["styled-components"])
|
|
2272
|
-
styling = "styled-components";
|
|
2273
|
-
else if (allDeps["@emotion/react"] || allDeps["@emotion/styled"])
|
|
2274
|
-
styling = "emotion";
|
|
2275
|
-
else if (allDeps["sass"] || allDeps["node-sass"])
|
|
2276
|
-
styling = "sass";
|
|
2277
|
-
let router = null;
|
|
2278
|
-
if (framework === "next.js") {
|
|
2279
|
-
if (fs4.existsSync(path4.join(projectPath, "src/app")) || fs4.existsSync(path4.join(projectPath, "app"))) {
|
|
2280
|
-
router = "app";
|
|
2281
|
-
} else if (fs4.existsSync(path4.join(projectPath, "src/pages")) || fs4.existsSync(path4.join(projectPath, "pages"))) {
|
|
2282
|
-
router = "pages";
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
const findFirst = (candidates) => {
|
|
2286
|
-
for (const c of candidates) {
|
|
2287
|
-
if (fs4.existsSync(path4.join(projectPath, c)))
|
|
2288
|
-
return c;
|
|
2289
|
-
}
|
|
2290
|
-
return null;
|
|
2291
|
-
};
|
|
2292
|
-
const layoutFile = findFirst([
|
|
2293
|
-
"src/app/layout.tsx",
|
|
2294
|
-
"src/app/layout.jsx",
|
|
2295
|
-
"src/app/layout.js",
|
|
2296
|
-
"app/layout.tsx",
|
|
2297
|
-
"app/layout.jsx",
|
|
2298
|
-
"app/layout.js",
|
|
2299
|
-
"src/pages/_app.tsx",
|
|
2300
|
-
"src/pages/_app.jsx",
|
|
2301
|
-
"pages/_app.tsx",
|
|
2302
|
-
"pages/_app.jsx",
|
|
2303
|
-
"src/App.tsx",
|
|
2304
|
-
"src/App.jsx",
|
|
2305
|
-
"src/App.js",
|
|
2306
|
-
"src/main.tsx",
|
|
2307
|
-
"src/main.jsx",
|
|
2308
|
-
"src/main.js",
|
|
2309
|
-
"index.html"
|
|
2310
|
-
]);
|
|
2311
|
-
const globalStyleFile = findFirst([
|
|
2312
|
-
"src/app/globals.css",
|
|
2313
|
-
"src/app/global.css",
|
|
2314
|
-
"app/globals.css",
|
|
2315
|
-
"app/global.css",
|
|
2316
|
-
"src/index.css",
|
|
2317
|
-
"src/styles/globals.css",
|
|
2318
|
-
"src/styles/global.css",
|
|
2319
|
-
"src/App.css",
|
|
2320
|
-
"src/styles.css",
|
|
2321
|
-
"styles/globals.css",
|
|
2322
|
-
"styles/global.css",
|
|
2323
|
-
"css/style.css",
|
|
2324
|
-
"css/main.css",
|
|
2325
|
-
"style.css",
|
|
2326
|
-
"styles.css"
|
|
2327
|
-
]);
|
|
2328
|
-
let componentsDir = findFirst([
|
|
2329
|
-
"src/components",
|
|
2330
|
-
"src/app/components",
|
|
2331
|
-
"components",
|
|
2332
|
-
"src/ui",
|
|
2333
|
-
"src/app/ui"
|
|
2334
|
-
]);
|
|
2335
|
-
let tailwindConfig = null;
|
|
2336
|
-
let tailwindVersion = null;
|
|
2337
|
-
if (styling === "tailwind") {
|
|
2338
|
-
tailwindConfig = findFirst([
|
|
2339
|
-
"tailwind.config.ts",
|
|
2340
|
-
"tailwind.config.js",
|
|
2341
|
-
"tailwind.config.mjs",
|
|
2342
|
-
"tailwind.config.cjs"
|
|
2343
|
-
]);
|
|
2344
|
-
tailwindVersion = allDeps["tailwindcss"] || null;
|
|
2345
|
-
}
|
|
2346
|
-
let componentPattern = "arrow-function";
|
|
2347
|
-
let exportPattern = "default";
|
|
2348
|
-
let sampleComponentFile = null;
|
|
2349
|
-
let sampleComponentCode = null;
|
|
2350
|
-
if (componentsDir) {
|
|
2351
|
-
const compDir = path4.join(projectPath, componentsDir);
|
|
2352
|
-
try {
|
|
2353
|
-
const files = fs4.readdirSync(compDir).filter(
|
|
2354
|
-
(f) => f.endsWith(".tsx") || f.endsWith(".jsx") || f.endsWith(".js") || f.endsWith(".vue") || f.endsWith(".svelte")
|
|
2355
|
-
);
|
|
2356
|
-
if (files.length > 0) {
|
|
2357
|
-
sampleComponentFile = path4.join(componentsDir, files[0]);
|
|
2358
|
-
const code = fs4.readFileSync(path4.join(compDir, files[0]), "utf-8");
|
|
2359
|
-
sampleComponentCode = code.split("\n").slice(0, 80).join("\n");
|
|
2360
|
-
if (/^export default function /m.test(code)) {
|
|
2361
|
-
componentPattern = "function-declaration";
|
|
2362
|
-
exportPattern = "default";
|
|
2363
|
-
} else if (/^export function /m.test(code)) {
|
|
2364
|
-
componentPattern = "function-declaration";
|
|
2365
|
-
exportPattern = "named";
|
|
2366
|
-
} else if (/const \w+ = \(/.test(code) || /const \w+ = \(\) =>/.test(code)) {
|
|
2367
|
-
componentPattern = "arrow-function";
|
|
2368
|
-
}
|
|
2369
|
-
if (/^export default /m.test(code))
|
|
2370
|
-
exportPattern = "default";
|
|
2371
|
-
else if (/^export \{/m.test(code) || /^export const/m.test(code))
|
|
2372
|
-
exportPattern = "named";
|
|
2373
|
-
if (/import\s+\w+\s+from\s+['"][^'"]+\.module\.(css|scss|sass)['"]/.test(code)) {
|
|
2374
|
-
styling = "css-modules";
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
} catch (e) {
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
if (styling === "plain-css" && layoutFile) {
|
|
2381
|
-
try {
|
|
2382
|
-
const layoutCode = fs4.readFileSync(path4.join(projectPath, layoutFile), "utf-8");
|
|
2383
|
-
if (/import\s+\w+\s+from\s+['"][^'"]+\.module\.(css|scss|sass)['"]/.test(layoutCode)) {
|
|
2384
|
-
styling = "css-modules";
|
|
2385
|
-
}
|
|
2386
|
-
} catch (e) {
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
let globalStylePreview = null;
|
|
2390
|
-
if (globalStyleFile) {
|
|
2391
|
-
try {
|
|
2392
|
-
const css = fs4.readFileSync(path4.join(projectPath, globalStyleFile), "utf-8");
|
|
2393
|
-
globalStylePreview = css.split("\n").slice(0, 40).join("\n");
|
|
2394
|
-
} catch (e) {
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2398
|
-
"node_modules",
|
|
2399
|
-
".git",
|
|
2400
|
-
".next",
|
|
2401
|
-
".nuxt",
|
|
2402
|
-
".svelte-kit",
|
|
2403
|
-
"dist",
|
|
2404
|
-
"build",
|
|
2405
|
-
".cache",
|
|
2406
|
-
".turbo",
|
|
2407
|
-
"coverage",
|
|
2408
|
-
"__pycache__",
|
|
2409
|
-
".vercel",
|
|
2410
|
-
".output",
|
|
2411
|
-
".parcel-cache"
|
|
2412
|
-
]);
|
|
2413
|
-
const UI_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2414
|
-
".tsx",
|
|
2415
|
-
".jsx",
|
|
2416
|
-
".js",
|
|
2417
|
-
".ts",
|
|
2418
|
-
".vue",
|
|
2419
|
-
".svelte",
|
|
2420
|
-
".css",
|
|
2421
|
-
".scss",
|
|
2422
|
-
".sass",
|
|
2423
|
-
".less",
|
|
2424
|
-
".module.css",
|
|
2425
|
-
".module.scss",
|
|
2426
|
-
".html",
|
|
2427
|
-
".astro"
|
|
2428
|
-
]);
|
|
2429
|
-
const treeLines = [];
|
|
2430
|
-
const MAX_TREE_LINES = 120;
|
|
2431
|
-
const dirComponentCount = /* @__PURE__ */ new Map();
|
|
2432
|
-
const buildTree = (dir, prefix, depth) => {
|
|
2433
|
-
if (depth > 4 || treeLines.length >= MAX_TREE_LINES)
|
|
2434
|
-
return;
|
|
2435
|
-
try {
|
|
2436
|
-
const entries = fs4.readdirSync(path4.join(projectPath, dir), { withFileTypes: true });
|
|
2437
|
-
const sorted = entries.filter((e) => !e.name.startsWith(".") || e.name === ".env").sort((a, b) => {
|
|
2438
|
-
if (a.isDirectory() && !b.isDirectory())
|
|
2439
|
-
return -1;
|
|
2440
|
-
if (!a.isDirectory() && b.isDirectory())
|
|
2441
|
-
return 1;
|
|
2442
|
-
return a.name.localeCompare(b.name);
|
|
2443
|
-
});
|
|
2444
|
-
for (let i = 0; i < sorted.length && treeLines.length < MAX_TREE_LINES; i++) {
|
|
2445
|
-
const entry = sorted[i];
|
|
2446
|
-
const isLast = i === sorted.length - 1;
|
|
2447
|
-
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
2448
|
-
const childPrefix = isLast ? " " : "\u2502 ";
|
|
2449
|
-
const childPath = dir ? `${dir}/${entry.name}` : entry.name;
|
|
2450
|
-
if (entry.isDirectory()) {
|
|
2451
|
-
if (SKIP_DIRS.has(entry.name))
|
|
2452
|
-
continue;
|
|
2453
|
-
treeLines.push(`${prefix}${connector}${entry.name}/`);
|
|
2454
|
-
buildTree(childPath, prefix + childPrefix, depth + 1);
|
|
2455
|
-
} else {
|
|
2456
|
-
const ext2 = path4.extname(entry.name).toLowerCase();
|
|
2457
|
-
const COMPONENT_EXTS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".vue", ".svelte"]);
|
|
2458
|
-
if (COMPONENT_EXTS.has(ext2) && /^[A-Z]/.test(entry.name)) {
|
|
2459
|
-
dirComponentCount.set(dir, (dirComponentCount.get(dir) ?? 0) + 1);
|
|
2460
|
-
}
|
|
2461
|
-
if (UI_EXTENSIONS.has(ext2) || depth <= 1 && (entry.name === "package.json" || entry.name === "tsconfig.json" || entry.name.startsWith("tailwind.config") || entry.name.startsWith("next.config") || entry.name.startsWith("vite.config"))) {
|
|
2462
|
-
treeLines.push(`${prefix}${connector}${entry.name}`);
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
|
-
} catch (e) {
|
|
2467
|
-
}
|
|
2468
|
-
};
|
|
2469
|
-
const scanRoots = ["src", "app", "pages", "components", "styles", "public", "lib", "utils"];
|
|
2470
|
-
const existingRoots = [];
|
|
2471
|
-
for (const root of scanRoots) {
|
|
2472
|
-
if (fs4.existsSync(path4.join(projectPath, root))) {
|
|
2473
|
-
existingRoots.push(root);
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
try {
|
|
2477
|
-
const rootEntries = fs4.readdirSync(projectPath, { withFileTypes: true });
|
|
2478
|
-
for (const entry of rootEntries) {
|
|
2479
|
-
if (!entry.isDirectory() && (entry.name === "package.json" || entry.name === "tsconfig.json" || entry.name.startsWith("tailwind.config") || entry.name.startsWith("next.config") || entry.name.startsWith("vite.config") || entry.name === "index.html")) {
|
|
2480
|
-
treeLines.push(entry.name);
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
} catch (e) {
|
|
2484
|
-
}
|
|
2485
|
-
for (const root of existingRoots) {
|
|
2486
|
-
treeLines.push(`${root}/`);
|
|
2487
|
-
buildTree(root, "", 1);
|
|
2488
|
-
}
|
|
2489
|
-
const projectTree = treeLines.length > 0 ? treeLines.join("\n") : null;
|
|
2490
|
-
if (dirComponentCount.size > 0) {
|
|
2491
|
-
let bestDir = "";
|
|
2492
|
-
let bestCount = 0;
|
|
2493
|
-
for (const [dir, count] of dirComponentCount.entries()) {
|
|
2494
|
-
if (count > bestCount) {
|
|
2495
|
-
bestCount = count;
|
|
2496
|
-
bestDir = dir;
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
if (bestCount > 0)
|
|
2500
|
-
componentsDir = bestDir;
|
|
2501
|
-
}
|
|
2502
|
-
response.data = {
|
|
2503
|
-
framework,
|
|
2504
|
-
typescript,
|
|
2505
|
-
styling,
|
|
2506
|
-
router,
|
|
2507
|
-
ext,
|
|
2508
|
-
pkgName,
|
|
2509
|
-
// Key file locations
|
|
2510
|
-
layoutFile,
|
|
2511
|
-
globalStyleFile,
|
|
2512
|
-
componentsDir,
|
|
2513
|
-
tailwindConfig,
|
|
2514
|
-
tailwindVersion,
|
|
2515
|
-
// Code conventions
|
|
2516
|
-
componentPattern,
|
|
2517
|
-
exportPattern,
|
|
2518
|
-
sampleComponentFile,
|
|
2519
|
-
sampleComponentCode,
|
|
2520
|
-
// CSS context
|
|
2521
|
-
globalStylePreview,
|
|
2522
|
-
// Scaffolding tree
|
|
2523
|
-
projectTree
|
|
2524
|
-
};
|
|
2525
|
-
console.log(chalk2.blue("\u2139") + ` Project detected: ${chalk2.yellow(framework)} + ${chalk2.cyan(styling)}${typescript ? chalk2.dim(" (TS)") : ""}${router ? chalk2.dim(` [${router} router]`) : ""}`);
|
|
2526
|
-
} catch (e) {
|
|
2527
|
-
response.error = e.message;
|
|
2528
|
-
}
|
|
2529
|
-
break;
|
|
2530
|
-
case "BUILD_CLASS_INDEX":
|
|
2531
|
-
try {
|
|
2532
|
-
const COMPONENT_EXTS_IDX = /* @__PURE__ */ new Set([".tsx", ".jsx", ".vue", ".svelte", ".js", ".ts"]);
|
|
2533
|
-
const SKIP_DIRS_IDX = /* @__PURE__ */ new Set(["node_modules", ".git", ".next", ".nuxt", "dist", "build", ".cache"]);
|
|
2534
|
-
const index = {};
|
|
2535
|
-
let filesScanned = 0;
|
|
2536
|
-
let classesIndexed = 0;
|
|
2537
|
-
const scanDir = (dir) => {
|
|
2538
|
-
try {
|
|
2539
|
-
const entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
2540
|
-
for (const entry of entries) {
|
|
2541
|
-
if (entry.name.startsWith("."))
|
|
2542
|
-
continue;
|
|
2543
|
-
const fullPath = path4.join(dir, entry.name);
|
|
2544
|
-
const relPath = path4.relative(projectPath, fullPath).replace(/\\/g, "/");
|
|
2545
|
-
if (entry.isDirectory()) {
|
|
2546
|
-
if (SKIP_DIRS_IDX.has(entry.name))
|
|
2547
|
-
continue;
|
|
2548
|
-
scanDir(fullPath);
|
|
2549
|
-
} else {
|
|
2550
|
-
const ext = path4.extname(entry.name).toLowerCase();
|
|
2551
|
-
if (!COMPONENT_EXTS_IDX.has(ext))
|
|
2552
|
-
continue;
|
|
2553
|
-
try {
|
|
2554
|
-
const content = fs4.readFileSync(fullPath, "utf-8");
|
|
2555
|
-
const lines = content.split("\n");
|
|
2556
|
-
filesScanned++;
|
|
2557
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2558
|
-
const line = lines[i];
|
|
2559
|
-
const classMatches = line.matchAll(
|
|
2560
|
-
/(?:className|class)\s*=\s*(?:"([^"]+)"|'([^']+)'|\{(?:`([^`]+)`|"([^"]+)"|'([^']+)')\})/g
|
|
2561
|
-
);
|
|
2562
|
-
for (const match of classMatches) {
|
|
2563
|
-
const classValue = match[1] || match[2] || match[3] || match[4] || match[5];
|
|
2564
|
-
if (!classValue)
|
|
2565
|
-
continue;
|
|
2566
|
-
const classes = classValue.split(/\s+/).filter(
|
|
2567
|
-
(c) => c.length > 2 && !c.startsWith("$") && !c.includes("{") && !c.includes("}")
|
|
2568
|
-
);
|
|
2569
|
-
if (classes.length === 0)
|
|
2570
|
-
continue;
|
|
2571
|
-
const sig = classes.slice(0, 3).sort().join(" ");
|
|
2572
|
-
if (sig && !index[sig]) {
|
|
2573
|
-
index[sig] = { file: relPath, line: i + 1 };
|
|
2574
|
-
classesIndexed++;
|
|
2575
|
-
}
|
|
2576
|
-
for (const cls of classes) {
|
|
2577
|
-
if (/^(flex|grid|block|hidden|relative|absolute|fixed|sticky|inline|w-|h-|p-|m-|text-|bg-|border|rounded|flex-|grid-|col-|row-)/.test(cls))
|
|
2578
|
-
continue;
|
|
2579
|
-
if (cls.length > 5 && !index[cls]) {
|
|
2580
|
-
index[cls] = { file: relPath, line: i + 1 };
|
|
2581
|
-
classesIndexed++;
|
|
2582
|
-
}
|
|
2583
|
-
}
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
} catch (e) {
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
} catch (e) {
|
|
2591
|
-
}
|
|
2592
|
-
};
|
|
2593
|
-
scanDir(projectPath);
|
|
2594
|
-
response.data = {
|
|
2595
|
-
index,
|
|
2596
|
-
filesScanned,
|
|
2597
|
-
classesIndexed
|
|
2598
|
-
};
|
|
2599
|
-
console.log(chalk2.blue("\u2139") + ` Class index built: ${chalk2.yellow(classesIndexed)} classes across ${chalk2.cyan(filesScanned)} files`);
|
|
2600
|
-
} catch (e) {
|
|
2601
|
-
response.error = e.message;
|
|
2602
|
-
}
|
|
2603
|
-
break;
|
|
2604
|
-
case "WRITE_FILE":
|
|
2605
|
-
try {
|
|
2606
|
-
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2607
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
2608
|
-
response.error = "Access denied: Path outside project";
|
|
2609
|
-
} else {
|
|
2610
|
-
const prevContent = fs4.existsSync(filePath) ? fs4.readFileSync(filePath, "utf-8") : null;
|
|
2611
|
-
undoStack.push({ filePath, prevContent, operation: "WRITE_FILE", timestamp: Date.now(), relativePath: message.filePath });
|
|
2612
|
-
if (undoStack.length > 20)
|
|
2613
|
-
undoStack.shift();
|
|
2614
|
-
if (filePath.endsWith(".css")) {
|
|
2615
|
-
const writeContent = message.content || "";
|
|
2616
|
-
const opens = (writeContent.match(/\{/g) || []).length;
|
|
2617
|
-
const closes = (writeContent.match(/\}/g) || []).length;
|
|
2618
|
-
if (opens !== closes) {
|
|
2619
|
-
undoStack.pop();
|
|
2620
|
-
response.error = `CSS validation failed: unbalanced braces (${opens} opening vs ${closes} closing). Fix the CSS before writing.`;
|
|
2621
|
-
break;
|
|
2622
|
-
}
|
|
2623
|
-
}
|
|
2624
|
-
const dirPath = path4.dirname(filePath);
|
|
2625
|
-
if (!fs4.existsSync(dirPath)) {
|
|
2626
|
-
fs4.mkdirSync(dirPath, { recursive: true });
|
|
2627
|
-
}
|
|
2628
|
-
await withFileLock(filePath, async () => {
|
|
2629
|
-
fs4.writeFileSync(filePath, message.content, "utf-8");
|
|
2630
|
-
const formatter = await autoFormat(filePath, projectPath);
|
|
2631
|
-
response.data = { success: true, path: filePath, formatted: formatter, undoAvailable: true };
|
|
2632
|
-
console.log(chalk2.blue("\u2139") + ` Wrote file: ${message.filePath}` + (formatter ? chalk2.dim(` (formatted with ${formatter})`) : "") + chalk2.dim(" [undo saved]"));
|
|
2633
|
-
});
|
|
2634
|
-
const _wLsp = await checkDiagnostics(filePath, projectPath);
|
|
2635
|
-
if (_wLsp.ran && response.data) {
|
|
2636
|
-
response.data.lspDiagnostics = _wLsp.diagnostics;
|
|
2637
|
-
if (_wLsp.summary) {
|
|
2638
|
-
response.data.lspSummary = _wLsp.summary;
|
|
2639
|
-
if (_wLsp.diagnostics.some((d) => d.severity === "error")) {
|
|
2640
|
-
console.log(chalk2.yellow("\u26A0") + ` ${_wLsp.summary}`);
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
}
|
|
2644
|
-
}
|
|
2645
|
-
} catch (e) {
|
|
2646
|
-
response.error = e.message;
|
|
2647
|
-
}
|
|
2648
|
-
break;
|
|
2649
|
-
case "APPEND_FILE":
|
|
2650
|
-
try {
|
|
2651
|
-
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2652
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
2653
|
-
response.error = "Access denied: Path outside project";
|
|
2654
|
-
} else {
|
|
2655
|
-
const isCssFile = filePath.endsWith(".css");
|
|
2656
|
-
const newContent = message.content || "";
|
|
2657
|
-
if (isCssFile) {
|
|
2658
|
-
const opens = (newContent.match(/\{/g) || []).length;
|
|
2659
|
-
const closes = (newContent.match(/\}/g) || []).length;
|
|
2660
|
-
if (opens !== closes) {
|
|
2661
|
-
response.error = `CSS validation failed: unbalanced braces (${opens} opening vs ${closes} closing). Fix the CSS before appending.`;
|
|
2662
|
-
break;
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
const prevContent = fs4.existsSync(filePath) ? fs4.readFileSync(filePath, "utf-8") : null;
|
|
2666
|
-
undoStack.push({ filePath, prevContent, operation: "APPEND_FILE", timestamp: Date.now(), relativePath: message.filePath });
|
|
2667
|
-
if (undoStack.length > 20)
|
|
2668
|
-
undoStack.shift();
|
|
2669
|
-
if (isCssFile && prevContent !== null) {
|
|
2670
|
-
const importRegex = /^@(?:import|charset)\s+[^;]+;\s*$/gm;
|
|
2671
|
-
const newImports = [];
|
|
2672
|
-
const bodyContent = newContent.replace(importRegex, (match) => {
|
|
2673
|
-
newImports.push(match.trim());
|
|
2674
|
-
return "";
|
|
2675
|
-
}).trim();
|
|
2676
|
-
if (newImports.length > 0) {
|
|
2677
|
-
const existingLines = prevContent.split("\n");
|
|
2678
|
-
let lastImportLineIdx = -1;
|
|
2679
|
-
for (let i = 0; i < existingLines.length; i++) {
|
|
2680
|
-
const trimmed = existingLines[i].trim();
|
|
2681
|
-
if (trimmed.startsWith("@import") || trimmed.startsWith("@charset")) {
|
|
2682
|
-
lastImportLineIdx = i;
|
|
2683
|
-
}
|
|
2684
|
-
if (trimmed && !trimmed.startsWith("@import") && !trimmed.startsWith("@charset") && !trimmed.startsWith("/*") && !trimmed.startsWith("*") && !trimmed.startsWith("//")) {
|
|
2685
|
-
break;
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
2688
|
-
const dedupedImports = newImports.filter((imp) => !prevContent.includes(imp));
|
|
2689
|
-
const insertIdx = lastImportLineIdx + 1;
|
|
2690
|
-
const topLines = existingLines.slice(0, insertIdx);
|
|
2691
|
-
const restLines = existingLines.slice(insertIdx);
|
|
2692
|
-
const merged = [
|
|
2693
|
-
...topLines,
|
|
2694
|
-
...dedupedImports,
|
|
2695
|
-
...restLines,
|
|
2696
|
-
"",
|
|
2697
|
-
// separator
|
|
2698
|
-
bodyContent
|
|
2699
|
-
].join("\n");
|
|
2700
|
-
fs4.writeFileSync(filePath, merged, "utf-8");
|
|
2701
|
-
console.log(chalk2.blue("\u2139") + ` CSS-safe append: hoisted ${dedupedImports.length} @import(s) to top of ${message.filePath}`);
|
|
2702
|
-
} else {
|
|
2703
|
-
fs4.appendFileSync(filePath, "\n\n" + newContent, "utf-8");
|
|
2704
|
-
}
|
|
2705
|
-
} else {
|
|
2706
|
-
fs4.appendFileSync(filePath, "\n" + newContent, "utf-8");
|
|
2707
|
-
}
|
|
2708
|
-
const formatter = await autoFormat(filePath, projectPath);
|
|
2709
|
-
response.data = { success: true, path: filePath, formatted: formatter, undoAvailable: true };
|
|
2710
|
-
console.log(chalk2.blue("\u2139") + ` Appended file: ${message.filePath}` + (formatter ? chalk2.dim(` (formatted with ${formatter})`) : "") + chalk2.dim(" [undo saved]"));
|
|
2711
|
-
}
|
|
2712
|
-
} catch (e) {
|
|
2713
|
-
response.error = e.message;
|
|
2714
|
-
}
|
|
2715
|
-
break;
|
|
2716
|
-
case "EDIT_CLASSNAME": {
|
|
2717
|
-
try {
|
|
2718
|
-
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2719
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
2720
|
-
response.error = "Access denied: Path outside project";
|
|
2721
|
-
break;
|
|
2722
|
-
}
|
|
2723
|
-
if (!fs4.existsSync(filePath)) {
|
|
2724
|
-
response.error = "File not found: " + message.filePath;
|
|
2725
|
-
break;
|
|
2726
|
-
}
|
|
2727
|
-
if (message.readToken) {
|
|
2728
|
-
const stat = fs4.statSync(filePath);
|
|
2729
|
-
const token = `${Math.round(stat.mtimeMs)}-${stat.size}`;
|
|
2730
|
-
if (token !== message.readToken) {
|
|
2731
|
-
response.error = "File has changed since last read (readToken mismatch). Re-read with read_project_file before editing.";
|
|
2732
|
-
break;
|
|
2733
|
-
}
|
|
2734
|
-
}
|
|
2735
|
-
const source = fs4.readFileSync(filePath, "utf-8");
|
|
2736
|
-
undoStack.push({ filePath, prevContent: source, operation: "EDIT_CLASSNAME", timestamp: Date.now(), relativePath: message.filePath });
|
|
2737
|
-
if (undoStack.length > 20)
|
|
2738
|
-
undoStack.shift();
|
|
2739
|
-
const result = editJSXClassName(source, {
|
|
2740
|
-
oldValue: message.oldValue,
|
|
2741
|
-
newValue: message.newValue,
|
|
2742
|
-
lineHint: message.lineHint
|
|
2743
|
-
});
|
|
2744
|
-
if (!result.success) {
|
|
2745
|
-
undoStack.pop();
|
|
2746
|
-
response.error = result.error ?? "AST edit failed";
|
|
2747
|
-
break;
|
|
2748
|
-
}
|
|
2749
|
-
await withFileLock(filePath, async () => {
|
|
2750
|
-
fs4.writeFileSync(filePath, result.code, "utf-8");
|
|
2751
|
-
const formatter = await autoFormat(filePath, projectPath);
|
|
2752
|
-
response.data = {
|
|
2753
|
-
success: true,
|
|
2754
|
-
path: filePath,
|
|
2755
|
-
strategy: result.strategy,
|
|
2756
|
-
matchedLine: result.matchedLine,
|
|
2757
|
-
formatted: formatter,
|
|
2758
|
-
undoAvailable: true
|
|
2759
|
-
};
|
|
2760
|
-
console.log(
|
|
2761
|
-
chalk2.blue("\u2139") + ` EDIT_CLASSNAME: ${message.filePath}` + chalk2.dim(` [${result.strategy} @ line ${result.matchedLine}]`) + (formatter ? chalk2.dim(` (${formatter})`) : "") + chalk2.dim(" [undo saved]")
|
|
2762
|
-
);
|
|
2763
|
-
});
|
|
2764
|
-
const lsp = await checkDiagnostics(filePath, projectPath);
|
|
2765
|
-
if (lsp.ran && response.data) {
|
|
2766
|
-
response.data.lspDiagnostics = lsp.diagnostics;
|
|
2767
|
-
if (lsp.summary)
|
|
2768
|
-
response.data.lspSummary = lsp.summary;
|
|
2769
|
-
}
|
|
2770
|
-
} catch (e) {
|
|
2771
|
-
response.error = e.message;
|
|
2772
|
-
}
|
|
2773
|
-
break;
|
|
2774
|
-
}
|
|
2775
|
-
case "EDIT_FILE":
|
|
2776
|
-
try {
|
|
2777
|
-
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2778
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
2779
|
-
response.error = "Access denied: Path outside project";
|
|
2780
|
-
} else if (!fs4.existsSync(filePath)) {
|
|
2781
|
-
const requestedBase = path4.basename(message.filePath);
|
|
2782
|
-
const requestedExt = path4.extname(requestedBase);
|
|
2783
|
-
const requestedStem = requestedBase.replace(requestedExt, "");
|
|
2784
|
-
const suggestions = [];
|
|
2785
|
-
const dirsToSearch = ["src/app", "app", "src/pages", "pages", "src/components", "components", "src"];
|
|
2786
|
-
for (const dir of dirsToSearch) {
|
|
2787
|
-
const absDir = path4.join(projectPath, dir);
|
|
2788
|
-
if (fs4.existsSync(absDir)) {
|
|
2789
|
-
try {
|
|
2790
|
-
const files = fs4.readdirSync(absDir, { recursive: true });
|
|
2791
|
-
for (const f of files.slice(0, 100)) {
|
|
2792
|
-
const fBase = path4.basename(f);
|
|
2793
|
-
if (fBase.startsWith(requestedStem + ".") || fBase === requestedBase) {
|
|
2794
|
-
suggestions.push(path4.join(dir, f).replace(/\\/g, "/"));
|
|
2795
|
-
}
|
|
2796
|
-
}
|
|
2797
|
-
} catch (e) {
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
const hint = suggestions.length > 0 ? " Did you mean: " + suggestions.slice(0, 3).join(" or ") + "?" : " Double-check the path against detect_project() output (layoutFile, globalStyleFile) or projectTree.";
|
|
2802
|
-
response.error = 'File not found: "' + message.filePath + '".' + hint;
|
|
2803
|
-
} else {
|
|
2804
|
-
if (message.readToken) {
|
|
2805
|
-
const _eStat = fs4.statSync(filePath);
|
|
2806
|
-
const _eToken = `${Math.round(_eStat.mtimeMs)}-${_eStat.size}`;
|
|
2807
|
-
if (_eToken !== message.readToken) {
|
|
2808
|
-
response.error = "File has changed since last read (readToken mismatch). Re-read the file with read_project_file to get fresh content and line numbers before editing. The file was NOT modified.";
|
|
2809
|
-
break;
|
|
2810
|
-
}
|
|
2811
|
-
}
|
|
2812
|
-
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2813
|
-
undoStack.push({ filePath, prevContent: content, operation: "EDIT_FILE", timestamp: Date.now(), relativePath: message.filePath });
|
|
2814
|
-
if (undoStack.length > 20)
|
|
2815
|
-
undoStack.shift();
|
|
2816
|
-
const result = fuzzyReplace(content, message.target, message.replacement, message.replaceAll || false);
|
|
2817
|
-
if ("error" in result) {
|
|
2818
|
-
undoStack.pop();
|
|
2819
|
-
response.error = result.error;
|
|
2820
|
-
} else {
|
|
2821
|
-
const _jsExts = [".js", ".jsx", ".tsx", ".ts"];
|
|
2822
|
-
if (_jsExts.some((ext) => filePath.endsWith(ext))) {
|
|
2823
|
-
const _resultContent = result.result;
|
|
2824
|
-
const _strip = (s) => s.replace(/`[^`]*`/g, "``").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/\'(?:[^\'\\]|\\.)*\'/g, "''");
|
|
2825
|
-
const _origStripped = _strip(content);
|
|
2826
|
-
const _origDebt = (_origStripped.match(/\{/g) || []).length - (_origStripped.match(/\}/g) || []).length;
|
|
2827
|
-
const _origPDebt = (_origStripped.match(/\(/g) || []).length - (_origStripped.match(/\)/g) || []).length;
|
|
2828
|
-
const _stripped = _strip(_resultContent);
|
|
2829
|
-
const _bo = (_stripped.match(/\{/g) || []).length;
|
|
2830
|
-
const _bc = (_stripped.match(/\}/g) || []).length;
|
|
2831
|
-
const _po = (_stripped.match(/\(/g) || []).length;
|
|
2832
|
-
const _pc = (_stripped.match(/\)/g) || []).length;
|
|
2833
|
-
const _newDebt = _bo - _bc;
|
|
2834
|
-
const _newPDebt = _po - _pc;
|
|
2835
|
-
if (Math.abs(_newDebt) > Math.abs(_origDebt) || Math.abs(_newPDebt) > Math.abs(_origPDebt)) {
|
|
2836
|
-
undoStack.pop();
|
|
2837
|
-
response.error = `JSX validation failed: resulting file has unbalanced syntax (braces: ${_bo} open / ${_bc} close, parens: ${_po} open / ${_pc} close). The file was NOT modified. Re-read the file and fix the replacement so the overall file stays balanced.`;
|
|
2838
|
-
break;
|
|
2839
|
-
}
|
|
2840
|
-
}
|
|
2841
|
-
await withFileLock(filePath, async () => {
|
|
2842
|
-
fs4.writeFileSync(filePath, result.result, "utf-8");
|
|
2843
|
-
const formatter = await autoFormat(filePath, projectPath);
|
|
2844
|
-
response.data = {
|
|
2845
|
-
success: true,
|
|
2846
|
-
path: filePath,
|
|
2847
|
-
strategy: result.strategy,
|
|
2848
|
-
formatted: formatter,
|
|
2849
|
-
undoAvailable: true
|
|
2850
|
-
};
|
|
2851
|
-
const strategyLabel = result.strategy === "exact" ? "" : chalk2.dim(` [${result.strategy}]`);
|
|
2852
|
-
const formatLabel = formatter ? chalk2.dim(` (${formatter})`) : "";
|
|
2853
|
-
console.log(chalk2.blue("\u2139") + ` Edited file: ${message.filePath}${strategyLabel}${formatLabel}` + chalk2.dim(" [undo saved]"));
|
|
2854
|
-
});
|
|
2855
|
-
const _eLsp = await checkDiagnostics(filePath, projectPath);
|
|
2856
|
-
if (_eLsp.ran && response.data) {
|
|
2857
|
-
response.data.lspDiagnostics = _eLsp.diagnostics;
|
|
2858
|
-
if (_eLsp.summary) {
|
|
2859
|
-
response.data.lspSummary = _eLsp.summary;
|
|
2860
|
-
if (_eLsp.diagnostics.some((d) => d.severity === "error")) {
|
|
2861
|
-
console.log(chalk2.yellow("\u26A0") + ` ${_eLsp.summary}`);
|
|
2862
|
-
}
|
|
2863
|
-
}
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
} catch (e) {
|
|
2868
|
-
response.error = e.message;
|
|
2869
|
-
}
|
|
2870
|
-
break;
|
|
2871
|
-
case "REPLACE_LINES":
|
|
2872
|
-
try {
|
|
2873
|
-
const filePath = path4.resolve(projectPath, message.filePath);
|
|
2874
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
2875
|
-
response.error = "Access denied: Path outside project";
|
|
2876
|
-
} else if (!fs4.existsSync(filePath)) {
|
|
2877
|
-
response.error = "File not found: " + message.filePath;
|
|
2878
|
-
} else {
|
|
2879
|
-
if (message.readToken) {
|
|
2880
|
-
const _rStat = fs4.statSync(filePath);
|
|
2881
|
-
const _rToken = `${Math.round(_rStat.mtimeMs)}-${_rStat.size}`;
|
|
2882
|
-
if (_rToken !== message.readToken) {
|
|
2883
|
-
response.error = "File has changed since last read (readToken mismatch). Re-read the file with read_project_file to get fresh line numbers before replacing. The file was NOT modified.";
|
|
2884
|
-
break;
|
|
2885
|
-
}
|
|
2886
|
-
}
|
|
2887
|
-
const content = fs4.readFileSync(filePath, "utf-8");
|
|
2888
|
-
const lines = content.split("\n");
|
|
2889
|
-
const startLine = Math.max(1, message.startLine || 1);
|
|
2890
|
-
const endLine = Math.min(lines.length, message.endLine || startLine);
|
|
2891
|
-
if (startLine > lines.length) {
|
|
2892
|
-
response.error = `Start line ${startLine} is beyond file end (${lines.length} lines)`;
|
|
2893
|
-
} else if (startLine > endLine) {
|
|
2894
|
-
response.error = `Invalid range: start (${startLine}) > end (${endLine})`;
|
|
2895
|
-
} else {
|
|
2896
|
-
undoStack.push({
|
|
2897
|
-
filePath,
|
|
2898
|
-
prevContent: content,
|
|
2899
|
-
operation: "REPLACE_LINES",
|
|
2900
|
-
timestamp: Date.now(),
|
|
2901
|
-
relativePath: message.filePath
|
|
2902
|
-
});
|
|
2903
|
-
if (undoStack.length > 20)
|
|
2904
|
-
undoStack.shift();
|
|
2905
|
-
const oldLineCount = endLine - startLine + 1;
|
|
2906
|
-
const newLines = (message.newContent || "").split("\n");
|
|
2907
|
-
const _jsExts2 = [".js", ".jsx", ".tsx", ".ts"];
|
|
2908
|
-
if (_jsExts2.some((ext) => filePath.endsWith(ext))) {
|
|
2909
|
-
const _strip = (s) => s.replace(/`[^`]*`/g, "``").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/\'(?:[^\'\\]|\\.)*\'/g, "''");
|
|
2910
|
-
const _origStripped2 = _strip(content);
|
|
2911
|
-
const _origDebt2 = (_origStripped2.match(/\{/g) || []).length - (_origStripped2.match(/\}/g) || []).length;
|
|
2912
|
-
const _origPDebt2 = (_origStripped2.match(/\(/g) || []).length - (_origStripped2.match(/\)/g) || []).length;
|
|
2913
|
-
const _previewLines = [...lines];
|
|
2914
|
-
const _previewNew = (message.newContent || "").split("\n");
|
|
2915
|
-
_previewLines.splice(startLine - 1, endLine - startLine + 1, ..._previewNew);
|
|
2916
|
-
const _stripped = _strip(_previewLines.join("\n"));
|
|
2917
|
-
const _bo = (_stripped.match(/\{/g) || []).length;
|
|
2918
|
-
const _bc = (_stripped.match(/\}/g) || []).length;
|
|
2919
|
-
const _po = (_stripped.match(/\(/g) || []).length;
|
|
2920
|
-
const _pc = (_stripped.match(/\)/g) || []).length;
|
|
2921
|
-
const _newDebt2 = _bo - _bc;
|
|
2922
|
-
const _newPDebt2 = _po - _pc;
|
|
2923
|
-
if (Math.abs(_newDebt2) > Math.abs(_origDebt2) || Math.abs(_newPDebt2) > Math.abs(_origPDebt2)) {
|
|
2924
|
-
undoStack.pop();
|
|
2925
|
-
const _deletedBlock = lines.slice(startLine - 1, endLine).join("\n");
|
|
2926
|
-
const _ds = _strip(_deletedBlock);
|
|
2927
|
-
const _braceDebt = (_ds.match(/\{/g) || []).length - (_ds.match(/\}/g) || []).length;
|
|
2928
|
-
const _parenDebt = (_ds.match(/\(/g) || []).length - (_ds.match(/\)/g) || []).length;
|
|
2929
|
-
let _hint = "";
|
|
2930
|
-
if (_braceDebt > 0 || _parenDebt > 0) {
|
|
2931
|
-
let _bd = _braceDebt;
|
|
2932
|
-
let _pd = _parenDebt;
|
|
2933
|
-
let _sugEnd = endLine;
|
|
2934
|
-
const _tail = lines.slice(endLine);
|
|
2935
|
-
for (let _i = 0; _i < _tail.length; _i++) {
|
|
2936
|
-
const _ls = _strip(_tail[_i]);
|
|
2937
|
-
_bd -= (_ls.match(/\}/g) || []).length - (_ls.match(/\{/g) || []).length;
|
|
2938
|
-
_pd -= (_ls.match(/\)/g) || []).length - (_ls.match(/\(/g) || []).length;
|
|
2939
|
-
_sugEnd = endLine + _i + 1;
|
|
2940
|
-
if (_bd <= 0 && _pd <= 0)
|
|
2941
|
-
break;
|
|
2942
|
-
}
|
|
2943
|
-
_hint = ` The range ${startLine}-${endLine} has unclosed syntax. Expand end_line to ~${_sugEnd} to include the closing syntax, then retry.`;
|
|
2944
|
-
} else {
|
|
2945
|
-
_hint = " Re-read the file and ensure the replacement keeps all JSX balanced.";
|
|
2946
|
-
}
|
|
2947
|
-
response.error = `JSX validation failed: resulting file has unbalanced syntax (braces: ${_bo} open / ${_bc} close, parens: ${_po} open / ${_pc} close). The file was NOT modified.${_hint}`;
|
|
2948
|
-
break;
|
|
2949
|
-
}
|
|
2950
|
-
}
|
|
2951
|
-
await withFileLock(filePath, async () => {
|
|
2952
|
-
lines.splice(startLine - 1, oldLineCount, ...newLines);
|
|
2953
|
-
const newContent = lines.join("\n");
|
|
2954
|
-
fs4.writeFileSync(filePath, newContent, "utf-8");
|
|
2955
|
-
const formatter = await autoFormat(filePath, projectPath);
|
|
2956
|
-
const lineDelta = newLines.length - oldLineCount;
|
|
2957
|
-
response.data = {
|
|
2958
|
-
success: true,
|
|
2959
|
-
path: filePath,
|
|
2960
|
-
linesReplaced: `${startLine}-${endLine}`,
|
|
2961
|
-
oldLineCount,
|
|
2962
|
-
newLineCount: newLines.length,
|
|
2963
|
-
lineDelta,
|
|
2964
|
-
totalLines: lines.length,
|
|
2965
|
-
formatted: formatter,
|
|
2966
|
-
undoAvailable: true,
|
|
2967
|
-
hint: lineDelta !== 0 ? `Line count changed by ${lineDelta > 0 ? "+" : ""}${lineDelta}. Adjust subsequent line numbers if making more edits.` : null
|
|
2968
|
-
};
|
|
2969
|
-
const formatLabel = formatter ? chalk2.dim(` (${formatter})`) : "";
|
|
2970
|
-
console.log(chalk2.blue("\u2139") + ` Replaced lines ${startLine}-${endLine} in ${message.filePath} (${oldLineCount}\u2192${newLines.length} lines)${formatLabel}` + chalk2.dim(" [undo saved]"));
|
|
2971
|
-
});
|
|
2972
|
-
const _rLsp = await checkDiagnostics(filePath, projectPath);
|
|
2973
|
-
if (_rLsp.ran && response.data) {
|
|
2974
|
-
response.data.lspDiagnostics = _rLsp.diagnostics;
|
|
2975
|
-
if (_rLsp.summary) {
|
|
2976
|
-
response.data.lspSummary = _rLsp.summary;
|
|
2977
|
-
if (_rLsp.diagnostics.some((d) => d.severity === "error")) {
|
|
2978
|
-
console.log(chalk2.yellow("\u26A0") + ` ${_rLsp.summary}`);
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
|
-
}
|
|
2982
|
-
}
|
|
2983
|
-
}
|
|
2984
|
-
} catch (e) {
|
|
2985
|
-
response.error = e.message;
|
|
2986
|
-
}
|
|
2987
|
-
break;
|
|
2988
|
-
case "UNDO_LAST":
|
|
2989
|
-
try {
|
|
2990
|
-
if (undoStack.length === 0) {
|
|
2991
|
-
response.error = "Nothing to undo \u2014 no file changes recorded in this session.";
|
|
2992
|
-
} else {
|
|
2993
|
-
const snapshot = undoStack.pop();
|
|
2994
|
-
if (snapshot.prevContent === null) {
|
|
2995
|
-
if (fs4.existsSync(snapshot.filePath)) {
|
|
2996
|
-
fs4.unlinkSync(snapshot.filePath);
|
|
2997
|
-
response.data = {
|
|
2998
|
-
success: true,
|
|
2999
|
-
undone: snapshot.operation,
|
|
3000
|
-
file: snapshot.relativePath,
|
|
3001
|
-
action: "deleted (was new file)",
|
|
3002
|
-
remaining: undoStack.length
|
|
3003
|
-
};
|
|
3004
|
-
console.log(chalk2.yellow("\u21A9") + ` Undo: deleted ${snapshot.relativePath} (was a new file)`);
|
|
3005
|
-
} else {
|
|
3006
|
-
response.data = {
|
|
3007
|
-
success: true,
|
|
3008
|
-
undone: snapshot.operation,
|
|
3009
|
-
file: snapshot.relativePath,
|
|
3010
|
-
action: "already gone",
|
|
3011
|
-
remaining: undoStack.length
|
|
3012
|
-
};
|
|
3013
|
-
}
|
|
3014
|
-
} else {
|
|
3015
|
-
fs4.writeFileSync(snapshot.filePath, snapshot.prevContent, "utf-8");
|
|
3016
|
-
response.data = {
|
|
3017
|
-
success: true,
|
|
3018
|
-
undone: snapshot.operation,
|
|
3019
|
-
file: snapshot.relativePath,
|
|
3020
|
-
action: "restored",
|
|
3021
|
-
remaining: undoStack.length
|
|
3022
|
-
};
|
|
3023
|
-
console.log(chalk2.yellow("\u21A9") + ` Undo: restored ${snapshot.relativePath} (reverted ${snapshot.operation})`);
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
3026
|
-
} catch (e) {
|
|
3027
|
-
response.error = e.message;
|
|
3028
|
-
}
|
|
3029
|
-
break;
|
|
3030
|
-
case "DELETE_FILE":
|
|
3031
|
-
try {
|
|
3032
|
-
const filePath = path4.resolve(projectPath, message.filePath);
|
|
3033
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
3034
|
-
response.error = "Access denied: Path outside project";
|
|
3035
|
-
break;
|
|
3036
|
-
}
|
|
3037
|
-
if (!fs4.existsSync(filePath)) {
|
|
3038
|
-
response.error = "File not found: " + message.filePath;
|
|
3039
|
-
break;
|
|
3040
|
-
}
|
|
3041
|
-
const relPath = message.filePath.replace(/\\/g, "/");
|
|
3042
|
-
const baseName = path4.basename(relPath);
|
|
3043
|
-
const PROTECTED_PATTERNS = [
|
|
3044
|
-
// Package & lock files
|
|
3045
|
-
"package.json",
|
|
3046
|
-
"package-lock.json",
|
|
3047
|
-
"yarn.lock",
|
|
3048
|
-
"pnpm-lock.yaml",
|
|
3049
|
-
"bun.lockb",
|
|
3050
|
-
"bun.lock",
|
|
3051
|
-
// TypeScript / JS config
|
|
3052
|
-
"tsconfig.json",
|
|
3053
|
-
"tsconfig.node.json",
|
|
3054
|
-
"jsconfig.json",
|
|
3055
|
-
// Framework configs
|
|
3056
|
-
/^next\.config\./,
|
|
3057
|
-
/^vite\.config\./,
|
|
3058
|
-
/^nuxt\.config\./,
|
|
3059
|
-
/^svelte\.config\./,
|
|
3060
|
-
/^remix\.config\./,
|
|
3061
|
-
/^astro\.config\./,
|
|
3062
|
-
/^webpack\.config\./,
|
|
3063
|
-
/^babel\.config\./,
|
|
3064
|
-
/^tailwind\.config\./,
|
|
3065
|
-
/^postcss\.config\./,
|
|
3066
|
-
/^prettier\.config\./,
|
|
3067
|
-
/^eslint\.config\./,
|
|
3068
|
-
".eslintrc",
|
|
3069
|
-
".prettierrc",
|
|
3070
|
-
".babelrc",
|
|
3071
|
-
// Env files
|
|
3072
|
-
".env",
|
|
3073
|
-
".env.local",
|
|
3074
|
-
".env.production",
|
|
3075
|
-
".env.development",
|
|
3076
|
-
".env.staging",
|
|
3077
|
-
// Next.js/React layout & entry roots
|
|
3078
|
-
/^layout\.(tsx|jsx|js|ts)$/,
|
|
3079
|
-
/^_app\.(tsx|jsx|js|ts)$/,
|
|
3080
|
-
/^_document\.(tsx|jsx|js|ts)$/,
|
|
3081
|
-
/^App\.(tsx|jsx|js|ts)$/,
|
|
3082
|
-
/^main\.(tsx|jsx|js|ts)$/,
|
|
3083
|
-
// Dockerfile & CI
|
|
3084
|
-
"Dockerfile",
|
|
3085
|
-
"docker-compose.yml",
|
|
3086
|
-
"docker-compose.yaml",
|
|
3087
|
-
".gitignore",
|
|
3088
|
-
".gitattributes"
|
|
3089
|
-
];
|
|
3090
|
-
const isProtected = PROTECTED_PATTERNS.some(
|
|
3091
|
-
(p) => typeof p === "string" ? baseName === p || relPath.endsWith("/" + p) : p.test(baseName)
|
|
3092
|
-
);
|
|
3093
|
-
if (isProtected) {
|
|
3094
|
-
response.error = `\u{1F6E1} Protected file: "${message.filePath}" cannot be deleted by the agent. This file is structurally critical to the project. To resolve conflicts involving this file, edit its contents instead of deleting it.`;
|
|
3095
|
-
break;
|
|
3096
|
-
}
|
|
3097
|
-
const trashDir = path4.join(projectPath, ".trace-trash");
|
|
3098
|
-
if (!fs4.existsSync(trashDir)) {
|
|
3099
|
-
fs4.mkdirSync(trashDir, { recursive: true });
|
|
3100
|
-
const gitignorePath = path4.join(projectPath, ".gitignore");
|
|
3101
|
-
if (fs4.existsSync(gitignorePath)) {
|
|
3102
|
-
const gi = fs4.readFileSync(gitignorePath, "utf-8");
|
|
3103
|
-
if (!gi.includes(".trace-trash")) {
|
|
3104
|
-
fs4.appendFileSync(gitignorePath, "\n# Trace soft-delete recovery folder\n.trace-trash/\n");
|
|
3105
|
-
}
|
|
3106
|
-
}
|
|
3107
|
-
}
|
|
3108
|
-
const timestamp = Date.now();
|
|
3109
|
-
const trashName = `${timestamp}_${baseName}`;
|
|
3110
|
-
const trashPath = path4.join(trashDir, trashName);
|
|
3111
|
-
const prevContent = fs4.readFileSync(filePath, "utf-8");
|
|
3112
|
-
undoStack.push({ filePath, prevContent, operation: "DELETE_FILE", timestamp, relativePath: message.filePath });
|
|
3113
|
-
if (undoStack.length > 20)
|
|
3114
|
-
undoStack.shift();
|
|
3115
|
-
fs4.renameSync(filePath, trashPath);
|
|
3116
|
-
response.data = {
|
|
3117
|
-
success: true,
|
|
3118
|
-
deleted: message.filePath,
|
|
3119
|
-
recoveryPath: path4.relative(projectPath, trashPath),
|
|
3120
|
-
undoAvailable: true,
|
|
3121
|
-
hint: "File moved to .trace-trash/ \u2014 recoverable even after CLI restart. Call UNDO_LAST to restore to original path."
|
|
3122
|
-
};
|
|
3123
|
-
console.log(chalk2.yellow("\u{1F5D1}") + ` Soft-deleted: ${message.filePath} \u2192 .trace-trash/${trashName}` + chalk2.dim(" [recoverable]"));
|
|
3124
|
-
} catch (e) {
|
|
3125
|
-
response.error = e.message;
|
|
3126
|
-
}
|
|
3127
|
-
break;
|
|
3128
|
-
case "RENAME_FILE":
|
|
3129
|
-
try {
|
|
3130
|
-
const oldPath = path4.resolve(projectPath, message.oldPath);
|
|
3131
|
-
const newPath = path4.resolve(projectPath, message.newPath);
|
|
3132
|
-
if (!_isInsideProject(projectPath, oldPath) || !_isInsideProject(projectPath, newPath)) {
|
|
3133
|
-
response.error = "Access denied: Path outside project";
|
|
3134
|
-
} else if (!fs4.existsSync(oldPath)) {
|
|
3135
|
-
response.error = "File not found: " + message.oldPath;
|
|
3136
|
-
} else if (fs4.existsSync(newPath)) {
|
|
3137
|
-
response.error = "Target already exists: " + message.newPath + ". Delete it first or choose a different name.";
|
|
3138
|
-
} else {
|
|
3139
|
-
const prevContent = fs4.readFileSync(oldPath, "utf-8");
|
|
3140
|
-
undoStack.push({ filePath: oldPath, prevContent, operation: "RENAME_FILE", timestamp: Date.now(), relativePath: message.oldPath });
|
|
3141
|
-
if (undoStack.length > 20)
|
|
3142
|
-
undoStack.shift();
|
|
3143
|
-
const destDir = path4.dirname(newPath);
|
|
3144
|
-
if (!fs4.existsSync(destDir))
|
|
3145
|
-
fs4.mkdirSync(destDir, { recursive: true });
|
|
3146
|
-
fs4.renameSync(oldPath, newPath);
|
|
3147
|
-
response.data = {
|
|
3148
|
-
success: true,
|
|
3149
|
-
oldPath: message.oldPath,
|
|
3150
|
-
newPath: message.newPath,
|
|
3151
|
-
undoAvailable: true
|
|
3152
|
-
};
|
|
3153
|
-
console.log(chalk2.blue("\u2139") + ` Renamed: ${message.oldPath} \u2192 ${message.newPath}` + chalk2.dim(" [undo saved]"));
|
|
3154
|
-
}
|
|
3155
|
-
} catch (e) {
|
|
3156
|
-
response.error = e.message;
|
|
3157
|
-
}
|
|
3158
|
-
break;
|
|
3159
|
-
case "GIT_BLAME":
|
|
3160
|
-
try {
|
|
3161
|
-
const filePath = path4.resolve(projectPath, message.filePath || "");
|
|
3162
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
3163
|
-
response.error = "Access denied: Path outside project";
|
|
3164
|
-
break;
|
|
3165
|
-
}
|
|
3166
|
-
const lineNum = parseInt(message.line, 10);
|
|
3167
|
-
if (!Number.isFinite(lineNum) || lineNum < 1) {
|
|
3168
|
-
response.error = "Invalid line number";
|
|
3169
|
-
break;
|
|
3170
|
-
}
|
|
3171
|
-
const { stdout } = await execFileAsync3("git", [
|
|
3172
|
-
"blame",
|
|
3173
|
-
"-L",
|
|
3174
|
-
`${lineNum},${lineNum}`,
|
|
3175
|
-
"--porcelain",
|
|
3176
|
-
filePath
|
|
3177
|
-
], { cwd: projectPath });
|
|
3178
|
-
const lines = stdout.split("\n");
|
|
3179
|
-
const commitHash = lines[0].split(" ")[0];
|
|
3180
|
-
const author = lines.find((l) => l.startsWith("author "))?.substring(7);
|
|
3181
|
-
const email = lines.find((l) => l.startsWith("author-mail "))?.substring(12);
|
|
3182
|
-
const date = lines.find((l) => l.startsWith("author-time "))?.substring(12);
|
|
3183
|
-
const summary = lines.find((l) => l.startsWith("summary "))?.substring(8);
|
|
3184
|
-
response.data = {
|
|
3185
|
-
commit: commitHash,
|
|
3186
|
-
author,
|
|
3187
|
-
email,
|
|
3188
|
-
date: new Date(parseInt(date) * 1e3).toISOString().split("T")[0],
|
|
3189
|
-
message: summary
|
|
3190
|
-
};
|
|
3191
|
-
} catch (e) {
|
|
3192
|
-
response.error = `Git blame failed: ${e.message}`;
|
|
3193
|
-
}
|
|
3194
|
-
break;
|
|
3195
|
-
case "GIT_RECENT_CHANGES":
|
|
3196
|
-
try {
|
|
3197
|
-
const filePath = path4.resolve(projectPath, message.filePath || "");
|
|
3198
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
3199
|
-
response.error = "Access denied: Path outside project";
|
|
3200
|
-
break;
|
|
3201
|
-
}
|
|
3202
|
-
const days = Number.isFinite(message.days) && message.days > 0 ? Math.floor(message.days) : 7;
|
|
3203
|
-
const { stdout } = await execFileAsync3("git", [
|
|
3204
|
-
"log",
|
|
3205
|
-
"-n",
|
|
3206
|
-
"10",
|
|
3207
|
-
"--since",
|
|
3208
|
-
`${days} days ago`,
|
|
3209
|
-
"--pretty=format:%h|%an|%ad|%s",
|
|
3210
|
-
"--date=short",
|
|
3211
|
-
"--",
|
|
3212
|
-
filePath
|
|
3213
|
-
], { cwd: projectPath });
|
|
3214
|
-
response.data = {
|
|
3215
|
-
history: stdout.split("\n").filter(Boolean).map((line) => {
|
|
3216
|
-
const [hash, author, date, message2] = line.split("|");
|
|
3217
|
-
return { hash, author, date, message: message2 };
|
|
3218
|
-
})
|
|
3219
|
-
};
|
|
3220
|
-
} catch (e) {
|
|
3221
|
-
response.error = `Git log failed: ${e.message}`;
|
|
3222
|
-
}
|
|
3223
|
-
break;
|
|
3224
|
-
case "GET_IMPORTS":
|
|
3225
|
-
try {
|
|
3226
|
-
const filePath = path4.resolve(projectPath, message.filePath || "");
|
|
3227
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
3228
|
-
response.error = "Access denied: Path outside project";
|
|
3229
|
-
} else if (fs4.existsSync(filePath)) {
|
|
3230
|
-
const content = fs4.readFileSync(filePath, "utf-8");
|
|
3231
|
-
const importRegex = /import\s+(?:[\w*\s{},]*)\s+from\s+['"]([^'"]+)['"]/g;
|
|
3232
|
-
const imports = [];
|
|
3233
|
-
let match;
|
|
3234
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
3235
|
-
imports.push(match[1]);
|
|
3236
|
-
}
|
|
3237
|
-
response.data = { imports };
|
|
3238
|
-
} else {
|
|
3239
|
-
response.error = "File not found";
|
|
3240
|
-
}
|
|
3241
|
-
} catch (e) {
|
|
3242
|
-
response.error = e.message;
|
|
3243
|
-
}
|
|
3244
|
-
break;
|
|
3245
|
-
case "FIND_USAGES":
|
|
3246
|
-
try {
|
|
3247
|
-
const query = typeof message.query === "string" ? message.query : "";
|
|
3248
|
-
if (!query) {
|
|
3249
|
-
response.error = "Empty query";
|
|
3250
|
-
break;
|
|
3251
|
-
}
|
|
3252
|
-
const { stdout } = await execFileAsync3("git", [
|
|
3253
|
-
"grep",
|
|
3254
|
-
"-n",
|
|
3255
|
-
"--",
|
|
3256
|
-
query
|
|
3257
|
-
], { cwd: projectPath });
|
|
3258
|
-
response.data = {
|
|
3259
|
-
usages: stdout.split("\n").filter(Boolean).slice(0, 20).map((line) => {
|
|
3260
|
-
const parts = line.split(":");
|
|
3261
|
-
return {
|
|
3262
|
-
file: parts[0],
|
|
3263
|
-
line: parseInt(parts[1]),
|
|
3264
|
-
content: parts.slice(2).join(":").trim()
|
|
3265
|
-
};
|
|
3266
|
-
})
|
|
3267
|
-
};
|
|
3268
|
-
} catch (e) {
|
|
3269
|
-
if (e.code === 1)
|
|
3270
|
-
response.data = { usages: [] };
|
|
3271
|
-
else
|
|
3272
|
-
response.error = `Grep failed: ${e.message}`;
|
|
3273
|
-
}
|
|
3274
|
-
break;
|
|
3275
|
-
case "GET_ENV_VARS":
|
|
3276
|
-
try {
|
|
3277
|
-
const filePath = path4.resolve(projectPath, message.filePath || "");
|
|
3278
|
-
if (!_isInsideProject(projectPath, filePath)) {
|
|
3279
|
-
response.error = "Access denied: Path outside project";
|
|
3280
|
-
} else if (fs4.existsSync(filePath)) {
|
|
3281
|
-
const content = fs4.readFileSync(filePath, "utf-8");
|
|
3282
|
-
const envRegex = /(?:process\.env\.|import\.meta\.env\.)([A-Z_][A-Z0-9_]*)/g;
|
|
3283
|
-
const vars = /* @__PURE__ */ new Set();
|
|
3284
|
-
let match;
|
|
3285
|
-
while ((match = envRegex.exec(content)) !== null) {
|
|
3286
|
-
vars.add(match[1]);
|
|
3287
|
-
}
|
|
3288
|
-
response.data = { envVars: Array.from(vars) };
|
|
3289
|
-
} else {
|
|
3290
|
-
response.error = "File not found";
|
|
3291
|
-
}
|
|
3292
|
-
} catch (e) {
|
|
3293
|
-
response.error = e.message;
|
|
3294
|
-
}
|
|
3295
|
-
break;
|
|
3296
|
-
case "GET_TERMINAL_BUFFER": {
|
|
3297
|
-
const lines = parseInt(message.lines) || 100;
|
|
3298
|
-
const buffer = globalTerminalBuffer.getLast(lines);
|
|
3299
|
-
response.data = {
|
|
3300
|
-
lines: buffer,
|
|
3301
|
-
totalLines: globalTerminalBuffer.length,
|
|
3302
|
-
hasDevProcess: devProcess !== null && !devProcess.killed
|
|
3303
|
-
};
|
|
3304
|
-
break;
|
|
3305
|
-
}
|
|
3306
|
-
case "GET_DEV_STATUS": {
|
|
3307
|
-
response.data = {
|
|
3308
|
-
running: devProcess !== null && !devProcess.killed,
|
|
3309
|
-
pid: devProcess?.pid ?? null
|
|
3310
|
-
};
|
|
3311
|
-
break;
|
|
3312
|
-
}
|
|
3313
|
-
default:
|
|
3314
|
-
response.error = `Unknown message type: ${type}`;
|
|
3315
|
-
}
|
|
3316
|
-
ws.send(JSON.stringify(response));
|
|
3317
|
-
} catch (e) {
|
|
3318
|
-
console.error(chalk2.red("Parse error:"), e.message);
|
|
3319
|
-
}
|
|
3320
|
-
});
|
|
3321
|
-
ws.on("message", (rawData) => {
|
|
3322
|
-
try {
|
|
3323
|
-
const msg = JSON.parse(rawData.toString());
|
|
3324
|
-
if (msg.type === "RESPONSE" && msg.id && globalBrowserPending.has(msg.id)) {
|
|
3325
|
-
const { resolve: resolve3 } = globalBrowserPending.get(msg.id);
|
|
3326
|
-
globalBrowserPending.delete(msg.id);
|
|
3327
|
-
resolve3(msg.error ? { error: msg.error } : msg.data ?? {});
|
|
3328
|
-
} else if (msg.type === "AGENT_PROGRESS" && msg.id && globalBrowserPending.has(msg.id)) {
|
|
3329
|
-
const pending = globalBrowserPending.get(msg.id);
|
|
3330
|
-
if (pending.onProgress) {
|
|
3331
|
-
try {
|
|
3332
|
-
pending.onProgress(msg.event);
|
|
3333
|
-
} catch {
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
}
|
|
3337
|
-
} catch {
|
|
3338
|
-
}
|
|
3339
|
-
});
|
|
3340
|
-
}
|
|
3341
|
-
program.command("connect", { isDefault: true }).alias("c").description('Start IDE bridge only (use "trace dev" to also start your dev server)').option("-p, --port <port>", "WebSocket port", "8765").action(async (options) => {
|
|
3342
|
-
const port = parseInt(options.port);
|
|
3343
|
-
const projectPath = process.cwd();
|
|
3344
|
-
console.log();
|
|
3345
|
-
console.log(chalk2.bold.cyan("\u{1F517} Trace IDE Bridge"));
|
|
3346
|
-
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
3347
|
-
console.log();
|
|
3348
|
-
console.log(`Project: ${chalk2.green(projectPath)}`);
|
|
3349
|
-
console.log(`Port: ${chalk2.cyan(port)}`);
|
|
3350
|
-
console.log();
|
|
3351
|
-
try {
|
|
3352
|
-
const pkgPath = path4.join(projectPath, "package.json");
|
|
3353
|
-
if (fs4.existsSync(pkgPath)) {
|
|
3354
|
-
const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
|
|
3355
|
-
console.log(`\u{1F4E6} Package: ${chalk2.yellow(pkg.name)} v${pkg.version}`);
|
|
3356
|
-
}
|
|
3357
|
-
} catch (_) {
|
|
3358
|
-
}
|
|
3359
|
-
const wss = new WebSocketServer({ port });
|
|
3360
|
-
let clientCount = 0;
|
|
3361
|
-
console.log();
|
|
3362
|
-
console.log(chalk2.green("\u2713") + " WebSocket server started");
|
|
3363
|
-
console.log(chalk2.dim("Waiting for extension to connect..."));
|
|
3364
|
-
console.log();
|
|
3365
|
-
console.log(chalk2.gray("\u2500".repeat(55)));
|
|
3366
|
-
console.log(chalk2.dim("Press Ctrl+C to stop"));
|
|
3367
|
-
console.log();
|
|
3368
|
-
wss.on("connection", (ws) => {
|
|
3369
|
-
clientCount++;
|
|
3370
|
-
console.log(chalk2.green("\u25CF") + ` Extension connected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
3371
|
-
attachMessageHandler(ws, projectPath);
|
|
3372
|
-
ws.on("close", () => {
|
|
3373
|
-
clientCount--;
|
|
3374
|
-
console.log(chalk2.yellow("\u25CF") + ` Extension disconnected (${clientCount} client${clientCount > 1 ? "s" : ""})`);
|
|
3375
|
-
});
|
|
3376
|
-
ws.on("error", (error) => {
|
|
3377
|
-
console.error(chalk2.red("WebSocket error:"), error.message);
|
|
3378
|
-
});
|
|
3379
|
-
});
|
|
3380
|
-
wss.on("error", (error) => {
|
|
3381
|
-
if (error.code === "EADDRINUSE") {
|
|
3382
|
-
console.log(chalk2.red(`\u2717 Port ${port} is already in use`));
|
|
3383
|
-
console.log(chalk2.dim("Try: trace-connect --port 8766"));
|
|
3384
|
-
} else {
|
|
3385
|
-
console.error(chalk2.red("Server error:"), error.message);
|
|
3386
|
-
}
|
|
3387
|
-
process.exit(1);
|
|
3388
|
-
});
|
|
3389
|
-
process.on("SIGINT", () => {
|
|
3390
|
-
console.log();
|
|
3391
|
-
console.log(chalk2.dim("Stopping..."));
|
|
3392
|
-
wss.close();
|
|
3393
|
-
process.exit(0);
|
|
3394
|
-
});
|
|
3395
|
-
});
|
|
3396
|
-
program.parse(process.argv);
|
|
3397
|
-
function getFileTree(dir, depth, currentDepth = 0) {
|
|
3398
|
-
if (currentDepth >= depth)
|
|
3399
|
-
return null;
|
|
3400
|
-
const result = {};
|
|
3401
|
-
const ignorePatterns = ["node_modules", ".git", "dist", "build", ".next", "coverage", ".cache"];
|
|
3402
|
-
try {
|
|
3403
|
-
const items = fs4.readdirSync(dir);
|
|
3404
|
-
for (const item of items.slice(0, 50)) {
|
|
3405
|
-
if (ignorePatterns.includes(item) || item.startsWith("."))
|
|
3406
|
-
continue;
|
|
3407
|
-
const fullPath = path4.join(dir, item);
|
|
3408
|
-
try {
|
|
3409
|
-
const stat = fs4.statSync(fullPath);
|
|
3410
|
-
if (stat.isDirectory()) {
|
|
3411
|
-
result[item] = getFileTree(fullPath, depth, currentDepth + 1);
|
|
3412
|
-
} else {
|
|
3413
|
-
result[item] = "file";
|
|
3414
|
-
}
|
|
3415
|
-
} catch (e) {
|
|
3416
|
-
}
|
|
3417
|
-
}
|
|
3418
|
-
} catch (e) {
|
|
3419
|
-
}
|
|
3420
|
-
return result;
|
|
3421
|
-
}
|
|
68
|
+
`+l,"utf-8")}else h.appendFileSync(t,`
|
|
69
|
+
`+l,"utf-8");let g=await de(t,e);i.data={success:!0,path:t,formatted:g,undoAvailable:!0},console.log(m.blue("\u2139")+` Appended file: ${n.filePath}`+(g?m.dim(` (formatted with ${g})`):"")+m.dim(" [undo saved]"))}}catch(t){i.error=t.message}break;case"EDIT_CLASSNAME":{try{let t=b.resolve(e,n.filePath);if(!Q(e,t)){i.error="Access denied: Path outside project";break}if(!h.existsSync(t)){i.error="File not found: "+n.filePath;break}if(n.readToken){let g=h.statSync(t);if(`${Math.round(g.mtimeMs)}-${g.size}`!==n.readToken){i.error="File has changed since last read (readToken mismatch). Re-read with read_project_file before editing.";break}}let f=h.readFileSync(t,"utf-8");s.push({filePath:t,prevContent:f,operation:"EDIT_CLASSNAME",timestamp:Date.now(),relativePath:n.filePath}),s.length>20&&s.shift();let l=De(f,{oldValue:n.oldValue,newValue:n.newValue,lineHint:n.lineHint});if(!l.success){s.pop(),i.error=l.error??"AST edit failed";break}await ge(t,async()=>{h.writeFileSync(t,l.code,"utf-8");let g=await de(t,e);i.data={success:!0,path:t,strategy:l.strategy,matchedLine:l.matchedLine,formatted:g,undoAvailable:!0},console.log(m.blue("\u2139")+` EDIT_CLASSNAME: ${n.filePath}`+m.dim(` [${l.strategy} @ line ${l.matchedLine}]`)+(g?m.dim(` (${g})`):"")+m.dim(" [undo saved]"))});let d=await he(t,e);d.ran&&i.data&&(i.data.lspDiagnostics=d.diagnostics,d.summary&&(i.data.lspSummary=d.summary))}catch(t){i.error=t.message}break}case"EDIT_FILE":try{let t=b.resolve(e,n.filePath);if(!Q(e,t))i.error="Access denied: Path outside project";else if(h.existsSync(t)){if(n.readToken){let d=h.statSync(t);if(`${Math.round(d.mtimeMs)}-${d.size}`!==n.readToken){i.error="File has changed since last read (readToken mismatch). Re-read the file with read_project_file to get fresh content and line numbers before editing. The file was NOT modified.";break}}let f=h.readFileSync(t,"utf-8");s.push({filePath:t,prevContent:f,operation:"EDIT_FILE",timestamp:Date.now(),relativePath:n.filePath}),s.length>20&&s.shift();let l=Bt(f,n.target,n.replacement,n.replaceAll||!1);if("error"in l)s.pop(),i.error=l.error;else{if([".js",".jsx",".tsx",".ts"].some(p=>t.endsWith(p))){let p=l.result,T=J=>J.replace(/`[^`]*`/g,"``").replace(/"(?:[^"\\]|\\.)*"/g,'""').replace(/\'(?:[^\'\\]|\\.)*\'/g,"''"),L=T(f),w=(L.match(/\{/g)||[]).length-(L.match(/\}/g)||[]).length,R=(L.match(/\(/g)||[]).length-(L.match(/\)/g)||[]).length,k=T(p),I=(k.match(/\{/g)||[]).length,D=(k.match(/\}/g)||[]).length,A=(k.match(/\(/g)||[]).length,F=(k.match(/\)/g)||[]).length,B=I-D,j=A-F;if(Math.abs(B)>Math.abs(w)||Math.abs(j)>Math.abs(R)){s.pop(),i.error=`JSX validation failed: resulting file has unbalanced syntax (braces: ${I} open / ${D} close, parens: ${A} open / ${F} close). The file was NOT modified. Re-read the file and fix the replacement so the overall file stays balanced.`;break}}await ge(t,async()=>{h.writeFileSync(t,l.result,"utf-8");let p=await de(t,e);i.data={success:!0,path:t,strategy:l.strategy,formatted:p,undoAvailable:!0};let T=l.strategy==="exact"?"":m.dim(` [${l.strategy}]`),L=p?m.dim(` (${p})`):"";console.log(m.blue("\u2139")+` Edited file: ${n.filePath}${T}${L}`+m.dim(" [undo saved]"))});let g=await he(t,e);g.ran&&i.data&&(i.data.lspDiagnostics=g.diagnostics,g.summary&&(i.data.lspSummary=g.summary,g.diagnostics.some(p=>p.severity==="error")&&console.log(m.yellow("\u26A0")+` ${g.summary}`)))}}else{let f=b.basename(n.filePath),l=b.extname(f),d=f.replace(l,""),g=[],p=["src/app","app","src/pages","pages","src/components","components","src"];for(let L of p){let w=b.join(e,L);if(h.existsSync(w))try{let R=h.readdirSync(w,{recursive:!0});for(let k of R.slice(0,100)){let I=b.basename(k);(I.startsWith(d+".")||I===f)&&g.push(b.join(L,k).replace(/\\/g,"/"))}}catch{}}let T=g.length>0?" Did you mean: "+g.slice(0,3).join(" or ")+"?":" Double-check the path against detect_project() output (layoutFile, globalStyleFile) or projectTree.";i.error='File not found: "'+n.filePath+'".'+T}}catch(t){i.error=t.message}break;case"REPLACE_LINES":try{let t=b.resolve(e,n.filePath);if(!Q(e,t))i.error="Access denied: Path outside project";else if(!h.existsSync(t))i.error="File not found: "+n.filePath;else{if(n.readToken){let p=h.statSync(t);if(`${Math.round(p.mtimeMs)}-${p.size}`!==n.readToken){i.error="File has changed since last read (readToken mismatch). Re-read the file with read_project_file to get fresh line numbers before replacing. The file was NOT modified.";break}}let f=h.readFileSync(t,"utf-8"),l=f.split(`
|
|
70
|
+
`),d=Math.max(1,n.startLine||1),g=Math.min(l.length,n.endLine||d);if(d>l.length)i.error=`Start line ${d} is beyond file end (${l.length} lines)`;else if(d>g)i.error=`Invalid range: start (${d}) > end (${g})`;else{s.push({filePath:t,prevContent:f,operation:"REPLACE_LINES",timestamp:Date.now(),relativePath:n.filePath}),s.length>20&&s.shift();let p=g-d+1,T=(n.newContent||"").split(`
|
|
71
|
+
`);if([".js",".jsx",".tsx",".ts"].some(R=>t.endsWith(R))){let R=q=>q.replace(/`[^`]*`/g,"``").replace(/"(?:[^"\\]|\\.)*"/g,'""').replace(/\'(?:[^\'\\]|\\.)*\'/g,"''"),k=R(f),I=(k.match(/\{/g)||[]).length-(k.match(/\}/g)||[]).length,D=(k.match(/\(/g)||[]).length-(k.match(/\)/g)||[]).length,A=[...l],F=(n.newContent||"").split(`
|
|
72
|
+
`);A.splice(d-1,g-d+1,...F);let B=R(A.join(`
|
|
73
|
+
`)),j=(B.match(/\{/g)||[]).length,J=(B.match(/\}/g)||[]).length,te=(B.match(/\(/g)||[]).length,se=(B.match(/\)/g)||[]).length,ie=j-J,K=te-se;if(Math.abs(ie)>Math.abs(I)||Math.abs(K)>Math.abs(D)){s.pop();let q=l.slice(d-1,g).join(`
|
|
74
|
+
`),X=R(q),S=(X.match(/\{/g)||[]).length-(X.match(/\}/g)||[]).length,v=(X.match(/\(/g)||[]).length-(X.match(/\)/g)||[]).length,x="";if(S>0||v>0){let $=S,M=v,Z=g,C=l.slice(g);for(let _=0;_<C.length;_++){let E=R(C[_]);if($-=(E.match(/\}/g)||[]).length-(E.match(/\{/g)||[]).length,M-=(E.match(/\)/g)||[]).length-(E.match(/\(/g)||[]).length,Z=g+_+1,$<=0&&M<=0)break}x=` The range ${d}-${g} has unclosed syntax. Expand end_line to ~${Z} to include the closing syntax, then retry.`}else x=" Re-read the file and ensure the replacement keeps all JSX balanced.";i.error=`JSX validation failed: resulting file has unbalanced syntax (braces: ${j} open / ${J} close, parens: ${te} open / ${se} close). The file was NOT modified.${x}`;break}}await ge(t,async()=>{l.splice(d-1,p,...T);let R=l.join(`
|
|
75
|
+
`);h.writeFileSync(t,R,"utf-8");let k=await de(t,e),I=T.length-p;i.data={success:!0,path:t,linesReplaced:`${d}-${g}`,oldLineCount:p,newLineCount:T.length,lineDelta:I,totalLines:l.length,formatted:k,undoAvailable:!0,hint:I!==0?`Line count changed by ${I>0?"+":""}${I}. Adjust subsequent line numbers if making more edits.`:null};let D=k?m.dim(` (${k})`):"";console.log(m.blue("\u2139")+` Replaced lines ${d}-${g} in ${n.filePath} (${p}\u2192${T.length} lines)${D}`+m.dim(" [undo saved]"))});let w=await he(t,e);w.ran&&i.data&&(i.data.lspDiagnostics=w.diagnostics,w.summary&&(i.data.lspSummary=w.summary,w.diagnostics.some(R=>R.severity==="error")&&console.log(m.yellow("\u26A0")+` ${w.summary}`)))}}}catch(t){i.error=t.message}break;case"UNDO_LAST":try{if(s.length===0)i.error="Nothing to undo \u2014 no file changes recorded in this session.";else{let t=s.pop();t.prevContent===null?h.existsSync(t.filePath)?(h.unlinkSync(t.filePath),i.data={success:!0,undone:t.operation,file:t.relativePath,action:"deleted (was new file)",remaining:s.length},console.log(m.yellow("\u21A9")+` Undo: deleted ${t.relativePath} (was a new file)`)):i.data={success:!0,undone:t.operation,file:t.relativePath,action:"already gone",remaining:s.length}:(h.writeFileSync(t.filePath,t.prevContent,"utf-8"),i.data={success:!0,undone:t.operation,file:t.relativePath,action:"restored",remaining:s.length},console.log(m.yellow("\u21A9")+` Undo: restored ${t.relativePath} (reverted ${t.operation})`))}}catch(t){i.error=t.message}break;case"DELETE_FILE":try{let t=b.resolve(e,n.filePath);if(!Q(e,t)){i.error="Access denied: Path outside project";break}if(!h.existsSync(t)){i.error="File not found: "+n.filePath;break}let f=n.filePath.replace(/\\/g,"/"),l=b.basename(f);if(["package.json","package-lock.json","yarn.lock","pnpm-lock.yaml","bun.lockb","bun.lock","tsconfig.json","tsconfig.node.json","jsconfig.json",/^next\.config\./,/^vite\.config\./,/^nuxt\.config\./,/^svelte\.config\./,/^remix\.config\./,/^astro\.config\./,/^webpack\.config\./,/^babel\.config\./,/^tailwind\.config\./,/^postcss\.config\./,/^prettier\.config\./,/^eslint\.config\./,".eslintrc",".prettierrc",".babelrc",".env",".env.local",".env.production",".env.development",".env.staging",/^layout\.(tsx|jsx|js|ts)$/,/^_app\.(tsx|jsx|js|ts)$/,/^_document\.(tsx|jsx|js|ts)$/,/^App\.(tsx|jsx|js|ts)$/,/^main\.(tsx|jsx|js|ts)$/,"Dockerfile","docker-compose.yml","docker-compose.yaml",".gitignore",".gitattributes"].some(k=>typeof k=="string"?l===k||f.endsWith("/"+k):k.test(l))){i.error=`\u{1F6E1} Protected file: "${n.filePath}" cannot be deleted by the agent. This file is structurally critical to the project. To resolve conflicts involving this file, edit its contents instead of deleting it.`;break}let p=b.join(e,".trace-trash");if(!h.existsSync(p)){h.mkdirSync(p,{recursive:!0});let k=b.join(e,".gitignore");h.existsSync(k)&&(h.readFileSync(k,"utf-8").includes(".trace-trash")||h.appendFileSync(k,`
|
|
76
|
+
# Trace soft-delete recovery folder
|
|
77
|
+
.trace-trash/
|
|
78
|
+
`))}let T=Date.now(),L=`${T}_${l}`,w=b.join(p,L),R=h.readFileSync(t,"utf-8");s.push({filePath:t,prevContent:R,operation:"DELETE_FILE",timestamp:T,relativePath:n.filePath}),s.length>20&&s.shift(),h.renameSync(t,w),i.data={success:!0,deleted:n.filePath,recoveryPath:b.relative(e,w),undoAvailable:!0,hint:"File moved to .trace-trash/ \u2014 recoverable even after CLI restart. Call UNDO_LAST to restore to original path."},console.log(m.yellow("\u{1F5D1}")+` Soft-deleted: ${n.filePath} \u2192 .trace-trash/${L}`+m.dim(" [recoverable]"))}catch(t){i.error=t.message}break;case"RENAME_FILE":try{let t=b.resolve(e,n.oldPath),f=b.resolve(e,n.newPath);if(!Q(e,t)||!Q(e,f))i.error="Access denied: Path outside project";else if(!h.existsSync(t))i.error="File not found: "+n.oldPath;else if(h.existsSync(f))i.error="Target already exists: "+n.newPath+". Delete it first or choose a different name.";else{let l=h.readFileSync(t,"utf-8");s.push({filePath:t,prevContent:l,operation:"RENAME_FILE",timestamp:Date.now(),relativePath:n.oldPath}),s.length>20&&s.shift();let d=b.dirname(f);h.existsSync(d)||h.mkdirSync(d,{recursive:!0}),h.renameSync(t,f),i.data={success:!0,oldPath:n.oldPath,newPath:n.newPath,undoAvailable:!0},console.log(m.blue("\u2139")+` Renamed: ${n.oldPath} \u2192 ${n.newPath}`+m.dim(" [undo saved]"))}}catch(t){i.error=t.message}break;case"GIT_BLAME":try{let t=b.resolve(e,n.filePath||"");if(!Q(e,t)){i.error="Access denied: Path outside project";break}let f=parseInt(n.line,10);if(!Number.isFinite(f)||f<1){i.error="Invalid line number";break}let{stdout:l}=await Re("git",["blame","-L",`${f},${f}`,"--porcelain",t],{cwd:e}),d=l.split(`
|
|
79
|
+
`),g=d[0].split(" ")[0],p=d.find(R=>R.startsWith("author "))?.substring(7),T=d.find(R=>R.startsWith("author-mail "))?.substring(12),L=d.find(R=>R.startsWith("author-time "))?.substring(12),w=d.find(R=>R.startsWith("summary "))?.substring(8);i.data={commit:g,author:p,email:T,date:new Date(parseInt(L)*1e3).toISOString().split("T")[0],message:w}}catch(t){i.error=`Git blame failed: ${t.message}`}break;case"GIT_RECENT_CHANGES":try{let t=b.resolve(e,n.filePath||"");if(!Q(e,t)){i.error="Access denied: Path outside project";break}let f=Number.isFinite(n.days)&&n.days>0?Math.floor(n.days):7,{stdout:l}=await Re("git",["log","-n","10","--since",`${f} days ago`,"--pretty=format:%h|%an|%ad|%s","--date=short","--",t],{cwd:e});i.data={history:l.split(`
|
|
80
|
+
`).filter(Boolean).map(d=>{let[g,p,T,L]=d.split("|");return{hash:g,author:p,date:T,message:L}})}}catch(t){i.error=`Git log failed: ${t.message}`}break;case"GET_IMPORTS":try{let t=b.resolve(e,n.filePath||"");if(!Q(e,t))i.error="Access denied: Path outside project";else if(h.existsSync(t)){let f=h.readFileSync(t,"utf-8"),l=/import\s+(?:[\w*\s{},]*)\s+from\s+['"]([^'"]+)['"]/g,d=[],g;for(;(g=l.exec(f))!==null;)d.push(g[1]);i.data={imports:d}}else i.error="File not found"}catch(t){i.error=t.message}break;case"FIND_USAGES":try{let t=typeof n.query=="string"?n.query:"";if(!t){i.error="Empty query";break}let{stdout:f}=await Re("git",["grep","-n","--",t],{cwd:e});i.data={usages:f.split(`
|
|
81
|
+
`).filter(Boolean).slice(0,20).map(l=>{let d=l.split(":");return{file:d[0],line:parseInt(d[1]),content:d.slice(2).join(":").trim()}})}}catch(t){t.code===1?i.data={usages:[]}:i.error=`Grep failed: ${t.message}`}break;case"GET_ENV_VARS":try{let t=b.resolve(e,n.filePath||"");if(!Q(e,t))i.error="Access denied: Path outside project";else if(h.existsSync(t)){let f=h.readFileSync(t,"utf-8"),l=/(?:process\.env\.|import\.meta\.env\.)([A-Z_][A-Z0-9_]*)/g,d=new Set,g;for(;(g=l.exec(f))!==null;)d.add(g[1]);i.data={envVars:Array.from(d)}}else i.error="File not found"}catch(t){i.error=t.message}break;case"GET_TERMINAL_BUFFER":{let t=parseInt(n.lines)||100,f=le.getLast(t);i.data={lines:f,totalLines:le.length,hasDevProcess:z!==null&&!z.killed};break}case"GET_DEV_STATUS":{i.data={running:z!==null&&!z.killed,pid:z?.pid??null};break}default:i.error=`Unknown message type: ${c}`}o.send(JSON.stringify(i))}catch(n){console.error(m.red("Parse error:"),n.message)}}),o.on("message",u=>{try{let a=JSON.parse(u.toString());if(a.type==="RESPONSE"&&a.id&&ne.has(a.id)){let{resolve:n}=ne.get(a.id);ne.delete(a.id),n(a.error?{error:a.error}:a.data??{})}else if(a.type==="AGENT_PROGRESS"&&a.id&&ne.has(a.id)){let n=ne.get(a.id);if(n.onProgress)try{n.onProgress(a.event)}catch{}}}catch{}})}ye.command("connect",{isDefault:!0}).alias("c").description('Start IDE bridge only (use "trace dev" to also start your dev server)').option("-p, --port <port>","WebSocket port","8765").action(async o=>{let e=parseInt(o.port),s=process.cwd();console.log(),console.log(m.bold.cyan("\u{1F517} Trace IDE Bridge")),console.log(m.gray("\u2500".repeat(55))),console.log(),console.log(`Project: ${m.green(s)}`),console.log(`Port: ${m.cyan(e)}`),console.log();try{let a=b.join(s,"package.json");if(h.existsSync(a)){let n=JSON.parse(h.readFileSync(a,"utf-8"));console.log(`\u{1F4E6} Package: ${m.yellow(n.name)} v${n.version}`)}}catch{}let r=new Me({port:e}),u=0;console.log(),console.log(m.green("\u2713")+" WebSocket server started"),console.log(m.dim("Waiting for extension to connect...")),console.log(),console.log(m.gray("\u2500".repeat(55))),console.log(m.dim("Press Ctrl+C to stop")),console.log(),r.on("connection",a=>{u++,console.log(m.green("\u25CF")+` Extension connected (${u} client${u>1?"s":""})`),Ge(a,s),a.on("close",()=>{u--,console.log(m.yellow("\u25CF")+` Extension disconnected (${u} client${u>1?"s":""})`)}),a.on("error",n=>{console.error(m.red("WebSocket error:"),n.message)})}),r.on("error",a=>{a.code==="EADDRINUSE"?(console.log(m.red(`\u2717 Port ${e} is already in use`)),console.log(m.dim("Try: trace-connect --port 8766"))):console.error(m.red("Server error:"),a.message),process.exit(1)}),process.on("SIGINT",()=>{console.log(),console.log(m.dim("Stopping...")),r.close(),process.exit(0)})});ye.parse(process.argv);function Ve(o,e,s=0){if(s>=e)return null;let r={},u=["node_modules",".git","dist","build",".next","coverage",".cache"];try{let a=h.readdirSync(o);for(let n of a.slice(0,50)){if(u.includes(n)||n.startsWith("."))continue;let y=b.join(o,n);try{h.statSync(y).isDirectory()?r[n]=Ve(y,e,s+1):r[n]="file"}catch{}}}catch{}return r}
|