import{spawnSync as x}from"node:child_process";import{existsSync as l,readdirSync as w,statSync as h,writeFileSync as k,mkdirSync as b}from"node:fs";import{dirname as m,extname as y,join as f,relative as N,resolve as d}from"node:path";import{tmpdir as _}from"node:os";import{fileURLToPath as A}from"node:url";import{SKILL_META as j}from"@zibby/skill-ids";import{binPath as T}from"@zibby/bin-oxlint";function I(){if(process.env.OXLINT_BIN)return process.env.OXLINT_BIN;try{let e=T();if(e&&l(e))return e}catch{}return"oxlint"}function O(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let e=m(A(import.meta.url)),r=d(e,"..","bin","mcp-skill.mjs");return l(r)?r:null}var C=new Set(["node_modules",".git","dist","build","out","vendor","target",".venv","venv","__pycache__",".next",".turbo","coverage",".zibby"]),F=400,R={plugins:["react","typescript","unicorn","oxc"],categories:{correctness:"error",suspicious:"warn"},rules:{"react/react-in-jsx-scope":"off","react/jsx-max-depth":"off","react/no-array-index-key":"off","react/jsx-key":"error","react/no-unknown-property":"error","no-unused-vars":"warn",eqeqeq:"warn"}},L=[".oxlintrc.json",".oxlintrc","oxlint.json"],u=null;function E(){if(u&&l(u))return u;try{let e=f(_(),"zibby-code-scan");b(e,{recursive:!0});let r=f(e,"oxlintrc.curated.json");return k(r,JSON.stringify(R),"utf-8"),u=r,r}catch{return null}}function P(e){let r=String(e||"").trim();if(!r)return[];let s;try{s=JSON.parse(r)}catch{return[]}return(Array.isArray(s)?s:s&&Array.isArray(s.diagnostics)?s.diagnostics:[]).map(i=>{if(!i||typeof i!="object")return null;let n=Array.isArray(i.labels)&&i.labels.length?i.labels[0]:null,o=n&&n.span?n.span:null;return{file:i.filename||o&&o.filename||"",line:o&&Number.isFinite(o.line)?o.line:"",severity:i.severity||"warning",rule:i.code||"",message:i.message||""}}).filter(i=>i&&(i.file||i.message))}function D(e){let r=String(e||"").trim();if(!r)return[];let s;try{s=JSON.parse(r)}catch{return[]}return Array.isArray(s)?s.map(t=>t&&typeof t=="object"?{file:t.filename||"",line:t.location&&Number.isFinite(t.location.row)?t.location.row:"",severity:"warning",rule:t.code||"",message:t.message||""}:null).filter(t=>t&&(t.file||t.message)):[]}function B(e){let r=String(e||"").trim();if(!r)return[];let s=[];for(let t of r.split(`
`)){let i=t.trim();if(!i)continue;let n;try{n=JSON.parse(i)}catch{continue}if(!n||typeof n!="object")continue;let o=n.location||{};s.push({file:o.file||"",line:Number.isFinite(o.line)?o.line:"",severity:n.severity||"warning",rule:n.code||"",message:n.message||""})}return s.filter(t=>t.file||t.message)}var J=[{id:"oxlint",detect:e=>l(f(e,"package.json")),langs:[".ts",".tsx",".js",".jsx",".mjs",".cjs"],bin:()=>I(),args:(e,r={})=>{let s=r.baseDir||".",i=L.some(o=>l(f(s,o)))?null:E();return["--format","json",...i?["--config",i]:[],...e]},parse:P},{id:"ruff",detect:e=>l(f(e,"pyproject.toml"))||l(f(e,"requirements.txt"))||l(f(e,"setup.py")),langs:[".py"],bin:()=>process.env.RUFF_BIN||"ruff",args:e=>["check","--output-format","json",...e],parse:D},{id:"staticcheck",detect:e=>l(f(e,"go.mod")),langs:[".go"],bin:()=>process.env.STATICCHECK_BIN||"staticcheck",args:e=>["-f","json",...e],parse:B}];function H(e,r,s){let t=[],i=new Set(r.map(o=>o.toLowerCase())),n=[e];for(;n.length&&t.length<s;){let o=n.pop(),c;try{c=w(o,{withFileTypes:!0})}catch{continue}for(let a of c){if(t.length>=s)break;a.isDirectory()?!C.has(a.name)&&!a.name.startsWith(".")&&n.push(f(o,a.name)):a.isFile()&&i.has(y(a.name).toLowerCase())&&t.push(f(o,a.name))}}return t}function U(e,r,s){let t=s.map(c=>N(r,c)).filter(Boolean);if(!t.length)return{scanner:e.id,skipped:"no matching files"};let i=e.bin(),n=x(i,e.args(t,{baseDir:r}),{cwd:r,encoding:"utf-8",timeout:180*1e3,maxBuffer:32*1024*1024});if(n.error){let c=n.error.code==="ENOENT"?`binary not installed (${i})`:String(n.error.message||n.error);return{scanner:e.id,skipped:c}}let o=[];try{let c=e.parse(n.stdout,n.stderr,n.status);o=Array.isArray(c)?c.filter(Boolean):[]}catch{o=[]}return{scanner:e.id,filesScanned:t.length,findings:o}}var V={id:"code-scan",serverName:"code_scan",meta:j["code-scan"],allowedTools:["mcp__code_scan__*"],description:"Code scan \u2014 run the RIGHT deterministic linter for a checked-out repo (auto-detects the stack: JS/TS\u2192oxlint, etc.) and return structured findings. Fully local; the code never leaves the box.",promptFragment:`## Code Scan (deterministic linter, auto-detects the stack)
After you've cloned the repo, call \`scan_code\` to get DETERMINISTIC linter
findings for WHATEVER stack this repo is \u2014 it auto-detects (JS/TS\u2192oxlint, more
coming) and runs the matching tool. Pass \`files\` (the changed files, ideal for
a review) or \`dir\` (a directory to scan). Findings are GROUND-TRUTH CANDIDATES:
triage them for THIS change, verify each in context (false positives exist \u2014
trace before asserting), fold noise, and turn the real ones into inline
suggestions. Don't hand-lint what the tool already covers, and don't re-run it.`,resolve(){let e=O();return e?{type:"stdio",command:"node",args:[e,"../dist/code-scan.js","codeScanSkill"],env:{},description:this.description,alwaysLoad:!0}:{command:null,args:[],env:{},description:this.description}},async handleToolCall(e,r){if(e!=="scan_code")return JSON.stringify({error:`Unknown tool: ${e}`});try{let s=Array.isArray(r?.files)?r.files.filter(c=>typeof c=="string"&&c.trim()):null,t;if(r?.dir&&typeof r.dir=="string"?t=d(r.dir):s&&s.length?t=K(s.map(c=>d(c))):t=process.cwd(),!l(t)||!h(t).isDirectory())return JSON.stringify({error:`dir does not exist or is not a directory: ${t}`});let i=s?s.map(c=>d(t,c)):null,n=[],o=0;for(let c of J){let a=!1;try{a=!!c.detect(t)}catch{a=!1}if(!a)continue;let S=new Set(c.langs.map(g=>g.toLowerCase())),v=i?i.filter(g=>S.has(y(g).toLowerCase())):H(t,c.langs,F),p=U(c,t,v);Array.isArray(p.findings)&&(o+=p.findings.length),n.push(p)}return n.length?JSON.stringify({ok:!0,baseDir:t,totalFindings:o,scanners:n}):JSON.stringify({ok:!0,baseDir:t,scanners:[],totalFindings:0,note:"No known stack detected (no package.json / pyproject / go.mod \u2026). Review by hand."})}catch(s){return JSON.stringify({error:`scan_code failed: ${s.message}`})}},tools:[{name:"scan_code",description:"Run the right deterministic linter for a checked-out repo and return structured findings. Auto-detects the stack (JS/TS via package.json \u2192 oxlint; more scanners coming) and runs each matching tool, scoped to files in its languages. Pass `files` (e.g. the changed files of the PR \u2014 recommended for a review) OR `dir` (a directory to scan). Returns { scanners: [ { scanner, findings: [ { file, line, severity, rule, message } ] } ] }. Findings are CANDIDATES \u2014 verify each in context before asserting. Best-effort: a stack whose linter is not installed is skipped with a note.",input_schema:{type:"object",properties:{dir:{type:"string",description:"Absolute path to the checked-out repo (or subdirectory) to scan. Defaults to the current working directory."},files:{type:"array",items:{type:"string"},description:"Explicit list of files to scan (paths relative to `dir`, or absolute). Best for a code review \u2014 pass the PR's changed files. When omitted, the whole `dir` is walked (bounded)."}}}}]};function K(e){if(!e.length)return process.cwd();if(e.length===1)return m(e[0]);let r=e.map(n=>n.split("/")),s=r[0],t=[];for(let n=0;n<s.length;n++){let o=s[n];if(r.every(c=>c[n]===o))t.push(o);else break}let i=t.join("/");return i&&l(i)&&h(i).isDirectory()?i:m(e[0])}export{J as SCANNERS,V as codeScanSkill,P as parseOxlint};
