thuban 0.4.2 → 0.4.3

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/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- const path=require("path"),fs=require("fs"),os=require("os"),CodeScanner=require("./packages/scanner/code-scanner.js"),DependencyGraph=require("./packages/scanner/dependency-graph.js"),CIIntegration=require("./packages/crucible/ci-integration.js"),BadgeGenerator=require("./packages/crucible/badge-generator.js"),Leaderboard=require("./packages/crucible/leaderboard.js"),Crucible=require("./packages/crucible/index.js"),DriftDetector=require("./packages/scanner/drift-detector.js"),WidgetGenerator=require("./packages/scanner/widget-generator.js"),AlertManager=require("./packages/scanner/alert-manager.js"),TechDebtAnalyzer=require("./packages/scanner/tech-debt-analyzer.js"),HallucinationDetector=require("./packages/scanner/hallucination-detector.js"),HtmlReportGenerator=require("./packages/scanner/html-report.js"),FileWatcher=require("./packages/scanner/file-watcher.js"),LicenseManager=require("./packages/scanner/license-manager.js"),PreCommitGate=require("./packages/scanner/pre-commit-gate.js"),TechDebtCostCalculator=require("./packages/scanner/tech-debt-cost.js"),GhostCodeDetector=require("./packages/scanner/ghost-code-detector.js"),AIConfidenceScorer=require("./packages/scanner/ai-confidence-scorer.js"),CopyPasteDriftDetector=require("./packages/scanner/copy-paste-detector.js"),CodebasePassport=require("./packages/scanner/codebase-passport.js"),ExecutiveReport=require("./packages/scanner/executive-report.js"),BaselineManager=require("./packages/scanner/baseline-manager.js"),MonitorStore=require("./packages/scanner/monitor-store.js"),{MonitorService:MonitorService,resolveIntervalMs:resolveIntervalMs}=require("./packages/scanner/monitor-service.js"),{collectFiles:collectFiles}=require("./packages/scanner/file-collector.js");function parseFlag(e,o,n){const c=e.find(e=>e.startsWith(`--${o}=`));return c?c.split("=").slice(1).join("="):!!e.includes(`--${o}`)||n}function parseIntFlag(e,o,n){const c=parseFlag(e,o,null);if(null===c)return n;const l=parseInt(c,10);return isNaN(l)?n:l}const COLORS={reset:"",bold:"",red:"",green:"",yellow:"",blue:"",cyan:"",gray:"",white:"",bgRed:"",bgGreen:"",bgYellow:""};function c(e,o){return`${COLORS[e]}${o}${COLORS.reset}`}function banner(){console.log(""),console.log(c("cyan"," ╔══════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," THUBAN Code Health Engine ")+c("cyan","║")),console.log(c("cyan"," ║")+c("gray"," v0.4.0 · Public Beta · thuban.dev ")+c("cyan","║")),console.log(c("cyan"," ╚══════════════════════════════════════╝")),console.log("")}function printHelp(){banner();const e=(new LicenseManager).getStatus(),o=JSON.parse(fs.readFileSync(path.join(__dirname,"package.json"),"utf-8"));console.log(c("gray",` v${o.version} · ${e.tierName} tier`)+(e.licensed?c("green"," ✓"):"")+c("gray"," · https://thuban.dev")),console.log(""),console.log(c("bold"," QUICK START")),console.log(""),console.log(c("gray"," Run these in order to see what Thuban does:")),console.log(""),console.log(` ${c("cyan","npx thuban scan .")} Scan your project`),console.log(` ${c("cyan","npx thuban fix .")} Preview what can be fixed`),console.log(` ${c("cyan","npx thuban fix . --fix")} Apply all safe fixes`),console.log(` ${c("cyan","npx thuban dashboard .")} Open visual HTML report`),console.log(""),console.log(c("bold"," SCAN & DETECT")),console.log(""),console.log(` ${c("cyan","scan")} [path|url] Full health scan — score, grade, all issues`),console.log(` ${c("cyan","compare")} [a] [b] Compare two codebases side by side`),console.log(` ${c("cyan","hallucinate")} [path] Find phantom APIs and invented imports`),console.log(` ${c("cyan","ghosts")} [path] Dead functions nobody calls`),console.log(` ${c("cyan","ai-score")} [path] Probability each function is AI-generated`),console.log(` ${c("cyan","clones")} [path] Copy-pasted code that has drifted apart`),console.log(` ${c("cyan","drift")} [path] Files that changed since Mother Code was set`),console.log(` ${c("cyan","deps")} [path] Dependency map — circular, orphan, critical`),console.log(` ${c("cyan","diff")} [path] Scan only git-changed files`),console.log(` ${c("cyan","debt")} [path] Detailed tech debt breakdown`),console.log(` ${c("cyan","cost")} [path] Tech debt translated to £ and hours`),console.log(""),console.log(c("bold"," FIX & PROTECT")),console.log(""),console.log(` ${c("cyan","fix")} [path] Preview fixable issues ${c("gray","(safe, no changes)")}`),console.log(` ${c("cyan","fix")} [path] ${c("gray","--fix")} Apply all safe fixes`),console.log(` ${c("cyan","fix")} [path] ${c("gray","--fix --commit")} Each fix = 1 git commit ${c("gray","(revert any)")}`),console.log(` ${c("cyan","inject")} [path] Add Mother Code DNA to every file`),console.log(` ${c("cyan","gate")} Install pre-commit hook ${c("gray","(blocks hallucinations)")}`),console.log(` ${c("cyan","watch")} [path] Live sentinel — scans files as you save`),console.log(` ${c("cyan","monitor")} [path] Recurring scans with saved history and alerts ${c("gray","(Pro)")}`),console.log(` ${c("cyan","history")} [path] View recurring scan history for a repo ${c("gray","(Pro)")}`),console.log(` ${c("cyan","trend")} [path] Show issue trends across recurring scans ${c("gray","(Pro)")}`),console.log(` ${c("cyan","baseline")} [path] Snapshot issues so future scans show only NEW ones`),console.log(""),console.log(c("bold"," REPORTS")),console.log(""),console.log(` ${c("cyan","dashboard")} [path] Interactive HTML report ${c("gray","(share with your team)")}`),console.log(` ${c("cyan","executive")} [path] CTO-ready report ${c("gray","(print to PDF from browser)")}`),console.log(` ${c("cyan","report")} [path] Combined scan + deps + drift summary`),console.log(` ${c("cyan","passport")} [path] Codebase identity card ${c("gray","(for new team members)")}`),console.log(""),console.log(c("bold"," LICENSE")),console.log(""),console.log(` ${c("cyan","activate")} <key> Activate a license key`),console.log(` ${c("cyan","status")} Show license, usage, and features`),console.log(` ${c("cyan","upgrade")} Open pricing page`),console.log(""),console.log(c("bold"," OPTIONS")),console.log(""),console.log(` ${c("gray","--fix")} Apply changes ${c("gray","(default is dry-run preview)")}`),console.log(` ${c("gray","--commit")} Auto-commit each fix for easy rollback`),console.log(` ${c("gray","--unsafe")} Include fixes that change runtime behaviour`),console.log(` ${c("gray","--json")} Machine-readable JSON output`),console.log(` ${c("gray","--baseline")} Only show NEW issues vs saved baseline`),console.log(` ${c("gray","--verbose")} Show detailed output`),console.log(` ${c("gray","--ignore <pat>")} Skip files matching pattern`),console.log(` ${c("gray","--max-issues <n>")} Limit displayed issues ${c("gray","(default: 50)")}`),console.log(""),console.log(c("bold"," EXAMPLES")),console.log(""),console.log(c("gray"," # Scan the current project")),console.log(` ${c("white","npx thuban scan .")}`),console.log(""),console.log(c("gray"," # Find and fix all AI hallucinations")),console.log(` ${c("white","npx thuban hallucinate .")}`),console.log(` ${c("white","npx thuban fix . --fix")}`),console.log(""),console.log(c("gray"," # Generate a report for your CTO")),console.log(` ${c("white","npx thuban executive .")}`),console.log(""),console.log(c("gray"," # Block hallucinated code on every commit")),console.log(` ${c("white","npx thuban gate --fix")}`),console.log(""),console.log(c("gray"," # Scan a specific folder, ignore tests")),console.log(` ${c("white",'npx thuban scan ./src --ignore "**/*.test.js"')}`),console.log(""),console.log(c("gray"," Report a bug or request a feature:")),console.log(` ${c("cyan","https://github.com/SilverwingsBenefitsGit/thuban/issues")}`),console.log("")}function parseArgs(e){const o={command:null,targetPath:process.cwd(),json:!1,fix:!1,dryRun:!1,safe:!0,gitCommit:!1,verbose:!1,ignore:[],maxIssues:50,licenseKey:null,baseline:!1,baselineCreate:!1,frequency:"daily",intervalMs:null,notify:"inbox",once:!1},n=["activate","status","upgrade","help","version"];let c=0;for(;c<e.length;){const l=e[c];"--json"===l?o.json=!0:"--fix"===l?o.fix=!0:"--dry-run"===l||"--dryrun"===l?o.dryRun=!0:"--unsafe"===l?o.safe=!1:"--git-commit"===l||"--commit"===l?o.gitCommit=!0:"--baseline"===l?o.baseline=!0:"--baseline-create"===l?o.baselineCreate=!0:"--frequency"===l&&e[c+1]?o.frequency=e[++c]:"--interval-ms"===l&&e[c+1]?o.intervalMs=parseInt(e[++c],10):"--notify"===l&&e[c+1]?o.notify=e[++c]:"--once"===l?o.once=!0:"--verbose"===l?o.verbose=!0:"--ignore"===l&&e[c+1]?o.ignore.push(e[++c]):"--max-issues"===l&&e[c+1]?o.maxIssues=parseInt(e[++c],10):"--help"===l||"-h"===l?o.command="help":"--version"===l||"-v"===l?o.command="version":o.command?"activate"!==o.command||o.licenseKey?n.includes(o.command)||(l.match(/^(https?:\/\/|git@)/)?o.targetPath=l:o.targetPath=path.resolve(l)):o.licenseKey=l:o.command=l,c++}return o}function summarizeRemainingFiles(e,o,n){const c=new Set(e),l=o.filter(e=>!c.has(e)),s=new Map;for(const e of l){const o=path.relative(n,e).split(path.sep).filter(Boolean),c=o.length>1?o[0]:"(root)";s.set(c,(s.get(c)||0)+1)}const t=Array.from(s.entries()).sort((e,o)=>o[1]-e[1]||e[0].localeCompare(o[0])).slice(0,3).map(([e,o])=>({directory:e,count:o}));return{remainingFiles:l,remainingCount:l.length,topDirectories:t,previewFiles:l.slice(0,5).map(e=>path.relative(n,e))}}function requireProFeature(e){const o=new LicenseManager;if(!o.canUseProFeature(e).allowed){const o="https://thuban.dev/pricing";console.log(""),console.log(c("yellow"," ╔══════════════════════════════════════════════════════════════╗")),console.log(c("yellow"," ║")+c("bold"," PRO FEATURE REQUIRED ")+c("yellow","║")),console.log(c("yellow"," ╠══════════════════════════════════════════════════════════════╣")),console.log(c("yellow"," ║")+` ${e} is available on Thuban Pro.`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" Free Thuban gives you a one-off checkup. Pro gives you".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" a pair of eyes on your codebase all day every day with".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" recurring scans, saved history, trends, and alerts.".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" "+c("yellow","║")),console.log(c("yellow"," ║")+` Upgrade now: ${o}`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" CLI shortcut: thuban upgrade".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ╚══════════════════════════════════════════════════════════════╝")),console.log(""),process.exit(0)}return o}function resolveRepoConfigOrExit(e,o){const n=e.getRepo(o);return n||(console.log(""),console.log(c("yellow"," No monitor configuration found for this repo.")),console.log(c("gray"," Run `thuban monitor <path> --once` or `thuban monitor <path>` first.")),console.log(""),process.exit(0)),n}function printFileCapUpgradePrompt({scannedCount:e,totalCount:o,remainingSummary:n,upgradeUrl:l}){const s=n.topDirectories.length;if(console.log(c("yellow"," ╔══════════════════════════════════════════════════════════════╗")),console.log(c("yellow"," ║")+c("bold"," FREE TIER LIMIT REACHED ")+c("yellow","║")),console.log(c("yellow"," ╠══════════════════════════════════════════════════════════════╣")),console.log(c("yellow"," ║")+` You have scanned ${e} of ${o} files.`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" Upgrade to Thuban Pro for continuous monitoring —".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" a pair of eyes on your codebase, all day, every day.".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" Your always-on guardian with unlimited scans.".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+` ${n.remainingCount} more files remaining including ${s}`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+` critical director${1===s?"y":"ies"}.`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" "+c("yellow","║")),console.log(c("yellow"," ║")+` Upgrade now: ${l}`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" CLI shortcut: thuban upgrade".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ╚══════════════════════════════════════════════════════════════╝")),console.log(""),n.topDirectories.length>0){console.log(c("bold"," Remaining scan preview"));for(const e of n.topDirectories)console.log(` ${c("yellow","•")} ${c("cyan",e.directory)} ${c("gray",`(${e.count} files)`)}`);if(n.previewFiles.length>0){console.log(""),console.log(c("gray"," Next files that would be scanned:"));for(const e of n.previewFiles)console.log(` ${c("gray","·")} ${e}`)}console.log("")}}async function cmdScan(e){const o=Date.now(),n=/^(https?:\/\/|git@)/.test(e.targetPath);let l=n?e.targetPath:path.resolve(e.targetPath),s=null;if(n){e.json||(console.log(""),console.log(c("cyan"," Cloning repository...")),console.log(c("gray",` ${e.targetPath}`)));const o=path.join(os.tmpdir(),"thuban-scan-"+Date.now()),{execFileSync:n}=require("child_process");try{n("git",["clone","--depth","1",e.targetPath,o],{stdio:"pipe",timeout:6e4}),l=o,s=o,e.json||(console.log(c("cyan"," Cloned successfully")),console.log(""))}catch(e){console.error(c("red",` Failed to clone repository: ${e.message}`)),console.error(c("gray"," Check the URL is correct and the repo is accessible")),process.exit(1)}}const t=new LicenseManager,a=t.getTierConfig(),r=t.canScan(),i="https://thuban.dev/pricing";r.allowed||e.json||(console.log(""),console.log(c("red"," ╔══════════════════════════════════════════════════╗")),console.log(c("red"," ║")+c("bold"," Monthly scan limit reached ")+c("red","║")),console.log(c("red"," ╚══════════════════════════════════════════════════╝")),console.log(""),console.log(` You've used ${c("bold",r.used+"/"+r.limit)} free scans this month.`),console.log(` Resets on ${c("cyan",r.resetsOn)}.`),console.log(""),console.log(` ${c("cyan","Upgrade to Pro")} for unlimited scans, full reports, auto-fix, and an always-on pair of eyes via thuban monitor:`),console.log(` ${c("bold",i)}`),console.log(""),console.log(` Or activate a key: ${c("cyan","thuban activate <your-key>")}`),console.log(""),process.exit(0)),e.json||(banner(),0===t.usage.totalScans&&(console.log(c("cyan"," ┌──────────────────────────────────────────────────────┐")),console.log(c("cyan"," │")+c("bold"," Welcome to Thuban! ")+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Thuban scans your codebase for AI hallucinations,")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","dead code, tech debt, and architecture drift.")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","After this scan, try:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix .")+" "+c("gray","Preview fixes")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban dashboard .")+" "+c("gray","Visual report")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban --help")+" "+c("gray","All commands")+" "+c("cyan","│")),console.log(c("cyan"," └──────────────────────────────────────────────────────┘")),console.log("")),console.log(c("bold"," SCANNING: ")+c("cyan",l)),console.log(""));const g=collectFiles(l,e.ignore),d=g.length,h=a.maxFiles<1/0?g.slice(0,a.maxFiles):g,u=d>a.maxFiles,y=u?summarizeRemainingFiles(h,g,l):null;!e.json&&u?(console.log(c("yellow",` Free tier: scanning ${a.maxFiles} of ${d} files`)),console.log(c("gray"," Partial results below. Upgrade to scan your entire codebase.")),console.log("")):e.json||(console.log(c("gray",` Found ${h.length} files to scan...`)),console.log(""));const p=new CodeScanner({rootPath:l,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),f=new HallucinationDetector({rootPath:l}),[m,b]=await Promise.all([p.scanFiles(h),f.scan(h)]),$=Date.now()-o;t.recordScan();const w=[];for(const e of m.issues||[])w.push({file:path.relative(l,e.file),...e});for(const e of b.phantomAPIs)w.push({file:e.file,line:e.line,severity:"critical",category:"hallucination",id:"HALL_API",message:`${e.name} — this API does not exist`,suggestion:e.suggestion,crash:!0});for(const e of b.phantomImports)w.push({file:e.file,line:e.line,severity:"critical",category:"hallucination",id:"HALL_IMPORT",message:`${e.module} — ${e.reason}`,crash:!0});for(const e of b.deprecatedAPIs)w.push({file:e.file,line:e.line,severity:"high",category:"deprecated",id:"DEPR_API",message:`${e.name} — deprecated since ${e.since}`,suggestion:e.suggestion});for(const e of b.aiSmells)w.push({file:e.file,line:e.line,severity:"warning",category:"ai_smell",id:e.id,message:e.message});if(e.json)return void console.log(JSON.stringify({files:d,filesScanned:h.length,elapsed_ms:$,tier:t.getTier(),issues:a.showFullDetails?w:w.slice(0,a.teaserIssues),totalIssues:w.length,gated:!a.showFullDetails,fileLimitReached:u,upgradeUrl:u?i:null,remainingFiles:y?y.remainingCount:0,remainingDirectories:y?y.topDirectories:[],remainingPreview:y?y.previewFiles:[]},null,2));const v={critical:0,high:1,error:2,medium:3,warning:4,low:5,info:6};w.sort((e,o)=>(v[e.severity]||99)-(v[o.severity]||99));const S=w.filter(e=>"critical"===e.severity),C=w.filter(e=>"high"===e.severity||"error"===e.severity),x=w.filter(e=>"medium"===e.severity||"warning"===e.severity),E=w.filter(e=>"low"===e.severity||"info"===e.severity),P=w.filter(e=>"hallucination"===e.category),I=w.filter(e=>"deprecated"===e.category),A=w.filter(e=>"ai_smell"===e.category),D=x.filter(e=>"ai_smell"!==e.category),N=Math.max(0,100-15*S.length-5*C.length-2*D.length-.5*A.length-.25*E.length),T=N>=90?"A":N>=80?"B":N>=70?"C+":N>=60?"C":N>=50?"D+":N>=40?"D":"F",R=N>=80?"green":N>=60?"yellow":"red",F=w.filter(e=>!1!==e.fixable).length;if(console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," THUBAN SCAN RESULTS ")+c("cyan","║")),console.log(c("cyan"," ║")+c("gray"," v0.4.0 · Public Beta ")+c("cyan","║")),console.log(c("cyan"," ╠══════════════════════════════════════════════════════╣")),console.log(c("cyan"," ║")+` Files scanned: ${c("bold",String(h.length).padEnd(6))}`+(u?c("yellow",` of ${d}`):" ")+c("cyan"," ║")),console.log(c("cyan"," ║")+` Health score: ${c(R,(T+" ("+Math.round(N)+"/100)").padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Issues found: ${c(w.length>0?"yellow":"green",String(w.length).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Auto-fixable: ${c("green",String(F).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Scan time: ${c("gray",($+"ms").padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log(""),h.some(e=>!(e.endsWith(".js")||e.endsWith(".ts")||e.endsWith(".jsx")||e.endsWith(".tsx")||e.endsWith(".mjs")))){const e=[...new Set(h.filter(e=>!/\.(js|ts|jsx|tsx|mjs)$/.test(e)).map(e=>path.extname(e)).filter(Boolean))];console.log(c("gray"," Note: Score includes Security + Hallucination + Code Quality categories.")),console.log(c("gray",` Architecture + Dependency analysis evaluated for JS/TS only. Detected non-JS: ${e.join(", ")}`)),console.log("")}if(P.length>0){console.log(c("bgRed"," CRITICAL ")+c("red",c("bold",` ${P.length} Hallucinated API${P.length>1?"s":""}`))),console.log(c("gray"," These imports or method calls do not exist. Your code WILL crash when it hits them.")),console.log("");const e=a.showFullDetails?P.length:Math.min(a.teaserIssues,P.length);for(let o=0;o<e;o++){const e=P[o],n=e.line?`:${e.line}`:"";console.log(` ${c("cyan",e.file+n)}`);try{const o=path.resolve(l,e.file),n=fs.readFileSync(o,"utf-8").split("\n");e.line&&n[e.line-1]&&console.log(` ${c("red",n[e.line-1].trim())}`)}catch(e){}console.log(` ${c("yellow","^^^")} ${e.message}`),e.suggestion&&console.log(` ${c("green","Fix:")} ${e.suggestion}`),console.log("")}P.length>e&&(console.log(c("gray",` ... ${P.length-e} more hallucinations hidden`)),console.log(c("cyan"," Upgrade to Pro to see all: ")+c("bold","thuban upgrade")),console.log(""))}if(I.length>0){console.log(c("red",c("bold",` DEPRECATED: ${I.length} API${I.length>1?"s":""} that will break on upgrade`))),console.log(c("gray"," These work NOW but will stop working when you upgrade Node.js or your dependencies.")),console.log(c("gray"," Fix: ")+c("cyan","npx thuban fix . --fix --unsafe")),console.log("");const e=a.showFullDetails?I.length:Math.min(a.teaserIssues,I.length);for(let o=0;o<e;o++){const e=I[o],n=e.line?`:${e.line}`:"";console.log(` ${c("cyan",e.file+n)}`),console.log(` ${c("yellow",e.message)}`),e.suggestion&&console.log(` ${c("green","→")} ${e.suggestion}`),console.log("")}I.length>e&&(console.log(c("gray",` ... ${I.length-e} more hidden`)),console.log(""))}const O=w.filter(e=>"security"===e.category),j=w.filter(e=>"quality"===e.category||"performance"===e.category||"ai"===e.category||"ai_smell"===e.category);if(O.length>0){console.log(c("red",c("bold",` SECURITY: ${O.length} issue${O.length>1?"s":""}`)));const e=a.showFullDetails?Math.min(O.length,10):Math.min(a.teaserIssues,O.length);for(let o=0;o<e;o++){const e=O[o],n=e.line?`:${e.line}`:"";console.log(` ${c("red","✗")} ${c("cyan",e.file+n)} ${c("gray",e.message)}`)}O.length>e&&console.log(c("gray",` ... ${O.length-e} more hidden`)),console.log("")}if(j.length>0){const e={};for(const o of j){const n=(o.message||o.type||"other").replace(/\s*—.*$/,"").trim();e[n]||(e[n]=[]),e[n].push(o)}const o=Object.entries(e).sort((e,o)=>o[1].length-e[1].length);console.log(c("yellow",c("bold",` CODE QUALITY: ${j.length} issue${j.length>1?"s":""}`))),console.log(c("gray"," Patterns that reduce code quality, maintainability, or production readiness.")),console.log("");const n=a.showFullDetails?o.length:Math.min(3,o.length);for(let e=0;e<n;e++){const[n,l]=o[e],s="high"===l[0].severity||"error"===l[0].severity?"red":"yellow";if(console.log(` ${c(s,"!")} ${c("bold",String(l.length))} x ${n}`),l[0].file){const e=l[0].line?`:${l[0].line}`:"";console.log(` ${c("gray","e.g.")} ${c("cyan",l[0].file+e)}`)}}o.length>n&&console.log(c("gray",` ... ${o.length-n} more issue types`)),console.log("");const l=j.filter(e=>!1!==e.fixable).length;l>0&&(console.log(c("gray",` ${l} of these can be auto-fixed: `)+c("cyan","npx thuban fix . --fix --unsafe")),console.log(""))}if(console.log(c("bold"," ┌──────────────────────────────────────────────────────┐")),N<60?console.log(c("bold"," │ ")+c("red","YOUR CODEBASE HAS A SERIOUS HEALTH PROBLEM ")+c("bold"," │")):N<80?console.log(c("bold"," │ ")+c("yellow","YOUR CODEBASE NEEDS ATTENTION ")+c("bold"," │")):console.log(c("bold"," │ ")+c("green","YOUR CODEBASE IS IN GOOD SHAPE ")+c("bold"," │")),console.log(c("bold"," │")+` Score: ${c(R,T+" ("+Math.round(N)+"/100)")} `+c("bold","│")),console.log(c("bold"," └──────────────────────────────────────────────────────┘")),console.log(""),S.length>0){console.log(c("red",` ${S.length} function${S.length>1?"s":""} will crash in production.`));const e=S.length<=3?"30 seconds":S.length<=10?"2 minutes":"10 minutes";console.log(c("gray",` Estimated cleanup: ${e} with Thuban auto-fix.`)),console.log(c("gray"," Estimated cost if shipped: your weekend.")),console.log("")}if(a.showFullDetails)w.length>0&&(w.filter(e=>!1!==e.fixable).length,console.log(c("cyan"," ┌──────────────────────────────────────────────────────┐")),console.log(c("cyan"," │")+c("bold"," WHAT TO DO NEXT ")+c("cyan","│")),console.log(c("cyan"," ├──────────────────────────────────────────────────────┤")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","1. See what would be fixed (safe preview):")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix . ")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","2. Apply all safe fixes:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix . --fix")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","3. Apply fixes with git rollback:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix . --fix --commit")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Each fix = 1 git commit. Undo any with git revert.")+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","4. Generate visual report (HTML):")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban dashboard .")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Opens a dashboard you can share with your team.")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","5. Show tech debt cost in £:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban cost .")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","6. Block bad code on every commit:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban gate")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Installs a pre-commit hook. Set once, forget.")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","All commands: npx thuban --help")+" "+c("cyan","│")),console.log(c("cyan"," └──────────────────────────────────────────────────────┘")),console.log(""));else{const e=w.length-a.teaserIssues;e>0&&(console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold",` ${e} more issues hidden `)+c("cyan","║")),console.log(c("cyan"," ║")+c("gray"," Upgrade to see every issue, fix them automatically,")+c("cyan","║")),console.log(c("cyan"," ║")+c("gray"," and get the full HTML dashboard report. ")+c("cyan","║")),console.log(c("cyan"," ╠══════════════════════════════════════════════════════╣")),console.log(c("cyan"," ║")+` ${c("bold","thuban upgrade")} Open pricing page `+c("cyan","║")),console.log(c("cyan"," ║")+` ${c("bold","thuban activate")} <key> Activate a license key `+c("cyan","║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log(""));const o=t.canScan(),n=o.remaining;console.log(c("gray",` Free scans remaining this month: ${n}/${o.limit}`)),console.log("")}u&&printFileCapUpgradePrompt({scannedCount:h.length,totalCount:d,remainingSummary:y,upgradeUrl:i}),w.length>0&&!u&&(console.log(c("gray"," ─────────────────────────────────────────────────────")),console.log(c("gray"," Thuban runs 100% on your machine. No data leaves.")),console.log(c("gray"," No cloud costs. As your codebase grows, Thuban grows with it.")),console.log(""),console.log(c("gray"," Keep your codebase healthy continuously:")),console.log(c("gray"," ")+c("cyan","npx thuban gate --fix")+c("gray"," Block bad code on every commit")),console.log(c("gray"," ")+c("cyan","npx thuban watch .")+c("gray"," Scan files as you save them")),console.log(c("gray"," ")+c("cyan","npx thuban baseline .")+c("gray"," Only alert on NEW issues")),console.log(""))}async function cmdDeps(e){const o=Date.now(),n=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," MAPPING DEPENDENCIES: ")+c("cyan",n)),console.log(""));const l=collectFiles(n,e.ignore),s=new DependencyGraph({rootPath:n});await s.build(l);const t=Date.now()-o,a=s.getModuleSummary(),r=s.getMostCriticalFiles(10),i=s.getOrphanFiles(),g=s.circularDeps||[];if(e.json)console.log(JSON.stringify({elapsed_ms:t,summary:a,critical:r,orphans:i,circular:g,graph:s.toJSON()},null,2));else{if(console.log(c("gray",` Analysed ${l.length} files in ${t}ms`)),console.log(""),console.log(c("bold"," DEPENDENCY SUMMARY")),console.log(` Files: ${a.totalFiles||l.length}`),console.log(` Import links: ${a.totalImports||0}`),console.log(` Circular deps: ${g.length>0?c("red",g.length):c("green","0")}`),console.log(` Orphan files: ${i.length>0?c("yellow",i.length):c("green","0")}`),console.log(""),r.length>0){console.log(c("bold"," MOST CRITICAL FILES")+c("gray"," (most dependents)"));for(const e of r){const o="string"==typeof e?e:e.file||e.path||String(e),l=path.relative(n,o),s="object"==typeof e&&(e.dependents||e.score||e.count)||"?";console.log(` ${c("cyan",l)} ${c("gray","→ "+s+" dependents")}`)}console.log("")}if(g.length>0){console.log(c("red"," CIRCULAR DEPENDENCIES"));for(const e of g.slice(0,5)){const o=Array.isArray(e)?e.map(e=>path.relative(n,e)).join(" → "):String(e);console.log(` ${c("yellow","⟲")} ${o}`)}console.log("")}if(i.length>0){console.log(c("yellow"," ORPHAN FILES")+c("gray"," (no imports, no exports consumed)"));for(const e of i.slice(0,10)){const o="string"==typeof e?e:e.file||e.path||String(e);console.log(` ${c("gray","○")} ${path.relative(n,o)}`)}i.length>10&&console.log(c("gray",` ... and ${i.length-10} more`)),console.log("")}}}async function cmdMonitor(e){requireProFeature("Scheduled monitoring");const o=path.resolve(e.targetPath),n=new MonitorService,l=n.configureRepo({repoPath:o,frequency:e.frequency,intervalMs:resolveIntervalMs(e.frequency,e.intervalMs),notification:{channels:String(e.notify||"inbox").split(",").map(e=>e.trim()).filter(Boolean)}});if(e.once){const o=await n.runRepoScan(l);return e.json?void console.log(JSON.stringify(o.run,null,2)):(console.log(""),console.log(c("bold"," THUBAN MONITOR RUN")),console.log(` Repo: ${l.repoPath}`),console.log(` New issues: ${o.diff.added.length}`),console.log(` Resolved issues: ${o.diff.resolved.length}`),console.log(` History saved: ${o.runPath}`),void console.log(""))}console.log(""),console.log(c("bold"," THUBAN MONITOR ACTIVE")),console.log(` Repo: ${l.repoPath}`),console.log(` Frequency: ${l.frequency} (${l.intervalMs}ms)`),console.log(` Notifications: ${(l.notification.channels||[]).join(", ")}`),console.log(c("gray"," Press Ctrl+C to stop.")),console.log(""),n.start(l),process.on("SIGINT",()=>{n.stopAll(),process.exit(0)})}async function cmdHistory(e){requireProFeature("Scan history");const o=new MonitorStore,n=resolveRepoConfigOrExit(o,e.targetPath),l=o.listRuns(n.repoId);if(e.json)return void console.log(JSON.stringify({repo:n,runs:l},null,2));console.log(""),console.log(c("bold"," THUBAN HISTORY")),console.log(` Repo: ${n.repoPath}`),console.log(` Runs: ${l.length}`),console.log("");for(const e of l.slice(-10).reverse())console.log(` ${c("cyan",e.timestamp)} issues=${e.metrics.issueCount} new=${e.diff?.added?.length||0} resolved=${e.diff?.resolved?.length||0}`);const s=o.getNotifications(10).filter(e=>e.repoId===n.repoId);if(s.length){console.log(""),console.log(c("bold"," RECENT ALERTS"));for(const e of s)console.log(` ${c("yellow",e.timestamp)} ${e.summary}`)}console.log("")}async function cmdTrend(e){requireProFeature("Trend reporting");const o=new MonitorStore,n=resolveRepoConfigOrExit(o,e.targetPath),l=o.listRuns(n.repoId).map(e=>({timestamp:e.timestamp,issueCount:e.metrics.issueCount,hallucinations:e.metrics.hallucinations,secrets:e.metrics.secrets,techDebtScore:e.metrics.techDebtScore,techDebtHours:e.metrics.techDebtHours}));if(e.json)console.log(JSON.stringify({repo:n,trend:l},null,2));else{console.log(""),console.log(c("bold"," THUBAN TREND")),console.log(` Repo: ${n.repoPath}`),console.log("");for(const e of l.slice(-12))console.log(` ${c("cyan",e.timestamp)} issues=${e.issueCount} hallucinations=${e.hallucinations} secrets=${e.secrets} debtScore=${e.techDebtScore??"n/a"} debtHours=${e.techDebtHours??"n/a"}`);console.log("")}}async function cmdDrift(e){const o=Date.now(),n=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," DETECTING DRIFT: ")+c("cyan",n)),console.log(""));const l=collectFiles(n,e.ignore),s=new DriftDetector({rootPath:n}),t=[];let a=0,r=0;for(const e of l)try{const o=await s.analyzeFile(e);o&&o.hasWidget&&(a++,o.drifts&&o.drifts.length>0&&(r++,t.push({file:path.relative(n,e),...o})))}catch(e){}const i=Date.now()-o;if(e.json)return void console.log(JSON.stringify({elapsed_ms:i,total:l.length,annotated:a,drifted:r,results:t},null,2));if(console.log(c("gray",` Checked ${l.length} files in ${i}ms`)),console.log(""),console.log(` Files with Mother Code: ${c("cyan",a)}`),console.log(` Files without: ${c("yellow",l.length-a)}`),console.log(` Files with drift: ${r>0?c("red",r):c("green","0")}`),console.log(""),t.length>0){console.log(c("bold"," DRIFT DETECTED"));for(const e of t.slice(0,20)){console.log(` ${c("yellow","⚠")} ${c("cyan",e.file)}`);for(const o of e.drifts)console.log(` ${c("gray","→")} ${o.type}: ${o.message||o.detail||JSON.stringify(o)}`)}console.log("")}const g=l.length>0?Math.round(a/l.length*100):0,d=g>=80?"green":g>=40?"yellow":"red";console.log(` Mother Code coverage: ${c(d,g+"%")}`),console.log("")}async function cmdInject(e){const o=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," INJECTING MOTHER CODE: ")+c("cyan",o)),console.log(""));const n=collectFiles(o,e.ignore).filter(e=>e.endsWith(".js")||e.endsWith(".ts")||e.endsWith(".jsx")||e.endsWith(".tsx")||e.endsWith(".py")||e.endsWith(".java")||e.endsWith(".cs")||e.endsWith(".go")||e.endsWith(".kt")||e.endsWith(".rs")||e.endsWith(".php")||e.endsWith(".rb")),l=new DependencyGraph({rootPath:o});await l.build(n);const s=new WidgetGenerator({rootPath:o,dependencyGraph:l});let t=0,a=0;const r=[];for(const l of n)try{const n=fs.readFileSync(l,"utf-8");if(n.includes("@purpose")&&n.includes("@module")){a++;continue}const i=await s.generateWidget(l,n);if(!i){a++;continue}const g=i&&i.widgetString?i.widgetString:String(i),d=path.extname(l).toLowerCase();let h;if(h=".py"===d||".rb"===d?g.replace(/^\/\*\*\n/,"").replace(/\s*\*\/\s*$/,"").split("\n").map(e=>e.replace(/^\s*\*\s?/,"# ")).join("\n"):".go"===d||".rs"===d?g.replace(/^\/\*\*\n/,"").replace(/\s*\*\/\s*$/,"").split("\n").map(e=>e.replace(/^\s*\*\s?/,"// ")).join("\n"):g,e.fix){const s=h+"\n\n"+n;fs.writeFileSync(l,s,"utf-8"),t++,e.json||console.log(` ${c("green","✓")} ${path.relative(o,l)}`)}else r.push({file:path.relative(o,l),widget:h}),t++}catch(e){a++}if(e.json)console.log(JSON.stringify({injected:t,skipped:a,dryRun:!e.fix,previews:r},null,2));else{if(console.log(""),e.fix)console.log(c("green",` ✓ Injected Mother Code into ${t} files`));else if(console.log(c("yellow",` DRY RUN — ${t} files would be annotated. Use --fix to apply.`)),e.verbose&&r.length>0){console.log("");for(const e of r.slice(0,5))console.log(` ${c("cyan",e.file)}:`),console.log(c("gray",e.widget.split("\n").map(e=>" "+e).join("\n"))),console.log("")}console.log(` ${c("gray",`Skipped: ${a} (already annotated or unsupported)`)}`),console.log("")}}async function cmdReport(e){const o=Date.now(),n=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," FULL REPORT: ")+c("cyan",n)),console.log(""));const l=collectFiles(n,e.ignore),s=new CodeScanner({rootPath:n}),t=new DependencyGraph({rootPath:n}),a=new DriftDetector({rootPath:n}),[r]=await Promise.all([s.scanFiles(l),t.build(l)]);let i=0,g=0;for(const e of l)try{const o=await a.analyzeFile(e);o&&o.hasWidget&&(g++,o.drifts&&o.drifts.length>0&&i++)}catch(e){}const d=Date.now()-o;let h=0,u=0,y=0;for(const e of r.issues||[])h++,"critical"===e.severity&&u++,"high"===e.severity&&y++;const p=t.getOrphanFiles(),f=t.circularDeps||[],m=l.length>0?Math.round(g/l.length*100):0,b=Math.max(0,Math.round(100-15*u-5*y-10*f.length-1*p.length-.1*(100-m)-3*i));if(e.json)return void console.log(JSON.stringify({elapsed_ms:d,files:l.length,health_score:b,issues:{total:h,critical:u,high:y},dependencies:{circular:f.length,orphans:p.length},mother_code:{annotated:g,coverage_pct:m,drifted:i}},null,2));const $=b>=80?"green":b>=60?"yellow":"red";console.log(c("bold"," ╔══════════════════════════════════════╗")),console.log(c("bold"," ║ THUBAN HEALTH REPORT ║")),console.log(c("bold"," ╚══════════════════════════════════════╝")),console.log(""),console.log(` ${c("bold","Health Score:")} ${c($,b+"/100")}`),console.log(` ${c("bold","Files Scanned:")} ${l.length}`),console.log(` ${c("bold","Time:")} ${d}ms`),console.log(""),console.log(c("bold"," ── Code Quality ──────────────────────")),console.log(` Total issues: ${h}`),console.log(` Critical: ${u>0?c("red",u):c("green","0")}`),console.log(` High: ${y>0?c("red",y):c("green","0")}`),console.log(""),console.log(c("bold"," ── Dependencies ─────────────────────")),console.log(` Circular deps: ${f.length>0?c("red",f.length):c("green","0")}`),console.log(` Orphan files: ${p.length>0?c("yellow",p.length):c("green","0")}`),console.log(""),console.log(c("bold"," ── Mother Code ──────────────────────")),console.log(` Annotated files: ${g}`),console.log(` Coverage: ${c(m>=80?"green":m>=40?"yellow":"red",m+"%")}`),console.log(` Drifted files: ${i>0?c("yellow",i):c("green","0")}`),console.log("");const w=b>=90?"A":b>=80?"B":b>=70?"C":b>=60?"D":"F",v=b>=80?"green":b>=60?"yellow":"red";console.log(` ${c("bold","Grade:")} ${c(v,w)}`),console.log(""),b<80&&(console.log(c("bold"," RECOMMENDATIONS:")),u>0&&console.log(` ${c("red","→")} Fix ${u} critical issues immediately`),f.length>0&&console.log(` ${c("yellow","→")} Resolve ${f.length} circular dependencies`),m<50&&console.log(` ${c("yellow","→")} Run ${c("cyan","thuban inject --fix")} to improve Mother Code coverage`),i>0&&console.log(` ${c("yellow","→")} ${i} files have drifted from their annotations`),p.length>5&&console.log(` ${c("gray","→")} Consider removing ${p.length} orphan files`),console.log(""))}async function cmdDebt(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore);e.json||(banner(),console.log(c("bold"," TECH DEBT ANALYSIS: ")+c("cyan",o)),console.log(c("gray",` Scanning ${n.length} files...`)),console.log(""));const l=new TechDebtAnalyzer({rootPath:o}),s=await l.analyze(n);e.json?console.log(JSON.stringify(l.toJSON(s),null,2)):console.log(l.formatReport(s))}async function cmdFix(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore),{execSync:l}=require("child_process");e.json||(banner(),console.log(c("bold"," AUTO-FIX TECH DEBT: ")+c("cyan",o)),console.log(""));const s=new TechDebtAnalyzer({rootPath:o});if(!e.fix){const o=await s.fix(n,{dryRun:!0});if(e.json)console.log(JSON.stringify(o,null,2));else{console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," DRY RUN — no files will be changed ")+c("cyan","║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log(""),console.log(` Fixable items: ${c("bold",o.totalFixable)}`),console.log("");const e={},n=["break_circular","extract_to_env","update_deprecated_api","wrap_localhost_url"];let l=0,s=0;for(const c of o.fixes){const o=c.fixAction||"unknown";e[o]||(e[o]=0),e[o]++,n.includes(o)?s++:l++}console.log(c("green",c("bold",` SAFE FIXES (${l})`))+c("gray"," — no runtime behaviour change"));for(const[o,l]of Object.entries(e).sort((e,o)=>o[1]-e[1])){if(n.includes(o))continue;const e={inject_widget:"Inject Mother Code DNA annotations",regenerate_widget:"Update stale annotations",remove_console_log:"Remove debug console.logs",flag_for_removal:"Confirm orphan files for removal"}[o]||o;console.log(` ${c("green","→")} ${e}: ${c("bold",l)}`)}if(console.log(""),s>0){console.log(c("yellow",c("bold",` UNSAFE FIXES (${s})`))+c("gray"," — may change runtime behaviour"));for(const[o,l]of Object.entries(e).sort((e,o)=>o[1]-e[1])){if(!n.includes(o))continue;const e={break_circular:"Restructure circular dependencies",extract_to_env:"Extract hardcoded secrets to env vars",update_deprecated_api:"Replace deprecated API calls with modern equivalents",wrap_localhost_url:"Wrap hardcoded localhost URLs in env var fallback"}[o]||o;console.log(` ${c("yellow","!")} ${e}: ${c("bold",l)}`)}console.log(""),console.log(c("gray"," Unsafe fixes require --unsafe flag:")),console.log(` ${c("cyan","thuban fix [path] --fix --unsafe")}`),console.log("")}console.log(c("bold"," TO APPLY:")),console.log(` ${c("cyan","thuban fix [path] --fix")} Apply safe fixes only`),console.log(` ${c("cyan","thuban fix [path] --fix --unsafe")} Apply all fixes`),console.log(` ${c("cyan","thuban fix [path] --fix --commit")} Each fix = separate git commit`),console.log("")}return}e.json||(e.safe?console.log(c("green"," SAFE MODE")+c("gray"," — only non-runtime-changing fixes will be applied")):console.log(c("yellow"," UNSAFE MODE")+c("gray"," — all fixes including runtime changes")),console.log(""));const t=await s.fix(n,{dryRun:!1,safeOnly:e.safe});if(e.gitCommit&&t.fixed>0)try{l("git rev-parse --git-dir",{cwd:o,stdio:"pipe"}),l("git add -A",{cwd:o,stdio:"pipe"});const n=`fix(thuban): auto-fix ${t.fixed} issues [safe=${e.safe}]`;l(`git commit -m "${n}"`,{cwd:o,stdio:"pipe"}),e.json||(console.log(c("green",` ✓ Changes committed: "${n}"`)),console.log(c("gray"," Revert with: git revert HEAD")),console.log(""))}catch(o){e.json||(console.log(c("yellow"," ⚠ Could not auto-commit: "+(o.message||"not a git repo"))),console.log(""))}if(e.json)console.log(JSON.stringify(t,null,2));else{console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," FIX RESULTS ")+c("cyan","║")),console.log(c("cyan"," ╠══════════════════════════════════════════════════════╣"));const o=(t.fixes||[]).filter(e=>"fixed"===e.status&&("inject_widget"===e.fixAction||"regenerate_widget"===e.fixAction)).length,n=t.fixed-o;o>0&&console.log(c("cyan"," ║")+` Annotations added: ${c("green",String(o).padEnd(19))}`+c("cyan"," ║")),n>0&&console.log(c("cyan"," ║")+` Issues fixed: ${c("green",String(n).padEnd(20))}`+c("cyan"," ║")),0===o&&0===n&&console.log(c("cyan"," ║")+` Fixed: ${c("green",String(t.fixed).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Flagged for review: ${c("yellow",String(t.flagged).padEnd(19))}`+c("cyan"," ║")),t.failed>0&&console.log(c("cyan"," ║")+` Failed: ${c("red",String(t.failed).padEnd(20))}`+c("cyan"," ║")),t.skipped>0&&console.log(c("cyan"," ║")+` Skipped: ${c("gray",String(t.skipped).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log(""),t.fixed>0&&(console.log(c("green"," Your files have been updated. Changes are saved to disk.")),e.gitCommit||console.log(c("gray"," Tip: use --commit next time to auto-commit each fix for easy rollback.")),console.log(""),console.log(c("gray"," Verify the changes:")),console.log(c("gray"," ")+c("cyan","npx thuban scan .")+c("gray"," Re-scan to see your new score")),console.log(c("gray"," ")+c("cyan","npx thuban dashboard .")+c("gray"," Generate updated report")),console.log("")),t.skipped>0&&(console.log(c("gray",` ${t.skipped} items were skipped (no auto-fix available or pattern not matched).`)),console.log(c("gray"," Review them manually or run: ")+c("cyan","npx thuban dashboard .")),console.log("")),t.flagged>0&&(console.log(c("gray",` ${t.flagged} items need human review (secrets, orphan files, complex patterns).`)),console.log(c("gray"," See details: ")+c("cyan","npx thuban dashboard .")),console.log(""))}}async function cmdHallucinate(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore);e.json||(banner(),console.log(c("bold"," HALLUCINATION SCAN: ")+c("cyan",o)),console.log(c("gray",` Scanning ${n.length} files for AI hallucinations...`)),console.log(""));const l=new HallucinationDetector({rootPath:o}),s=await l.scan(n);e.json?console.log(JSON.stringify(s,null,2)):console.log(l.formatReport(s))}async function cmdWatch(e){const o=path.resolve(e.targetPath),n=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),c=new HallucinationDetector({rootPath:o}),l=new FileWatcher({rootPath:o,ignorePatterns:["node_modules",".git","dist",".next","__pycache__",...e.ignore]});process.on("SIGINT",()=>{l.stop(),process.exit(0)}),l.start(async e=>{const o=[],l=await n.scanFile(e);l&&o.push(...l);const s=await c.scan([e]);if(s.phantomAPIs.length>0)for(const e of s.phantomAPIs)o.push({severity:"high",message:`Phantom API: ${e.name} — ${e.suggestion}`,line:e.line});if(s.phantomImports.length>0)for(const e of s.phantomImports)o.push({severity:"critical",message:`Phantom import: ${e.module} — ${e.reason}`,line:e.line});if(s.aiSmells.length>0)for(const e of s.aiSmells)o.push({severity:"warning",message:`AI smell: ${e.message}`,line:e.line});return o})}async function cmdDashboard(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore);e.json||(banner(),console.log(c("bold"," GENERATING DASHBOARD: ")+c("cyan",o)),console.log(c("gray",` Scanning ${n.length} files...`)),console.log(""));const l=new TechDebtAnalyzer({rootPath:o}),s=await l.analyze(n),t=new DependencyGraph({rootPath:o});await t.build(n);const a={circular:t.circularDeps||[],orphans:t.getOrphanFiles()||[],critical:t.getMostCriticalFiles(5)||[]},r=new HallucinationDetector({rootPath:o}),i=await r.scan(n),g=new HtmlReportGenerator({rootPath:o,outputDir:o}),d=path.basename(o),h=await g.writeReport(s,a,{projectName:d,hallucinations:i});e.json?console.log(JSON.stringify({outputPath:h,scores:s.scores,stats:s.stats},null,2)):(console.log(c("green"," ✓ Dashboard generated:")),console.log(` ${c("cyan",h)}`),console.log(""),console.log(c("gray"," Open in your browser to view the interactive report")),console.log(""))}async function cmdDiff(e){const o=path.resolve(e.targetPath),{execSync:n}=require("child_process");e.json||(banner(),console.log(c("bold"," DIFF SCAN: ")+c("cyan",o)));let l=[];try{let e="main";try{n("git rev-parse --verify main",{cwd:o,stdio:"pipe"})}catch{try{n("git rev-parse --verify master",{cwd:o,stdio:"pipe"}),e="master"}catch{e="HEAD~1"}}const c=n(`git diff --name-only ${e}...HEAD`,{cwd:o,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim(),s=n("git diff --name-only HEAD",{cwd:o,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim(),t=n("git ls-files --others --exclude-standard",{cwd:o,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim(),a=new Set([...c.split("\n").filter(Boolean),...s.split("\n").filter(Boolean),...t.split("\n").filter(Boolean)]);for(const e of a){const n=path.resolve(o,e),c=path.extname(e).toLowerCase();[".js",".ts",".jsx",".tsx",".mjs",".cjs",".json"].includes(c)&&fs.existsSync(n)&&l.push(n)}}catch(o){e.json||console.log(c("red",` ✗ Not a git repository or git error: ${o.message}`)),process.exit(1)}if(0===l.length)return void(e.json?console.log(JSON.stringify({changedFiles:0,issues:0})):console.log(c("green"," ✓ No changed files to scan")));e.json||console.log(c("gray",` Scanning ${l.length} changed file${1===l.length?"":"s"}...\n`));const s=new CodeScanner({rootPath:o}),t=new HallucinationDetector({rootPath:o}),a=await s.scanFiles(l),r=await t.scan(l);let i=0;const g={};for(const e of a.issues||[]){const n=path.relative(o,e.file);g[n]||(g[n]=[]),g[n].push(e),i++}if(i+=r.stats.totalIssues,e.json)console.log(JSON.stringify({changedFiles:l.length,totalIssues:i,codeIssues:g,hallucinations:r},null,2));else{for(const[e,o]of Object.entries(g)){console.log(` ${c("cyan",e)}`);for(const e of o.slice(0,5)){const o="critical"===e.severity||"high"===e.severity?"red":"warning"===e.severity?"yellow":"gray";console.log(` ${c(o,"✗")} ${c("gray",e.message||e.id||"")}`)}o.length>5&&console.log(` ${c("gray",`... and ${o.length-5} more`)}`),console.log("")}r.stats.totalIssues>0&&console.log(t.formatReport(r));const e=0===i?"green":"yellow";console.log(` ${c(e,`${l.length} files changed, ${i} new issues introduced`)}\n`)}}async function cmdActivate(e){banner();const o=new LicenseManager,n=e.licenseKey;if(!n)return console.log(c("red"," Usage: thuban activate <license-key>")),console.log(""),console.log(c("gray"," Get a key at: ")+c("cyan","https://thuban.dev/pricing")),void console.log("");const l=o.activate(n);l.success?(console.log(c("green"," ╔══════════════════════════════════════╗")),console.log(c("green"," ║")+c("bold"," License activated! ")+c("green","║")),console.log(c("green"," ╚══════════════════════════════════════╝")),console.log(""),console.log(` Tier: ${c("bold",l.tierName)}`),console.log(` Status: ${c("green","Active")}`),console.log(""),console.log(c("gray"," All features unlocked. Run ")+c("cyan","thuban scan .")+c("gray"," to get started.")),console.log("")):(console.log(c("red"," Invalid license key.")),console.log(""),console.log(c("gray"," Check for typos or get a new key at: ")+c("cyan","https://thuban.dev/pricing")),console.log(""))}async function cmdStatus(e){banner();const o=(new LicenseManager).getStatus();console.log(c("bold"," LICENSE STATUS")),console.log(""),console.log(` Tier: ${c("bold",o.tierName)}${o.licensed?c("green"," (active)"):""}`),console.log(` Machine ID: ${c("gray",o.machineId)}`),console.log(` Scans this month: ${c("bold",o.scansUsed)}${o.scansLimit<1/0?c("gray"," / "+o.scansLimit):""}`),console.log(` Lifetime scans: ${c("gray",o.totalScans)}`),console.log(""),console.log(c("bold"," FEATURES")),console.log(` Full scan details: ${o.features.fullDetails?c("green","Yes"):c("red","No")}`),console.log(` HTML dashboard: ${o.features.dashboard?c("green","Yes"):c("red","No")}`),console.log(` Auto-fix: ${o.features.autoFix?c("green","Yes"):c("red","No")}`),console.log(` CI Action: ${o.features.ciAction?c("green","Yes"):c("red","No")}`),console.log(""),o.licensed||(console.log(c("cyan"," Upgrade at: ")+c("bold","https://thuban.dev/pricing")),console.log(""))}function cmdUpgrade(){banner(),console.log(c("bold"," THUBAN PRICING")),console.log(""),console.log(` ${c("gray","FREE")} ${c("bold","£0/mo")}`),console.log(" 5 scans/month, 100 files, summary only"),console.log(""),console.log(` ${c("cyan","PRO")} ${c("bold","£19/mo")} ${c("gray","(or £149/year — save 35%)")}`),console.log(" Unlimited scans, full details, auto-fix, HTML dashboard"),console.log(""),console.log(` ${c("cyan","TEAM")} ${c("bold","£99/mo")}`),console.log(" Everything in Pro + 5 seats, CI Action, team reports"),console.log(""),console.log(` ${c("cyan","ENTERPRISE")} ${c("bold","£299/mo")}`),console.log(" Everything in Team + unlimited seats, priority support, SLA"),console.log(""),console.log(c("bold"," Subscribe at: ")+c("cyan","https://thuban.dev/pricing")),console.log("");try{const{exec:e}=require("child_process"),o=process.platform;e(("win32"===o?"start":"darwin"===o?"open":"xdg-open")+" https://thuban.dev/pricing")}catch(e){}}async function cmdGate(e){const o=path.resolve(e.targetPath);if(e.fix){const e=PreCommitGate.install(o);return banner(),e.success?(console.log(c("green"," ✓ Pre-commit gate installed!")),console.log(""),console.log(c("gray"," Every commit will now be checked for hallucinated APIs.")),console.log(c("gray"," Commits with phantom APIs will be blocked automatically.")),console.log(""),console.log(` To remove: ${c("cyan","thuban gate --uninstall")}`)):console.log(c("red",` Error: ${e.error}`)),void console.log("")}if(process.argv.includes("--uninstall")){const e=PreCommitGate.uninstall(o);return banner(),console.log(c("green",` ✓ ${e.message}`)),void console.log("")}const n=new PreCommitGate({rootPath:o,strict:!process.argv.includes("--warn")}).run();if(e.json)return void console.log(JSON.stringify(n,null,2));const l=process.argv.includes("--ci");if(l||banner(),0===n.files)return console.log(c("gray"," No staged files to check.")),console.log(""),console.log(` Install the gate: ${c("cyan","thuban gate --fix")}`),void console.log("");if(n.passed)console.log(c("green",` ✓ ${n.message}`)),console.log(c("gray",` ${n.elapsed}ms`)),l||console.log(""),process.exit(0);else{console.log(c("red",` ✗ ${n.message}`)),console.log("");for(const e of n.issues)console.log(` ${c("red",e.file)}:${e.line}`),console.log(` ${c("yellow",e.code)}`),console.log(` ${c("red","^^^")} ${e.message}`),e.fix&&console.log(` ${c("green","Fix:")} ${e.fix}`),console.log("");process.exit(1)}}async function cmdCost(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," SCANNING: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),s=new HallucinationDetector({rootPath:o}),[t,a]=await Promise.all([l.scanFiles(n),s.scan(n)]),r=[];for(const e of t.issues||[])r.push({file:path.relative(o,e.file),...e});for(const e of a.phantomAPIs)r.push({category:"hallucination",severity:"critical",message:e.name});for(const e of a.deprecatedAPIs)r.push({category:"deprecated",severity:"high",message:e.name});const i=new TechDebtCostCalculator,g=i.calculate(r,n.length);e.json?console.log(JSON.stringify(g,null,2)):console.log(i.formatCLI(g))}async function cmdGhosts(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," SCANNING FOR GHOST CODE: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new GhostCodeDetector({rootPath:o}).scan(n);if(e.json)console.log(JSON.stringify(l,null,2));else{if(0===l.ghosts.length)return console.log(c("green"," No ghost code detected. Every function is referenced.")),void console.log("");console.log(c("yellow",c("bold",` GHOST CODE: ${l.totalGhosts} function${l.totalGhosts>1?"s":""} exist but are never called`))),console.log("");for(const o of l.ghosts.slice(0,e.maxIssues)){const e="high"===o.severity?"red":"yellow";console.log(` ${c(e,"⊘")} ${c("cyan",o.file)}:${o.line}`),console.log(` ${c("bold",o.name+"()")} — ${o.wastedLines} lines, ${o.references} references`),console.log("")}console.log(c("bold"," ┌──────────────────────────────────────────────────────┐")),console.log(c("bold"," │")+` Wasted code: ${c("red",l.totalWastedLines+" lines")} (${l.wastedPercent}% of codebase) `+c("bold","│")),console.log(c("bold"," └──────────────────────────────────────────────────────┘")),console.log("")}}async function cmdAIScore(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," AI CONFIDENCE SCORING: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new AIConfidenceScorer({rootPath:o}).scan(n);if(e.json)console.log(JSON.stringify(l,null,2));else{if(0===l.highConfidence.length&&0===l.mediumConfidence.length)return console.log(c("green"," No AI-generated code patterns detected.")),void console.log("");console.log(c("red",c("bold",` HIGH CONFIDENCE AI-GENERATED (${l.highConfidence.length} functions)`))),console.log("");for(const e of l.highConfidence.slice(0,15)){console.log(` ${c("red",e.confidence+"%")} AI ${c("cyan",e.file)}:${e.line}`),console.log(` ${c("bold",e.name+"()")} — ${e.lineCount} lines | Test coverage: ${e.testCoverage}`);for(const o of e.signals.slice(0,2))console.log(` ${c("gray","• "+o)}`);console.log("")}if(l.mediumConfidence.length>0){console.log(c("yellow",c("bold",` POSSIBLE AI-GENERATED (${l.mediumConfidence.length} functions)`))),console.log("");for(const e of l.mediumConfidence.slice(0,10))console.log(` ${c("yellow",e.confidence+"%")} AI ${c("cyan",e.file)}:${e.line} ${c("gray",e.name+"()")}`);console.log("")}console.log(c("bold"," ┌──────────────────────────────────────────────────────┐")),console.log(c("bold"," │")+` ${c("red",l.aiPercentage+"%")} of functions are likely AI-generated `+c("bold","│")),console.log(c("bold"," │")+` ${c("yellow",l.aiLikelyCount)} high confidence | ${l.aiPossibleCount} possible `+c("bold","│")),console.log(c("bold"," └──────────────────────────────────────────────────────┘")),console.log("")}}async function cmdClones(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," COPY-PASTE DRIFT DETECTION: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new CopyPasteDriftDetector({rootPath:o}).scan(n);if(e.json)console.log(JSON.stringify(l,null,2));else{if(0===l.clusters.length)return console.log(c("green"," No copy-paste drift detected.")),void console.log("");console.log(c("yellow",c("bold",` COPY-PASTE CLUSTERS: ${l.totalClusters} found`))),console.log("");for(const e of l.clusters.slice(0,10)){const o="high"===e.severity?"red":"yellow";console.log(` ${c(o,"◈")} ${c("bold",e.pattern+" pattern")} — ${e.totalInstances} instances`);for(const o of e.members){const e=o.similarity<100?c("gray",` (${o.similarity}% similar)`):"";console.log(` ${c("cyan",o.file)}:${o.line} ${c("gray",o.name+"()")}${e}`)}console.log(` ${c("green","→")} ${e.recommendation}`),console.log("")}console.log(c("bold",` Total duplicate lines: ${c("red",l.totalDuplicateLines)}`)),console.log(c("gray",` ${l.recommendation}`)),console.log("")}}async function cmdPassport(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," GENERATING CODEBASE PASSPORT: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new CodebasePassport({rootPath:o}),s=l.generate(n);if(e.json)return void console.log(JSON.stringify(s,null,2));const t=l.save(s),a=s.project,r=s.onboarding;console.log(c("green",` ✓ Passport generated: ${c("bold",path.relative(o,t))}`)),console.log(""),console.log(c("bold"," PROJECT IDENTITY")),console.log(` Name: ${c("cyan",a.name)}`),console.log(` Files: ${c("bold",a.totalFiles)}`),console.log(` Lines: ${c("bold",a.totalLines.toLocaleString())}`),console.log(` Architecture: ${c("bold",a.architecture)}`),console.log(` Frameworks: ${a.frameworks.join(", ")||"vanilla"}`),console.log(""),console.log(c("bold"," LANGUAGES"));for(const e of a.languages.slice(0,5)){const o="█".repeat(Math.max(1,Math.round(e.percentage/5)));console.log(` ${e.extension.padEnd(6)} ${c("cyan",o)} ${e.percentage}% (${e.lines} lines)`)}if(console.log(""),console.log(c("bold"," ONBOARDING")),console.log(` Read time: ~${r.estimatedReadTimeMinutes} minutes`),console.log(` Start here: ${r.startHere.slice(0,3).join(", ")}`),r.doNotTouch.length>0&&console.log(` Do not touch: ${c("red",r.doNotTouch.join(", "))}`),console.log(` Summary: ${r.quickSummary}`),console.log(""),s.sacred.length>0){console.log(c("bold"," SACRED FILES (high impact if changed)"));for(const e of s.sacred.slice(0,5))console.log(` ${c("red","!")} ${c("cyan",e.file)} — imported by ${e.importedBy} files`);console.log("")}console.log(c("gray",` Full passport: ${t}`)),console.log(c("gray"," Share with new team members for instant onboarding.")),console.log("")}async function cmdExecutive(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," GENERATING EXECUTIVE REPORT: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore);console.log(c("gray",` Scanning ${n.length} files...`));const l=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),s=new HallucinationDetector({rootPath:o}),t=new GhostCodeDetector({rootPath:o}),a=new AIConfidenceScorer({rootPath:o}),r=new CopyPasteDriftDetector({rootPath:o}),[i,g,d,h,u]=await Promise.all([l.scanFiles(n),s.scan(n),t.scan(n),a.scan(n),r.scan(n)]);let y=0;for(const e of n)try{y+=fs.readFileSync(e,"utf-8").split("\n").length}catch{}const p=[];for(const e of i.issues||[])p.push({file:path.relative(o,e.file),...e});for(const e of g.phantomAPIs)p.push({category:"hallucination",severity:"critical",message:e.name});for(const e of g.deprecatedAPIs)p.push({category:"deprecated",severity:"high",message:e.name});const f=(new TechDebtCostCalculator).calculate(p,n.length),m=new ExecutiveReport({rootPath:o,companyName:e.companyName||void 0}),b=m.generate({scanResults:i,hallResults:g,debtCost:f,ghostResults:d,aiScoreResults:h,cloneResults:u,fileCount:n.length,lineCount:y}),$=m.save(b,o);console.log(c("green"," ✓ Executive report generated!")),console.log(""),console.log(` ${c("bold","Report:")} ${c("cyan",$)}`),console.log(""),console.log(c("gray"," Open in browser → Ctrl+P → Save as PDF")),console.log(c("gray",' Or click the "Save as PDF" button in the report.')),console.log("");try{const{exec:e}=require("child_process"),o=process.platform;e(`${"win32"===o?"start":"darwin"===o?"open":"xdg-open"} "${$}"`),console.log(c("green"," ✓ Opened in your default browser.")),console.log("")}catch(e){}}async function cmdBaseline(e){const o=path.resolve(e.targetPath),n=new BaselineManager(o);if(banner(),e.baselineCreate||!n.exists()){console.log(c("bold"," CREATING BASELINE: ")+c("cyan",o)),console.log("");const l=collectFiles(o,e.ignore),s=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),t=new HallucinationDetector({rootPath:o}),[a,r]=await Promise.all([s.scanFiles(l),t.scan(l)]),i=[];for(const e of a.issues||[])i.push({file:path.relative(o,e.file),...e});for(const e of r.phantomAPIs)i.push({file:e.file,category:"hallucination",severity:"critical",id:e.id,message:e.name});for(const e of r.deprecatedAPIs)i.push({file:e.file,category:"deprecated",severity:"high",id:e.id,message:e.name});for(const e of r.aiSmells)i.push({file:e.file,category:"ai_smell",severity:"warning",id:e.id,message:e.message});const g=n.create(i);console.log(c("green",` ✓ Baseline created: ${c("bold",g.count)} known issues recorded`)),console.log(` ${c("cyan",g.path)}`),console.log(""),console.log(c("gray"," Future scans with --baseline will only report NEW issues.")),console.log(c("gray"," Commit .thuban-baseline.json to share baseline with your team.")),console.log("")}else{const e=n.load();console.log(c("bold"," BASELINE STATUS")),console.log(""),console.log(` Created: ${c("cyan",e.created)}`),console.log(` Known issues: ${c("bold",e.issueCount)}`),console.log(""),console.log(c("gray"," Use --baseline-create to update the baseline")),console.log(c("gray"," Use --baseline with scan to only see new issues")),console.log("")}}async function cmdCrucible(e){const o=process.argv.slice(2),n=o[1]||"",l=path.resolve(e.targetPath);if("ci"===n){const e=o[2]||"",n=CIIntegration.listPlatforms();if(!e||!n.includes(e))return console.log(""),console.log(c("bold"," CRUCIBLE CI INTEGRATION")),console.log(""),console.log(c("gray"," Usage:")),console.log(` ${c("cyan","thuban crucible ci github")} → .github/workflows/thuban-crucible.yml`),console.log(` ${c("cyan","thuban crucible ci gitlab")} → .gitlab-ci.yml`),console.log(` ${c("cyan","thuban crucible ci bitbucket")} → bitbucket-pipelines.yml`),console.log(""),e&&(console.log(c("red",` Unknown platform: ${e}`)),console.log(c("gray",` Supported: ${n.join(", ")}`))),void console.log("");const s=o.find(e=>e.startsWith("--threshold=")),t=o.find(e=>e.startsWith("--level=")),a=o.find(e=>e.startsWith("--node=")),r=s?parseInt(s.split("=")[1],10):70,i=t?t.split("=")[1]:"gentle",g=a?a.split("=")[1]:"20";try{const o=CIIntegration.generateCIConfig(e,l,{threshold:r,level:i,nodeVersion:g});console.log(""),console.log(c("green"," ✓ CI config generated")),console.log(` ${c("cyan",o.filename)}`),console.log(""),console.log(c("gray",` Config: threshold=${o.threshold}, level=${o.level}`)),console.log(c("gray"," Commit this file and Crucible will run on every PR.")),console.log("")}catch(e){console.error(c("red",` Error: ${e.message}`))}return}if("badge"===n){const e=o.find(e=>e.startsWith("--score="));let n=e?parseInt(e.split("=")[1],10):null;if(null===n&&(n=BadgeGenerator.readLastScore(l)),null===n)return console.log(""),console.log(c("yellow"," No Crucible report found.")),console.log(c("gray"," Run `thuban crucible run .` first, or pass --score=<n>")),void console.log("");const s=o.find(e=>e.startsWith("--style=")),t=o.find(e=>e.startsWith("--threshold=")),a=s?s.split("=")[1]:"score",r=t?parseInt(t.split("=")[1],10):70,i=BadgeGenerator.generateBadge({score:n,targetPath:l,style:a,threshold:r});return console.log(""),console.log(c("green",` ✓ Badge generated: ${c("cyan",path.relative(l,i.outputPath))}`)),console.log(` Score: ${c("green"===i.color?"green":"yellow"===i.color?"yellow":"red",n+"/100")} (${i.verdict})`),console.log(""),console.log(c("bold"," Add to README.md:")),console.log(` ${c("cyan",i.markdown)}`),void console.log("")}if("leaderboard"!==n&&"lb"!==n){if("run"===n||"seed"===n){const n=parseFlag(o,"level","gentle"),s=parseIntFlag(o,"seed",42),t=parseFlag(o,"lang",null),a=parseFlag(o,"category",null);console.log(Crucible.banner.fullBanner({level:n,version:"0.4.0"})),console.log(c("bold"," Running adversarial seed tests...")),console.log(c("gray",` Level: ${n} · PRNG seed: ${s}`)),console.log("");let r=0,i=0,g=0;const d=await Crucible.run({targetPath:l,level:n,seed:s,languages:t?t.split(","):void 0,categories:a?a.split(","):void 0,onProgress(e,o,n){g++,"seed"===e&&(n.detected?(r++,process.stdout.write(c("green","."))):(i++,process.stdout.write(c("red","x"))),g%50==0&&process.stdout.write("\n"))}});process.stdout.write("\n\n"),console.log(c("bold"," Results:")+` ${c("green",d.seedResults.detected+" detected")} ${c("red",d.seedResults.missed+" missed")} (${d.seedResults.total} seeds)`),console.log(Crucible.banner.scoreBanner(d.score));const h=Crucible.compareGolden(d);return h.hasMaster?console.log(h.passed?c("green"," ✓ No regressions vs golden master"):c("red",` ✗ ${h.regressions} regression(s) vs golden master`)):console.log(c("gray"," (No golden master — run `thuban crucible approve` to set one)")),console.log(""),e.json&&console.log(JSON.stringify(d,null,2)),BadgeGenerator.writeLastScore&&BadgeGenerator.writeLastScore(l,d.score),void(Leaderboard.addEntry&&Leaderboard.addEntry(l,{score:d.score,level:n,timestamp:d.timestamp}))}if("approve"===n){console.log(""),console.log(c("bold"," Approving current results as golden master..."));const e=await Crucible.run({targetPath:l,level:"gentle",seed:42}),o=Crucible.approve(e);return console.log(c("green",` ✓ Golden master saved: ${o.count} findings, score ${o.meta.score}`)),console.log(c("gray"," File: .golden/golden-master.json")),void console.log("")}if("kenny"===n){console.log(Crucible.banner.compactBanner("KENNY MODE")),console.log(c("bold",` Kenny's 7-report test battery (${Crucible.KennyMode.testCount} tests)`)),console.log("");const o=await Crucible.kenny({onProgress(e,o){const n=o.passed?c("green","✓"):o.crashed?c("yellow","⚡"):c("red","✗"),l=c("gray",e.id)+" "+e.name,s=o.missingDetections&&o.missingDetections.length?c("red"," missing: "+o.missingDetections.join(", ")):"";console.log(` ${n} ${l}${s}`)}});return console.log(Crucible.banner.summaryLine(o.passed,o.total-o.skipped,o.score)),void(e.json&&console.log(JSON.stringify(o,null,2)))}if("self-test"===n||"selftest"===n){console.log(Crucible.banner.compactBanner("SELF-ATTACK")),console.log(c("bold",` Adversarial self-attack (${Crucible.SelfAttack.payloadCount} payloads)`)),console.log("");const o=await Crucible.selfTest({onProgress(e,o){const n="pass"===o.type?c("green","✓"):"crash"===o.type?c("red","⚡ CRASH"):"hang"===o.type?c("yellow","⏱ HANG"):c("yellow","? MISSED");console.log(` ${n} ${c("gray",e.name)} ${e.description}`)}});console.log("");const n=o.crashes.length,l=o.hangs.length;return 0===n&&0===l?console.log(c("green",` ✓ All ${o.payloadCount} payloads handled gracefully`)):(n&&console.log(c("red",` ✗ ${n} crash(es) detected`)),l&&console.log(c("yellow",` ⚠ ${l} hang(s) detected`))),console.log(Crucible.banner.scoreBanner(o.score)),void(e.json&&console.log(JSON.stringify(o,null,2)))}if("report"===n){console.log(""),console.log(c("bold"," Generating Crucible report..."));const o=await Crucible.run({targetPath:l,level:"gentle",seed:42}),n=await Crucible.kenny(),s=Crucible.compareGolden(o),t={title:"Thuban Crucible Report",level:"gentle",score:o.score,seedResults:o.seedResults,mutationResults:o.mutationResults,kennyResults:n,goldenComparison:s.hasMaster?s:null},a=e.output?path.resolve(e.output):l,{htmlPath:r,jsonPath:i}=Crucible.report(t,{outputDir:a});return console.log(c("green",` ✓ HTML report: ${c("cyan",r)}`)),console.log(c("green",` ✓ JSON summary: ${c("cyan",i)}`)),void console.log(Crucible.banner.scoreBanner(o.score))}if("golden"===n){const e=(new Crucible.GoldenMaster).loadMaster();return console.log(""),e?(console.log(c("bold"," Golden Master Status")),console.log(` Score: ${null!=e.meta.score?e.meta.score:"N/A"}`),console.log(` Findings: ${e.count}`),console.log(` Saved: ${e.timestamp}`),console.log(` Hash: ${e.hash}`)):console.log(c("yellow"," No golden master. Run `thuban crucible approve` to create one.")),void console.log("")}console.log(""),console.log(c("bold"," CRUCIBLE — Adversarial Testing Engine")),console.log(""),console.log(c("gray"," Core commands:")),console.log(` ${c("cyan","thuban crucible run")} [path] [--level gentle|medium|hell]`),console.log(` ${c("cyan","thuban crucible seed")} [path]`),console.log(` ${c("cyan","thuban crucible mutate")} [path]`),console.log(` ${c("cyan","thuban crucible golden")} [path]`),console.log(` ${c("cyan","thuban crucible approve")}`),console.log(` ${c("cyan","thuban crucible self-test")}`),console.log(` ${c("cyan","thuban crucible kenny")}`),console.log(` ${c("cyan","thuban crucible report")}`),console.log(""),console.log(c("gray"," CI/CD & badges:")),console.log(` ${c("cyan","thuban crucible ci github")} Generate GitHub Actions workflow`),console.log(` ${c("cyan","thuban crucible ci gitlab")} Generate GitLab CI config`),console.log(` ${c("cyan","thuban crucible ci bitbucket")} Generate Bitbucket Pipelines config`),console.log(` ${c("cyan","thuban crucible badge")} Generate SVG score badge for README`),console.log(` ${c("cyan","thuban crucible leaderboard")} View score history & trend`),console.log(""),console.log(c("gray"," Options:")),console.log(` ${c("gray","--threshold=<n>")} Fail CI if score drops below n (default 70)`),console.log(` ${c("gray","--level=<l>")} gentle | medium | hell`),console.log(` ${c("gray","--score=<n>")} Explicit score for badge (else reads last report)`),console.log(` ${c("gray","--style=score|verdict")} Badge text style`),console.log("")}else{if(e.json){const e=Leaderboard.getSummary(l);return void console.log(JSON.stringify(e,null,2))}Leaderboard.printLeaderboard(l,{color:!0})}}function getTelemetryConfigPath(){const e=process.env.HOME||process.env.USERPROFILE||os.homedir();return path.join(e,".thuban-telemetry.json")}function isTelemetryEnabled(){try{return!0===JSON.parse(fs.readFileSync(getTelemetryConfigPath(),"utf-8")).enabled}catch{return!1}}function sendAnonymousTelemetry(e){if(isTelemetryEnabled())try{const o=JSON.stringify({v:e.version||"0.4.0",cmd:e.command,files:e.fileCount,score:e.score,grade:e.grade,langs:e.languages,issues:e.issueCount,duration:e.duration,os:process.platform,node:process.version,ts:(new Date).toISOString()}),n=require("https").request({hostname:"thuban-telemetry.silverwingsbenefits.workers.dev",path:"/report",method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(o)},timeout:3e3});n.on("error",()=>{}),n.write(o),n.end()}catch{}}async function cmdTelemetry(e){const o=getTelemetryConfigPath(),n=process.argv.slice(2);if(n.includes("--on"))fs.writeFileSync(o,JSON.stringify({enabled:!0,updated:(new Date).toISOString()})),console.log(""),console.log(c("cyan"," Telemetry enabled")),console.log(c("gray"," Anonymous usage stats will be sent (no code, no paths, no PII)")),console.log(c("gray"," Turn off anytime: npx thuban telemetry --off")),console.log("");else if(n.includes("--off"))fs.writeFileSync(o,JSON.stringify({enabled:!1,updated:(new Date).toISOString()})),console.log(""),console.log(c("cyan"," Telemetry disabled")),console.log(c("gray"," No data will be collected")),console.log("");else{const e=isTelemetryEnabled();console.log(""),console.log(c("bold"," TELEMETRY STATUS")),console.log(""),console.log(` Status: ${e?c("cyan","Enabled"):c("gray","Disabled (default)")}`),console.log(""),console.log(c("gray"," Thuban telemetry is 100% opt-in and anonymous.")),console.log(c("gray"," We collect: file count, score, grade, languages, OS, scan duration.")),console.log(c("gray"," We never collect: code, file names, paths, project names, or PII.")),console.log(""),console.log(` Enable: ${c("cyan","npx thuban telemetry --on")}`),console.log(` Disable: ${c("cyan","npx thuban telemetry --off")}`),console.log("")}}async function runScanSilent(e){const o=/^(https?:\/\/|git@)/.test(e);let n=o?e:path.resolve(e),c=null,l=e;if(o){const o=path.join(os.tmpdir(),"thuban-compare-"+Date.now()+"-"+Math.random().toString(36).slice(2,6)),{execFileSync:s}=require("child_process");s("git",["clone","--depth","1",e,o],{stdio:"pipe",timeout:6e4}),n=o,c=o;const t=e.replace(/\.git$/,"").split("/");l=t[t.length-1]||e}else l=path.basename(path.resolve(e))||e;const s=Date.now(),t=new CodeScanner,a=new DependencyGraph,r=new HallucinationDetector({rootPath:n}),i=collectFiles(n,[]),g={};let d=0,h=0;for(const e of i)try{const o=t.scanFile(e);path.relative(n,e),g[e]=o,a.addFile(e,n),o.motherCode&&o.motherCode.hasDNA&&d++,o.drifts&&o.drifts.length>0&&h++}catch(e){}const u=r.scan(i,n),y=Date.now()-s;let p=0,f=0,m=0;const b={};for(const[,e]of Object.entries(g))if(e.issues){p+=e.issues.length;for(const o of e.issues){"critical"===o.severity&&f++,"high"===o.severity&&m++;const e=o.category||o.type||"other";b[e]=(b[e]||0)+1}}const $=(u.phantomAPIs||[]).length+(u.phantomImports||[]).length+(u.deprecatedAPIs||[]).length;p+=$,f+=(u.phantomAPIs||[]).length+(u.phantomImports||[]).length,m+=(u.deprecatedAPIs||[]).length,$>0&&(b.hallucination=$);const w=a.getOrphanFiles(),v=a.circularDeps||[],S=i.length>0?Math.round(d/i.length*100):0,C=Math.max(0,Math.round(100-15*f-5*m-10*v.length-1*w.length-.1*(100-S)-3*h)),x=C>=90?"A":C>=80?"B":C>=70?"C":C>=60?"D":"F";if(c)try{fs.rmSync(c,{recursive:!0,force:!0})}catch{}return{label:l,files:i.length,score:C,grade:x,elapsed:y,issues:{total:p,critical:f,high:m},categories:b,dependencies:{circular:v.length,orphans:w.length},motherCode:{annotated:d,coverage:S,drifted:h}}}async function cmdCompare(e){const o=process.argv.slice(2).filter(e=>"compare"!==e&&!e.startsWith("--"));o.length<2&&(console.log(""),console.log(c("red"," Compare requires two paths or URLs")),console.log(""),console.log(c("gray"," Usage:")),console.log(` ${c("cyan","npx thuban compare ./project-a ./project-b")}`),console.log(` ${c("cyan","npx thuban compare . https://github.com/user/repo")}`),console.log(` ${c("cyan","npx thuban compare https://github.com/a/repo https://github.com/b/repo")}`),console.log(""),process.exit(1)),console.log(""),console.log(c("bold"," ╔══════════════════════════════════════╗")),console.log(c("bold"," ║ THUBAN — Codebase Comparison ║")),console.log(c("bold"," ╚══════════════════════════════════════╝")),console.log(""),console.log(c("cyan",` Scanning: ${o[0]}`));const n=await runScanSilent(o[0]);console.log(c("cyan",` Scanning: ${o[1]}`));const l=await runScanSilent(o[1]);console.log("");const s=(e,o)=>String(e).padEnd(o),t=(e,o)=>String(e).padStart(o),a=20,r=n.label.length>a?n.label.slice(0,18)+"..":n.label,i=l.label.length>a?l.label.slice(0,18)+"..":l.label,g=n.score>=80?"green":n.score>=60?"yellow":"red",d=l.score>=80?"green":l.score>=60?"yellow":"red";console.log(c("bold"," ┌──────────────────────┬──────────────────────┬──────────────────────┐")),console.log(c("bold",` │ ${s("METRIC",a)} │ ${s(r,a)} │ ${s(i,a)} │`)),console.log(c("bold"," ├──────────────────────┼──────────────────────┼──────────────────────┤"));const h=(e,o,n)=>{console.log(` │ ${c("gray",s(e,a))} │ ${s(o,a)} │ ${s(n,a)} │`)};h("Health Score",`${c(g,n.score+"/100")}`,`${c(d,l.score+"/100")}`),h("Grade",`${c(g,n.grade)}`,`${c(d,l.grade)}`),h("Files Scanned",String(n.files),String(l.files)),h("Total Issues",String(n.issues.total),String(l.issues.total)),h("Critical Issues",String(n.issues.critical),String(l.issues.critical)),h("High Issues",String(n.issues.high),String(l.issues.high)),h("Circular Deps",String(n.dependencies.circular),String(l.dependencies.circular)),h("Orphan Files",String(n.dependencies.orphans),String(l.dependencies.orphans)),h("Mother Code DNA",`${n.motherCode.coverage}%`,`${l.motherCode.coverage}%`),h("Drift Issues",String(n.motherCode.drifted),String(l.motherCode.drifted)),h("Scan Time",`${n.elapsed}ms`,`${l.elapsed}ms`),console.log(c("bold"," └──────────────────────┴──────────────────────┴──────────────────────┘")),console.log("");const u=n.score-l.score;if(0===u)console.log(c("cyan",` VERDICT: Both codebases score identically (${n.score}/100)`));else{const e=u>0?n:l,o=u>0?l:n;console.log(c("cyan",` VERDICT: ${e.label} is healthier`)),console.log(c("gray",` ${e.label} scores ${e.score}/100 (${e.grade}) vs ${o.label} at ${o.score}/100 (${o.grade})`)),console.log(c("gray",` ${Math.abs(u)} point difference — ${Math.abs(u)>20?"significant gap":Math.abs(u)>10?"moderate gap":"close race"}`))}const y=new Set([...Object.keys(n.categories),...Object.keys(l.categories)]);if(y.size>0){console.log(""),console.log(c("bold"," ISSUE BREAKDOWN"));for(const e of y){const o=n.categories[e]||0,a=l.categories[e]||0,g=e.charAt(0).toUpperCase()+e.slice(1);console.log(c("gray",` ${s(g,22)} ${t(o,5)} vs ${t(a,5)} ${o<a?c("green",`← ${r}`):o>a?c("green",`→ ${i}`):c("gray","tied")}`))}}console.log(""),console.log(c("gray",` Full scan: npx thuban scan ${o[0]}`)),console.log(c("gray",` Full scan: npx thuban scan ${o[1]}`)),console.log("")}async function main(){const e=parseArgs(process.argv.slice(2));if(e.command&&"help"!==e.command||(printHelp(),process.exit(0)),"version"===e.command){const e=JSON.parse(fs.readFileSync(path.join(__dirname,"package.json"),"utf-8"));console.log(`thuban v${e.version}`),process.exit(0)}["activate","status","upgrade","telemetry","compare","crucible"].includes(e.command)||e.targetPath.match(/^(https?:\/\/|git@)/)||fs.existsSync(e.targetPath)||(console.error(c("red",` Error: Path not found: ${e.targetPath}`)),process.exit(1));try{switch(e.command){case"scan":await cmdScan(e);break;case"deps":await cmdDeps(e);break;case"drift":await cmdDrift(e);break;case"inject":await cmdInject(e);break;case"health":case"report":await cmdReport(e);break;case"debt":await cmdDebt(e);break;case"fix":await cmdFix(e);break;case"hallucinate":case"hal":await cmdHallucinate(e);break;case"watch":await cmdWatch(e);break;case"monitor":await cmdMonitor(e);break;case"history":await cmdHistory(e);break;case"trend":await cmdTrend(e);break;case"dashboard":case"dash":await cmdDashboard(e);break;case"diff":await cmdDiff(e);break;case"activate":await cmdActivate(e);break;case"status":await cmdStatus(e);break;case"upgrade":cmdUpgrade();break;case"gate":await cmdGate(e);break;case"cost":await cmdCost(e);break;case"ghosts":await cmdGhosts(e);break;case"ai-score":case"aiscore":await cmdAIScore(e);break;case"clones":case"duplicates":await cmdClones(e);break;case"passport":await cmdPassport(e);break;case"executive":case"exec-report":case"pdf":await cmdExecutive(e);break;case"baseline":await cmdBaseline(e);break;case"telemetry":await cmdTelemetry(e);break;case"compare":await cmdCompare(e);break;case"crucible":await cmdCrucible(e);break;default:console.error(""),console.error(c("red",` Unknown command: ${e.command}`)),console.error("");const o=["scan","compare","deps","drift","inject","health","report","debt","fix","hallucinate","watch","dashboard","diff","gate","cost","ghosts","ai-score","clones","passport","executive","baseline","activate","status","upgrade","telemetry"].filter(o=>o.startsWith(e.command.slice(0,2))||o.includes(e.command));o.length>0&&(console.error(c("gray"," Did you mean: ")+c("cyan",o.join(", "))+c("gray","?")),console.error("")),console.error(c("gray"," Quick start:")),console.error(` ${c("cyan","npx thuban scan .")} Scan your project`),console.error(` ${c("cyan","npx thuban --help")} See all commands`),console.error(""),process.exit(1)}}catch(o){console.error(c("red",` Error: ${o.message}`)),e.verbose&&console.error(o.stack),process.exit(1)}}main();
2
+ const path=require("path"),fs=require("fs"),os=require("os"),_CLI_VERSION=JSON.parse(fs.readFileSync(path.join(__dirname,"package.json"),"utf-8")).version,CodeScanner=require("./packages/scanner/code-scanner.js"),DependencyGraph=require("./packages/scanner/dependency-graph.js"),CIIntegration=require("./packages/crucible/ci-integration.js"),BadgeGenerator=require("./packages/crucible/badge-generator.js"),Leaderboard=require("./packages/crucible/leaderboard.js"),Crucible=require("./packages/crucible/index.js"),DriftDetector=require("./packages/scanner/drift-detector.js"),WidgetGenerator=require("./packages/scanner/widget-generator.js"),AlertManager=require("./packages/scanner/alert-manager.js"),TechDebtAnalyzer=require("./packages/scanner/tech-debt-analyzer.js"),HallucinationDetector=require("./packages/scanner/hallucination-detector.js"),HtmlReportGenerator=require("./packages/scanner/html-report.js"),FileWatcher=require("./packages/scanner/file-watcher.js"),LicenseManager=require("./packages/scanner/license-manager.js"),PreCommitGate=require("./packages/scanner/pre-commit-gate.js"),TechDebtCostCalculator=require("./packages/scanner/tech-debt-cost.js"),GhostCodeDetector=require("./packages/scanner/ghost-code-detector.js"),AIConfidenceScorer=require("./packages/scanner/ai-confidence-scorer.js"),CopyPasteDriftDetector=require("./packages/scanner/copy-paste-detector.js"),CodebasePassport=require("./packages/scanner/codebase-passport.js"),ExecutiveReport=require("./packages/scanner/executive-report.js"),BaselineManager=require("./packages/scanner/baseline-manager.js"),MonitorStore=require("./packages/scanner/monitor-store.js"),{MonitorService:MonitorService,resolveIntervalMs:resolveIntervalMs}=require("./packages/scanner/monitor-service.js"),{collectFiles:collectFiles}=require("./packages/scanner/file-collector.js");function parseFlag(e,o,n){const c=e.find(e=>e.startsWith(`--${o}=`));return c?c.split("=").slice(1).join("="):!!e.includes(`--${o}`)||n}function parseIntFlag(e,o,n){const c=parseFlag(e,o,null);if(null===c)return n;const l=parseInt(c,10);return isNaN(l)?n:l}const COLORS={reset:"",bold:"",red:"",green:"",yellow:"",blue:"",cyan:"",gray:"",white:"",bgRed:"",bgGreen:"",bgYellow:""};function c(e,o){return`${COLORS[e]}${o}${COLORS.reset}`}function banner(){console.log(""),console.log(c("cyan"," ╔══════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," THUBAN Code Health Engine ")+c("cyan","║")),console.log(c("cyan"," ║")+c("gray",` v${_CLI_VERSION} · Public Beta · thuban.dev `)+c("cyan","║")),console.log(c("cyan"," ╚══════════════════════════════════════╝")),console.log("")}function printHelp(){banner();const e=(new LicenseManager).getStatus(),o=JSON.parse(fs.readFileSync(path.join(__dirname,"package.json"),"utf-8"));console.log(c("gray",` v${o.version} · ${e.tierName} tier`)+(e.licensed?c("green"," ✓"):"")+c("gray"," · https://thuban.dev")),console.log(""),console.log(c("bold"," QUICK START")),console.log(""),console.log(c("gray"," Run these in order to see what Thuban does:")),console.log(""),console.log(` ${c("cyan","npx thuban scan .")} Scan your project`),console.log(` ${c("cyan","npx thuban fix .")} Preview what can be fixed`),console.log(` ${c("cyan","npx thuban fix . --fix")} Apply all safe fixes`),console.log(` ${c("cyan","npx thuban dashboard .")} Open visual HTML report`),console.log(""),console.log(c("bold"," SCAN & DETECT")),console.log(""),console.log(` ${c("cyan","scan")} [path|url] Full health scan — score, grade, all issues`),console.log(` ${c("cyan","compare")} [a] [b] Compare two codebases side by side`),console.log(` ${c("cyan","hallucinate")} [path] Find phantom APIs and invented imports`),console.log(` ${c("cyan","ghosts")} [path] Dead functions nobody calls`),console.log(` ${c("cyan","ai-score")} [path] Probability each function is AI-generated`),console.log(` ${c("cyan","clones")} [path] Copy-pasted code that has drifted apart`),console.log(` ${c("cyan","drift")} [path] Files that changed since Mother Code was set`),console.log(` ${c("cyan","deps")} [path] Dependency map — circular, orphan, critical`),console.log(` ${c("cyan","diff")} [path] Scan only git-changed files`),console.log(` ${c("cyan","debt")} [path] Detailed tech debt breakdown`),console.log(` ${c("cyan","cost")} [path] Tech debt translated to £ and hours`),console.log(""),console.log(c("bold"," FIX & PROTECT")),console.log(""),console.log(` ${c("cyan","fix")} [path] Preview fixable issues ${c("gray","(safe, no changes)")}`),console.log(` ${c("cyan","fix")} [path] ${c("gray","--fix")} Apply all safe fixes`),console.log(` ${c("cyan","fix")} [path] ${c("gray","--fix --commit")} Each fix = 1 git commit ${c("gray","(revert any)")}`),console.log(` ${c("cyan","inject")} [path] Add Mother Code DNA to every file`),console.log(` ${c("cyan","gate")} Install pre-commit hook ${c("gray","(blocks hallucinations)")}`),console.log(` ${c("cyan","watch")} [path] Live sentinel — scans files as you save`),console.log(` ${c("cyan","monitor")} [path] Recurring scans with saved history and alerts ${c("gray","(Pro)")}`),console.log(` ${c("cyan","history")} [path] View recurring scan history for a repo ${c("gray","(Pro)")}`),console.log(` ${c("cyan","trend")} [path] Show issue trends across recurring scans ${c("gray","(Pro)")}`),console.log(` ${c("cyan","baseline")} [path] Snapshot issues so future scans show only NEW ones`),console.log(""),console.log(c("bold"," REPORTS")),console.log(""),console.log(` ${c("cyan","dashboard")} [path] Interactive HTML report ${c("gray","(share with your team)")}`),console.log(` ${c("cyan","executive")} [path] CTO-ready report ${c("gray","(print to PDF from browser)")}`),console.log(` ${c("cyan","report")} [path] Combined scan + deps + drift summary`),console.log(` ${c("cyan","passport")} [path] Codebase identity card ${c("gray","(for new team members)")}`),console.log(""),console.log(c("bold"," LICENSE")),console.log(""),console.log(` ${c("cyan","activate")} <key> Activate a license key`),console.log(` ${c("cyan","status")} Show license, usage, and features`),console.log(` ${c("cyan","upgrade")} Open pricing page`),console.log(""),console.log(c("bold"," OPTIONS")),console.log(""),console.log(` ${c("gray","--fix")} Apply changes ${c("gray","(default is dry-run preview)")}`),console.log(` ${c("gray","--commit")} Auto-commit each fix for easy rollback`),console.log(` ${c("gray","--unsafe")} Include fixes that change runtime behaviour`),console.log(` ${c("gray","--json")} Machine-readable JSON output`),console.log(` ${c("gray","--baseline")} Only show NEW issues vs saved baseline`),console.log(` ${c("gray","--verbose")} Show detailed output`),console.log(` ${c("gray","--ignore <pat>")} Skip files matching pattern`),console.log(` ${c("gray","--max-issues <n>")} Limit displayed issues ${c("gray","(default: 50)")}`),console.log(""),console.log(c("bold"," EXAMPLES")),console.log(""),console.log(c("gray"," # Scan the current project")),console.log(` ${c("white","npx thuban scan .")}`),console.log(""),console.log(c("gray"," # Find and fix all AI hallucinations")),console.log(` ${c("white","npx thuban hallucinate .")}`),console.log(` ${c("white","npx thuban fix . --fix")}`),console.log(""),console.log(c("gray"," # Generate a report for your CTO")),console.log(` ${c("white","npx thuban executive .")}`),console.log(""),console.log(c("gray"," # Block hallucinated code on every commit")),console.log(` ${c("white","npx thuban gate --fix")}`),console.log(""),console.log(c("gray"," # Scan a specific folder, ignore tests")),console.log(` ${c("white",'npx thuban scan ./src --ignore "**/*.test.js"')}`),console.log(""),console.log(c("gray"," Report a bug or request a feature:")),console.log(` ${c("cyan","https://github.com/SilverwingsBenefitsGit/thuban/issues")}`),console.log("")}function parseArgs(e){const o={command:null,targetPath:process.cwd(),json:!1,fix:!1,dryRun:!1,safe:!0,gitCommit:!1,verbose:!1,ignore:[],maxIssues:50,licenseKey:null,baseline:!1,baselineCreate:!1,frequency:"daily",intervalMs:null,notify:"inbox",once:!1},n=["activate","status","upgrade","help","version"];let c=0;for(;c<e.length;){const l=e[c];"--json"===l?o.json=!0:"--fix"===l?o.fix=!0:"--dry-run"===l||"--dryrun"===l?o.dryRun=!0:"--unsafe"===l?o.safe=!1:"--git-commit"===l||"--commit"===l?o.gitCommit=!0:"--baseline"===l?o.baseline=!0:"--baseline-create"===l?o.baselineCreate=!0:"--frequency"===l&&e[c+1]?o.frequency=e[++c]:"--interval-ms"===l&&e[c+1]?o.intervalMs=parseInt(e[++c],10):"--notify"===l&&e[c+1]?o.notify=e[++c]:"--once"===l?o.once=!0:"--verbose"===l?o.verbose=!0:"--ignore"===l&&e[c+1]?o.ignore.push(e[++c]):"--max-issues"===l&&e[c+1]?o.maxIssues=parseInt(e[++c],10):"--help"===l||"-h"===l?o.command="help":"--version"===l||"-v"===l?o.command="version":o.command?"activate"!==o.command||o.licenseKey?n.includes(o.command)||(l.match(/^(https?:\/\/|git@)/)?o.targetPath=l:o.targetPath=path.resolve(l)):o.licenseKey=l:o.command=l,c++}return o}function summarizeRemainingFiles(e,o,n){const c=new Set(e),l=o.filter(e=>!c.has(e)),s=new Map;for(const e of l){const o=path.relative(n,e).split(path.sep).filter(Boolean),c=o.length>1?o[0]:"(root)";s.set(c,(s.get(c)||0)+1)}const t=Array.from(s.entries()).sort((e,o)=>o[1]-e[1]||e[0].localeCompare(o[0])).slice(0,3).map(([e,o])=>({directory:e,count:o}));return{remainingFiles:l,remainingCount:l.length,topDirectories:t,previewFiles:l.slice(0,5).map(e=>path.relative(n,e))}}function requireProFeature(e){const o=new LicenseManager;if(!o.canUseProFeature(e).allowed){const o="https://thuban.dev/pricing";console.log(""),console.log(c("yellow"," ╔══════════════════════════════════════════════════════════════╗")),console.log(c("yellow"," ║")+c("bold"," PRO FEATURE REQUIRED ")+c("yellow","║")),console.log(c("yellow"," ╠══════════════════════════════════════════════════════════════╣")),console.log(c("yellow"," ║")+` ${e} is available on Thuban Pro.`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" Free Thuban gives you a one-off checkup. Pro gives you".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" a pair of eyes on your codebase all day every day with".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" recurring scans, saved history, trends, and alerts.".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" "+c("yellow","║")),console.log(c("yellow"," ║")+` Upgrade now: ${o}`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" CLI shortcut: thuban upgrade".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ╚══════════════════════════════════════════════════════════════╝")),console.log(""),process.exit(0)}return o}function resolveRepoConfigOrExit(e,o){const n=e.getRepo(o);return n||(console.log(""),console.log(c("yellow"," No monitor configuration found for this repo.")),console.log(c("gray"," Run `thuban monitor <path> --once` or `thuban monitor <path>` first.")),console.log(""),process.exit(0)),n}function printFileCapUpgradePrompt({scannedCount:e,totalCount:o,remainingSummary:n,upgradeUrl:l}){const s=n.topDirectories.length;if(console.log(c("yellow"," ╔══════════════════════════════════════════════════════════════╗")),console.log(c("yellow"," ║")+c("bold"," FREE TIER LIMIT REACHED ")+c("yellow","║")),console.log(c("yellow"," ╠══════════════════════════════════════════════════════════════╣")),console.log(c("yellow"," ║")+` You have scanned ${e} of ${o} files.`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" Upgrade to Thuban Pro for continuous monitoring —".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" a pair of eyes on your codebase, all day, every day.".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" Your always-on guardian with unlimited scans.".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+` ${n.remainingCount} more files remaining including ${s}`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+` critical director${1===s?"y":"ies"}.`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" "+c("yellow","║")),console.log(c("yellow"," ║")+` Upgrade now: ${l}`.padEnd(62)+c("yellow","║")),console.log(c("yellow"," ║")+" CLI shortcut: thuban upgrade".padEnd(62)+c("yellow","║")),console.log(c("yellow"," ╚══════════════════════════════════════════════════════════════╝")),console.log(""),n.topDirectories.length>0){console.log(c("bold"," Remaining scan preview"));for(const e of n.topDirectories)console.log(` ${c("yellow","•")} ${c("cyan",e.directory)} ${c("gray",`(${e.count} files)`)}`);if(n.previewFiles.length>0){console.log(""),console.log(c("gray"," Next files that would be scanned:"));for(const e of n.previewFiles)console.log(` ${c("gray","·")} ${e}`)}console.log("")}}async function cmdScan(e){const o=Date.now(),n=/^(https?:\/\/|git@)/.test(e.targetPath);let l=n?e.targetPath:path.resolve(e.targetPath),s=null;if(n){e.json||(console.log(""),console.log(c("cyan"," Cloning repository...")),console.log(c("gray",` ${e.targetPath}`)));const o=path.join(os.tmpdir(),"thuban-scan-"+Date.now()),{execFileSync:n}=require("child_process");try{n("git",["clone","--depth","1",e.targetPath,o],{stdio:"pipe",timeout:6e4}),l=o,s=o,e.json||(console.log(c("cyan"," Cloned successfully")),console.log(""))}catch(e){console.error(c("red",` Failed to clone repository: ${e.message}`)),console.error(c("gray"," Check the URL is correct and the repo is accessible")),process.exit(1)}}const t=new LicenseManager,a=t.getTierConfig(),r=t.canScan(),i="https://thuban.dev/pricing";r.allowed||e.json||(console.log(""),console.log(c("red"," ╔══════════════════════════════════════════════════╗")),console.log(c("red"," ║")+c("bold"," Monthly scan limit reached ")+c("red","║")),console.log(c("red"," ╚══════════════════════════════════════════════════╝")),console.log(""),console.log(` You've used ${c("bold",r.used+"/"+r.limit)} free scans this month.`),console.log(` Resets on ${c("cyan",r.resetsOn)}.`),console.log(""),console.log(` ${c("cyan","Upgrade to Pro")} for unlimited scans, full reports, auto-fix, and an always-on pair of eyes via thuban monitor:`),console.log(` ${c("bold",i)}`),console.log(""),console.log(` Or activate a key: ${c("cyan","thuban activate <your-key>")}`),console.log(""),process.exit(0)),e.json||(banner(),0===t.usage.totalScans&&(console.log(c("cyan"," ┌──────────────────────────────────────────────────────┐")),console.log(c("cyan"," │")+c("bold"," Welcome to Thuban! ")+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Thuban scans your codebase for AI hallucinations,")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","dead code, tech debt, and architecture drift.")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","After this scan, try:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix .")+" "+c("gray","Preview fixes")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban dashboard .")+" "+c("gray","Visual report")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban --help")+" "+c("gray","All commands")+" "+c("cyan","│")),console.log(c("cyan"," └──────────────────────────────────────────────────────┘")),console.log("")),console.log(c("bold"," SCANNING: ")+c("cyan",l)),console.log(""));const g=collectFiles(l,e.ignore),d=g.length,h=a.maxFiles<1/0?g.slice(0,a.maxFiles):g,y=d>a.maxFiles,u=y?summarizeRemainingFiles(h,g,l):null;!e.json&&y?(console.log(c("yellow",` Free tier: scanning ${a.maxFiles} of ${d} files`)),console.log(c("gray"," Partial results below. Upgrade to scan your entire codebase.")),console.log("")):e.json||(console.log(c("gray",` Found ${h.length} files to scan...`)),console.log(""));const p=new CodeScanner({rootPath:l,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),f=new HallucinationDetector({rootPath:l}),[m,b]=await Promise.all([p.scanFiles(h),f.scan(h)]),$=Date.now()-o;t.recordScan();const w=[];for(const e of m.issues||[])w.push({file:path.relative(l,e.file),...e});for(const e of b.phantomAPIs)w.push({file:e.file,line:e.line,severity:"critical",category:"hallucination",id:"HALL_API",message:`${e.name} — this API does not exist`,suggestion:e.suggestion,crash:!0});for(const e of b.phantomImports)w.push({file:e.file,line:e.line,severity:"critical",category:"hallucination",id:"HALL_IMPORT",message:`${e.module} — ${e.reason}`,crash:!0});for(const e of b.deprecatedAPIs)w.push({file:e.file,line:e.line,severity:"high",category:"deprecated",id:"DEPR_API",message:`${e.name} — deprecated since ${e.since}`,suggestion:e.suggestion});for(const e of b.aiSmells)w.push({file:e.file,line:e.line,severity:"warning",category:"ai_smell",id:e.id,message:e.message});if(e.json)return void console.log(JSON.stringify({files:d,filesScanned:h.length,elapsed_ms:$,tier:t.getTier(),issues:a.showFullDetails?w:w.slice(0,a.teaserIssues),totalIssues:w.length,gated:!a.showFullDetails,fileLimitReached:y,upgradeUrl:y?i:null,remainingFiles:u?u.remainingCount:0,remainingDirectories:u?u.topDirectories:[],remainingPreview:u?u.previewFiles:[]},null,2));const v={critical:0,high:1,error:2,medium:3,warning:4,low:5,info:6};w.sort((e,o)=>(v[e.severity]||99)-(v[o.severity]||99));const S=w.filter(e=>"critical"===e.severity),C=w.filter(e=>"high"===e.severity||"error"===e.severity),x=w.filter(e=>"medium"===e.severity||"warning"===e.severity),E=w.filter(e=>"low"===e.severity||"info"===e.severity),I=w.filter(e=>"hallucination"===e.category),P=w.filter(e=>"deprecated"===e.category),A=w.filter(e=>"ai_smell"===e.category),D=x.filter(e=>"ai_smell"!==e.category),T=Math.max(0,100-15*S.length-5*C.length-2*D.length-.5*A.length-.25*E.length),N=T>=90?"A":T>=80?"B":T>=70?"C+":T>=60?"C":T>=50?"D+":T>=40?"D":"F",R=T>=80?"green":T>=60?"yellow":"red",F=w.filter(e=>!1!==e.fixable).length;console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," THUBAN SCAN RESULTS ")+c("cyan","║")),console.log(c("cyan"," ║")+c("gray",` v${_CLI_VERSION} · Public Beta `)+c("cyan","║")),console.log(c("cyan"," ╠══════════════════════════════════════════════════════╣")),console.log(c("cyan"," ║")+` Files scanned: ${c("bold",String(h.length).padEnd(6))}`+(y?c("yellow",` of ${d}`):" ")+c("cyan"," ║")),console.log(c("cyan"," ║")+` Health score: ${c(R,(N+" ("+Math.round(T)+"/100)").padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Issues found: ${c(w.length>0?"yellow":"green",String(w.length).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Auto-fixable: ${c("green",String(F).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Scan time: ${c("gray",($+"ms").padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log("");const O=t.getTier();if("trial"===O&&t._trialInfo){const{daysRemaining:e,expiresAt:o}=t._trialInfo,n=e<=3?"red":e<=7?"yellow":"cyan",l=new Date(o).toLocaleDateString("en-GB",{day:"numeric",month:"long",year:"numeric"});console.log(c(n,` Trial: ${e} day${1===e?"":"s"} remaining (expires ${l})`)),e<=7&&console.log(c("gray"," Love it? Upgrade at: ")+c("cyan","https://thuban.dev/pricing")),console.log("")}else"free"===O&&t._trialExpired&&(console.log(c("yellow"," Your Thuban trial has expired. Upgrade to continue:")),console.log(c("cyan"," https://thuban.dev/pricing")),console.log(""));if(h.some(e=>!(e.endsWith(".js")||e.endsWith(".ts")||e.endsWith(".jsx")||e.endsWith(".tsx")||e.endsWith(".mjs")))){const e=[...new Set(h.filter(e=>!/\.(js|ts|jsx|tsx|mjs)$/.test(e)).map(e=>path.extname(e)).filter(Boolean))];console.log(c("gray"," Note: Score includes Security + Hallucination + Code Quality categories.")),console.log(c("gray",` Architecture + Dependency analysis evaluated for JS/TS only. Detected non-JS: ${e.join(", ")}`)),console.log("")}if(I.length>0){console.log(c("bgRed"," CRITICAL ")+c("red",c("bold",` ${I.length} Hallucinated API${I.length>1?"s":""}`))),console.log(c("gray"," These imports or method calls do not exist. Your code WILL crash when it hits them.")),console.log("");const e=a.showFullDetails?I.length:Math.min(a.teaserIssues,I.length);for(let o=0;o<e;o++){const e=I[o],n=e.line?`:${e.line}`:"";console.log(` ${c("cyan",e.file+n)}`);try{const o=path.resolve(l,e.file),n=fs.readFileSync(o,"utf-8").split("\n");e.line&&n[e.line-1]&&console.log(` ${c("red",n[e.line-1].trim())}`)}catch(e){}console.log(` ${c("yellow","^^^")} ${e.message}`),e.suggestion&&console.log(` ${c("green","Fix:")} ${e.suggestion}`),console.log("")}I.length>e&&(console.log(c("gray",` ... ${I.length-e} more hallucinations hidden`)),console.log(c("cyan"," Upgrade to Pro to see all: ")+c("bold","thuban upgrade")),console.log(""))}if(P.length>0){console.log(c("red",c("bold",` DEPRECATED: ${P.length} API${P.length>1?"s":""} that will break on upgrade`))),console.log(c("gray"," These work NOW but will stop working when you upgrade Node.js or your dependencies.")),console.log(c("gray"," Fix: ")+c("cyan","npx thuban fix . --fix --unsafe")),console.log("");const e=a.showFullDetails?P.length:Math.min(a.teaserIssues,P.length);for(let o=0;o<e;o++){const e=P[o],n=e.line?`:${e.line}`:"";console.log(` ${c("cyan",e.file+n)}`),console.log(` ${c("yellow",e.message)}`),e.suggestion&&console.log(` ${c("green","→")} ${e.suggestion}`),console.log("")}P.length>e&&(console.log(c("gray",` ... ${P.length-e} more hidden`)),console.log(""))}const j=w.filter(e=>"security"===e.category),k=w.filter(e=>"quality"===e.category||"performance"===e.category||"ai"===e.category||"ai_smell"===e.category);if(j.length>0){console.log(c("red",c("bold",` SECURITY: ${j.length} issue${j.length>1?"s":""}`)));const e=a.showFullDetails?Math.min(j.length,10):Math.min(a.teaserIssues,j.length);for(let o=0;o<e;o++){const e=j[o],n=e.line?`:${e.line}`:"";console.log(` ${c("red","✗")} ${c("cyan",e.file+n)} ${c("gray",e.message)}`)}j.length>e&&console.log(c("gray",` ... ${j.length-e} more hidden`)),console.log("")}if(k.length>0){const e={};for(const o of k){const n=(o.message||o.type||"other").replace(/\s*—.*$/,"").trim();e[n]||(e[n]=[]),e[n].push(o)}const o=Object.entries(e).sort((e,o)=>o[1].length-e[1].length);console.log(c("yellow",c("bold",` CODE QUALITY: ${k.length} issue${k.length>1?"s":""}`))),console.log(c("gray"," Patterns that reduce code quality, maintainability, or production readiness.")),console.log("");const n=a.showFullDetails?o.length:Math.min(3,o.length);for(let e=0;e<n;e++){const[n,l]=o[e],s="high"===l[0].severity||"error"===l[0].severity?"red":"yellow";if(console.log(` ${c(s,"!")} ${c("bold",String(l.length))} x ${n}`),l[0].file){const e=l[0].line?`:${l[0].line}`:"";console.log(` ${c("gray","e.g.")} ${c("cyan",l[0].file+e)}`)}}o.length>n&&console.log(c("gray",` ... ${o.length-n} more issue types`)),console.log("");const l=k.filter(e=>!1!==e.fixable).length;l>0&&(console.log(c("gray",` ${l} of these can be auto-fixed: `)+c("cyan","npx thuban fix . --fix --unsafe")),console.log(""))}if(console.log(c("bold"," ┌──────────────────────────────────────────────────────┐")),T<60?console.log(c("bold"," │ ")+c("red","YOUR CODEBASE HAS A SERIOUS HEALTH PROBLEM ")+c("bold"," │")):T<80?console.log(c("bold"," │ ")+c("yellow","YOUR CODEBASE NEEDS ATTENTION ")+c("bold"," │")):console.log(c("bold"," │ ")+c("green","YOUR CODEBASE IS IN GOOD SHAPE ")+c("bold"," │")),console.log(c("bold"," │")+` Score: ${c(R,N+" ("+Math.round(T)+"/100)")} `+c("bold","│")),console.log(c("bold"," └──────────────────────────────────────────────────────┘")),console.log(""),S.length>0){console.log(c("red",` ${S.length} function${S.length>1?"s":""} will crash in production.`));const e=S.length<=3?"30 seconds":S.length<=10?"2 minutes":"10 minutes";console.log(c("gray",` Estimated cleanup: ${e} with Thuban auto-fix.`)),console.log(c("gray"," Estimated cost if shipped: your weekend.")),console.log("")}if(a.showFullDetails)w.length>0&&(w.filter(e=>!1!==e.fixable).length,console.log(c("cyan"," ┌──────────────────────────────────────────────────────┐")),console.log(c("cyan"," │")+c("bold"," WHAT TO DO NEXT ")+c("cyan","│")),console.log(c("cyan"," ├──────────────────────────────────────────────────────┤")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","1. See what would be fixed (safe preview):")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix . ")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","2. Apply all safe fixes:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix . --fix")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","3. Apply fixes with git rollback:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban fix . --fix --commit")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Each fix = 1 git commit. Undo any with git revert.")+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","4. Generate visual report (HTML):")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban dashboard .")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Opens a dashboard you can share with your team.")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","5. Show tech debt cost in £:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban cost .")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("bold","6. Block bad code on every commit:")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","npx thuban gate")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","Installs a pre-commit hook. Set once, forget.")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("cyan","│")),console.log(c("cyan"," │")+" "+c("gray","All commands: npx thuban --help")+" "+c("cyan","│")),console.log(c("cyan"," └──────────────────────────────────────────────────────┘")),console.log(""));else{const e=w.length-a.teaserIssues;e>0&&(console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold",` ${e} more issues hidden `)+c("cyan","║")),console.log(c("cyan"," ║")+c("gray"," Upgrade to see every issue, fix them automatically,")+c("cyan","║")),console.log(c("cyan"," ║")+c("gray"," and get the full HTML dashboard report. ")+c("cyan","║")),console.log(c("cyan"," ╠══════════════════════════════════════════════════════╣")),console.log(c("cyan"," ║")+` ${c("bold","thuban upgrade")} Open pricing page `+c("cyan","║")),console.log(c("cyan"," ║")+` ${c("bold","thuban activate")} <key> Activate a license key `+c("cyan","║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log(""));const o=t.canScan(),n=o.remaining;console.log(c("gray",` Free scans remaining this month: ${n}/${o.limit}`)),console.log("")}y&&printFileCapUpgradePrompt({scannedCount:h.length,totalCount:d,remainingSummary:u,upgradeUrl:i}),w.length>0&&!y&&(console.log(c("gray"," ─────────────────────────────────────────────────────")),console.log(c("gray"," Thuban runs 100% on your machine. No data leaves.")),console.log(c("gray"," No cloud costs. As your codebase grows, Thuban grows with it.")),console.log(""),console.log(c("gray"," Keep your codebase healthy continuously:")),console.log(c("gray"," ")+c("cyan","npx thuban gate --fix")+c("gray"," Block bad code on every commit")),console.log(c("gray"," ")+c("cyan","npx thuban watch .")+c("gray"," Scan files as you save them")),console.log(c("gray"," ")+c("cyan","npx thuban baseline .")+c("gray"," Only alert on NEW issues")),console.log(""))}async function cmdDeps(e){const o=Date.now(),n=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," MAPPING DEPENDENCIES: ")+c("cyan",n)),console.log(""));const l=collectFiles(n,e.ignore),s=new DependencyGraph({rootPath:n});await s.build(l);const t=Date.now()-o,a=s.getModuleSummary(),r=s.getMostCriticalFiles(10),i=s.getOrphanFiles(),g=s.circularDeps||[];if(e.json)console.log(JSON.stringify({elapsed_ms:t,summary:a,critical:r,orphans:i,circular:g,graph:s.toJSON()},null,2));else{if(console.log(c("gray",` Analysed ${l.length} files in ${t}ms`)),console.log(""),console.log(c("bold"," DEPENDENCY SUMMARY")),console.log(` Files: ${a.totalFiles||l.length}`),console.log(` Import links: ${a.totalImports||0}`),console.log(` Circular deps: ${g.length>0?c("red",g.length):c("green","0")}`),console.log(` Orphan files: ${i.length>0?c("yellow",i.length):c("green","0")}`),console.log(""),r.length>0){console.log(c("bold"," MOST CRITICAL FILES")+c("gray"," (most dependents)"));for(const e of r){const o="string"==typeof e?e:e.file||e.path||String(e),l=path.relative(n,o),s="object"==typeof e&&(e.dependents||e.score||e.count)||"?";console.log(` ${c("cyan",l)} ${c("gray","→ "+s+" dependents")}`)}console.log("")}if(g.length>0){console.log(c("red"," CIRCULAR DEPENDENCIES"));for(const e of g.slice(0,5)){const o=Array.isArray(e)?e.map(e=>path.relative(n,e)).join(" → "):String(e);console.log(` ${c("yellow","⟲")} ${o}`)}console.log("")}if(i.length>0){console.log(c("yellow"," ORPHAN FILES")+c("gray"," (no imports, no exports consumed)"));for(const e of i.slice(0,10)){const o="string"==typeof e?e:e.file||e.path||String(e);console.log(` ${c("gray","○")} ${path.relative(n,o)}`)}i.length>10&&console.log(c("gray",` ... and ${i.length-10} more`)),console.log("")}}}async function cmdMonitor(e){requireProFeature("Scheduled monitoring");const o=path.resolve(e.targetPath),n=new MonitorService,l=n.configureRepo({repoPath:o,frequency:e.frequency,intervalMs:resolveIntervalMs(e.frequency,e.intervalMs),notification:{channels:String(e.notify||"inbox").split(",").map(e=>e.trim()).filter(Boolean)}});if(e.once){const o=await n.runRepoScan(l);return e.json?void console.log(JSON.stringify(o.run,null,2)):(console.log(""),console.log(c("bold"," THUBAN MONITOR RUN")),console.log(` Repo: ${l.repoPath}`),console.log(` New issues: ${o.diff.added.length}`),console.log(` Resolved issues: ${o.diff.resolved.length}`),console.log(` History saved: ${o.runPath}`),void console.log(""))}console.log(""),console.log(c("bold"," THUBAN MONITOR ACTIVE")),console.log(` Repo: ${l.repoPath}`),console.log(` Frequency: ${l.frequency} (${l.intervalMs}ms)`),console.log(` Notifications: ${(l.notification.channels||[]).join(", ")}`),console.log(c("gray"," Press Ctrl+C to stop.")),console.log(""),n.start(l),process.on("SIGINT",()=>{n.stopAll(),process.exit(0)})}async function cmdHistory(e){requireProFeature("Scan history");const o=new MonitorStore,n=resolveRepoConfigOrExit(o,e.targetPath),l=o.listRuns(n.repoId);if(e.json)return void console.log(JSON.stringify({repo:n,runs:l},null,2));console.log(""),console.log(c("bold"," THUBAN HISTORY")),console.log(` Repo: ${n.repoPath}`),console.log(` Runs: ${l.length}`),console.log("");for(const e of l.slice(-10).reverse())console.log(` ${c("cyan",e.timestamp)} issues=${e.metrics.issueCount} new=${e.diff?.added?.length||0} resolved=${e.diff?.resolved?.length||0}`);const s=o.getNotifications(10).filter(e=>e.repoId===n.repoId);if(s.length){console.log(""),console.log(c("bold"," RECENT ALERTS"));for(const e of s)console.log(` ${c("yellow",e.timestamp)} ${e.summary}`)}console.log("")}async function cmdTrend(e){requireProFeature("Trend reporting");const o=new MonitorStore,n=resolveRepoConfigOrExit(o,e.targetPath),l=o.listRuns(n.repoId).map(e=>({timestamp:e.timestamp,issueCount:e.metrics.issueCount,hallucinations:e.metrics.hallucinations,secrets:e.metrics.secrets,techDebtScore:e.metrics.techDebtScore,techDebtHours:e.metrics.techDebtHours}));if(e.json)console.log(JSON.stringify({repo:n,trend:l},null,2));else{console.log(""),console.log(c("bold"," THUBAN TREND")),console.log(` Repo: ${n.repoPath}`),console.log("");for(const e of l.slice(-12))console.log(` ${c("cyan",e.timestamp)} issues=${e.issueCount} hallucinations=${e.hallucinations} secrets=${e.secrets} debtScore=${e.techDebtScore??"n/a"} debtHours=${e.techDebtHours??"n/a"}`);console.log("")}}async function cmdDrift(e){const o=Date.now(),n=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," DETECTING DRIFT: ")+c("cyan",n)),console.log(""));const l=collectFiles(n,e.ignore),s=new DriftDetector({rootPath:n}),t=[];let a=0,r=0;for(const e of l)try{const o=await s.analyzeFile(e);o&&o.hasWidget&&(a++,o.drifts&&o.drifts.length>0&&(r++,t.push({file:path.relative(n,e),...o})))}catch(e){}const i=Date.now()-o;if(e.json)return void console.log(JSON.stringify({elapsed_ms:i,total:l.length,annotated:a,drifted:r,results:t},null,2));if(console.log(c("gray",` Checked ${l.length} files in ${i}ms`)),console.log(""),console.log(` Files with Mother Code: ${c("cyan",a)}`),console.log(` Files without: ${c("yellow",l.length-a)}`),console.log(` Files with drift: ${r>0?c("red",r):c("green","0")}`),console.log(""),t.length>0){console.log(c("bold"," DRIFT DETECTED"));for(const e of t.slice(0,20)){console.log(` ${c("yellow","⚠")} ${c("cyan",e.file)}`);for(const o of e.drifts)console.log(` ${c("gray","→")} ${o.type}: ${o.message||o.detail||JSON.stringify(o)}`)}console.log("")}const g=l.length>0?Math.round(a/l.length*100):0,d=g>=80?"green":g>=40?"yellow":"red";console.log(` Mother Code coverage: ${c(d,g+"%")}`),console.log("")}async function cmdInject(e){const o=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," INJECTING MOTHER CODE: ")+c("cyan",o)),console.log(""));const n=collectFiles(o,e.ignore).filter(e=>e.endsWith(".js")||e.endsWith(".ts")||e.endsWith(".jsx")||e.endsWith(".tsx")||e.endsWith(".py")||e.endsWith(".java")||e.endsWith(".cs")||e.endsWith(".go")||e.endsWith(".kt")||e.endsWith(".rs")||e.endsWith(".php")||e.endsWith(".rb")),l=new DependencyGraph({rootPath:o});await l.build(n);const s=new WidgetGenerator({rootPath:o,dependencyGraph:l});let t=0,a=0;const r=[];for(const l of n)try{const n=fs.readFileSync(l,"utf-8");if(n.includes("@purpose")&&n.includes("@module")){a++;continue}const i=await s.generateWidget(l,n);if(!i){a++;continue}const g=i&&i.widgetString?i.widgetString:String(i),d=path.extname(l).toLowerCase();let h;if(h=".py"===d||".rb"===d?g.replace(/^\/\*\*\n/,"").replace(/\s*\*\/\s*$/,"").split("\n").map(e=>e.replace(/^\s*\*\s?/,"# ")).join("\n"):".go"===d||".rs"===d?g.replace(/^\/\*\*\n/,"").replace(/\s*\*\/\s*$/,"").split("\n").map(e=>e.replace(/^\s*\*\s?/,"// ")).join("\n"):g,e.fix){let s;if(".php"===d&&n.trimStart().startsWith("<?php")){const e=n.indexOf("<?php")+5,o=n.slice(e);s=n.slice(0,e)+"\n"+h+"\n"+o}else s=h+"\n\n"+n;fs.writeFileSync(l,s,"utf-8"),t++,e.json||console.log(` ${c("green","✓")} ${path.relative(o,l)}`)}else r.push({file:path.relative(o,l),widget:h}),t++}catch(e){a++}if(e.json)console.log(JSON.stringify({injected:t,skipped:a,dryRun:!e.fix,previews:r},null,2));else{if(console.log(""),e.fix)console.log(c("green",` ✓ Injected Mother Code into ${t} files`));else if(console.log(c("yellow",` DRY RUN — ${t} files would be annotated. Use --fix to apply.`)),e.verbose&&r.length>0){console.log("");for(const e of r.slice(0,5))console.log(` ${c("cyan",e.file)}:`),console.log(c("gray",e.widget.split("\n").map(e=>" "+e).join("\n"))),console.log("")}console.log(` ${c("gray",`Skipped: ${a} (already annotated or unsupported)`)}`),console.log("")}}async function cmdReport(e){const o=Date.now(),n=path.resolve(e.targetPath);e.json||(banner(),console.log(c("bold"," FULL REPORT: ")+c("cyan",n)),console.log(""));const l=collectFiles(n,e.ignore),s=new CodeScanner({rootPath:n}),t=new DependencyGraph({rootPath:n}),a=new DriftDetector({rootPath:n}),[r]=await Promise.all([s.scanFiles(l),t.build(l)]);let i=0,g=0;for(const e of l)try{const o=await a.analyzeFile(e);o&&o.hasWidget&&(g++,o.drifts&&o.drifts.length>0&&i++)}catch(e){}const d=Date.now()-o;let h=0,y=0,u=0;for(const e of r.issues||[])h++,"critical"===e.severity&&y++,"high"===e.severity&&u++;const p=t.getOrphanFiles(),f=t.circularDeps||[],m=l.length>0?Math.round(g/l.length*100):0,b=Math.max(0,Math.round(100-15*y-5*u-10*f.length-1*p.length-.1*(100-m)-3*i));if(e.json)return void console.log(JSON.stringify({elapsed_ms:d,files:l.length,health_score:b,issues:{total:h,critical:y,high:u},dependencies:{circular:f.length,orphans:p.length},mother_code:{annotated:g,coverage_pct:m,drifted:i}},null,2));const $=b>=80?"green":b>=60?"yellow":"red";console.log(c("bold"," ╔══════════════════════════════════════╗")),console.log(c("bold"," ║ THUBAN HEALTH REPORT ║")),console.log(c("bold"," ╚══════════════════════════════════════╝")),console.log(""),console.log(` ${c("bold","Health Score:")} ${c($,b+"/100")}`),console.log(` ${c("bold","Files Scanned:")} ${l.length}`),console.log(` ${c("bold","Time:")} ${d}ms`),console.log(""),console.log(c("bold"," ── Code Quality ──────────────────────")),console.log(` Total issues: ${h}`),console.log(` Critical: ${y>0?c("red",y):c("green","0")}`),console.log(` High: ${u>0?c("red",u):c("green","0")}`),console.log(""),console.log(c("bold"," ── Dependencies ─────────────────────")),console.log(` Circular deps: ${f.length>0?c("red",f.length):c("green","0")}`),console.log(` Orphan files: ${p.length>0?c("yellow",p.length):c("green","0")}`),console.log(""),console.log(c("bold"," ── Mother Code ──────────────────────")),console.log(` Annotated files: ${g}`),console.log(` Coverage: ${c(m>=80?"green":m>=40?"yellow":"red",m+"%")}`),console.log(` Drifted files: ${i>0?c("yellow",i):c("green","0")}`),console.log("");const w=b>=90?"A":b>=80?"B":b>=70?"C":b>=60?"D":"F",v=b>=80?"green":b>=60?"yellow":"red";console.log(` ${c("bold","Grade:")} ${c(v,w)}`),console.log(""),b<80&&(console.log(c("bold"," RECOMMENDATIONS:")),y>0&&console.log(` ${c("red","→")} Fix ${y} critical issues immediately`),f.length>0&&console.log(` ${c("yellow","→")} Resolve ${f.length} circular dependencies`),m<50&&console.log(` ${c("yellow","→")} Run ${c("cyan","thuban inject --fix")} to improve Mother Code coverage`),i>0&&console.log(` ${c("yellow","→")} ${i} files have drifted from their annotations`),p.length>5&&console.log(` ${c("gray","→")} Consider removing ${p.length} orphan files`),console.log(""))}async function cmdDebt(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore);e.json||(banner(),console.log(c("bold"," TECH DEBT ANALYSIS: ")+c("cyan",o)),console.log(c("gray",` Scanning ${n.length} files...`)),console.log(""));const l=new TechDebtAnalyzer({rootPath:o}),s=await l.analyze(n);e.json?console.log(JSON.stringify(l.toJSON(s),null,2)):console.log(l.formatReport(s))}async function cmdFix(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore),{execSync:l}=require("child_process");e.json||(banner(),console.log(c("bold"," AUTO-FIX TECH DEBT: ")+c("cyan",o)),console.log(""));const s=new TechDebtAnalyzer({rootPath:o});if(!e.fix){const o=await s.fix(n,{dryRun:!0});if(e.json)console.log(JSON.stringify(o,null,2));else{console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," DRY RUN — no files will be changed ")+c("cyan","║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log(""),console.log(` Fixable items: ${c("bold",o.totalFixable)}`),console.log("");const e={},n=["break_circular","extract_to_env","update_deprecated_api","wrap_localhost_url"];let l=0,s=0;for(const c of o.fixes){const o=c.fixAction||"unknown";e[o]||(e[o]=0),e[o]++,n.includes(o)?s++:l++}console.log(c("green",c("bold",` SAFE FIXES (${l})`))+c("gray"," — no runtime behaviour change"));for(const[o,l]of Object.entries(e).sort((e,o)=>o[1]-e[1])){if(n.includes(o))continue;const e={inject_widget:"Inject Mother Code DNA annotations",regenerate_widget:"Update stale annotations",remove_console_log:"Remove debug console.logs",flag_for_removal:"Confirm orphan files for removal"}[o]||o;console.log(` ${c("green","→")} ${e}: ${c("bold",l)}`)}if(console.log(""),s>0){console.log(c("yellow",c("bold",` UNSAFE FIXES (${s})`))+c("gray"," — may change runtime behaviour"));for(const[o,l]of Object.entries(e).sort((e,o)=>o[1]-e[1])){if(!n.includes(o))continue;const e={break_circular:"Restructure circular dependencies",extract_to_env:"Extract hardcoded secrets to env vars",update_deprecated_api:"Replace deprecated API calls with modern equivalents",wrap_localhost_url:"Wrap hardcoded localhost URLs in env var fallback"}[o]||o;console.log(` ${c("yellow","!")} ${e}: ${c("bold",l)}`)}console.log(""),console.log(c("gray"," Unsafe fixes require --unsafe flag:")),console.log(` ${c("cyan","thuban fix [path] --fix --unsafe")}`),console.log("")}console.log(c("bold"," TO APPLY:")),console.log(` ${c("cyan","thuban fix [path] --fix")} Apply safe fixes only`),console.log(` ${c("cyan","thuban fix [path] --fix --unsafe")} Apply all fixes`),console.log(` ${c("cyan","thuban fix [path] --fix --commit")} Each fix = separate git commit`),console.log("")}return}e.json||(e.safe?console.log(c("green"," SAFE MODE")+c("gray"," — only non-runtime-changing fixes will be applied")):console.log(c("yellow"," UNSAFE MODE")+c("gray"," — all fixes including runtime changes")),console.log(""));const t=await s.fix(n,{dryRun:!1,safeOnly:e.safe});if(e.gitCommit&&t.fixed>0)try{l("git rev-parse --git-dir",{cwd:o,stdio:"pipe"}),l("git add -A",{cwd:o,stdio:"pipe"});const n=`fix(thuban): auto-fix ${t.fixed} issues [safe=${e.safe}]`;l(`git commit -m "${n}"`,{cwd:o,stdio:"pipe"}),e.json||(console.log(c("green",` ✓ Changes committed: "${n}"`)),console.log(c("gray"," Revert with: git revert HEAD")),console.log(""))}catch(o){e.json||(console.log(c("yellow"," ⚠ Could not auto-commit: "+(o.message||"not a git repo"))),console.log(""))}if(e.json)console.log(JSON.stringify(t,null,2));else{console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," FIX RESULTS ")+c("cyan","║")),console.log(c("cyan"," ╠══════════════════════════════════════════════════════╣"));const o=(t.fixes||[]).filter(e=>"fixed"===e.status&&("inject_widget"===e.fixAction||"regenerate_widget"===e.fixAction)).length,n=t.fixed-o;o>0&&console.log(c("cyan"," ║")+` Annotations added: ${c("green",String(o).padEnd(19))}`+c("cyan"," ║")),n>0&&console.log(c("cyan"," ║")+` Issues fixed: ${c("green",String(n).padEnd(20))}`+c("cyan"," ║")),0===o&&0===n&&console.log(c("cyan"," ║")+` Fixed: ${c("green",String(t.fixed).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Flagged for review: ${c("yellow",String(t.flagged).padEnd(19))}`+c("cyan"," ║")),t.failed>0&&console.log(c("cyan"," ║")+` Failed: ${c("red",String(t.failed).padEnd(20))}`+c("cyan"," ║")),t.skipped>0&&console.log(c("cyan"," ║")+` Skipped: ${c("gray",String(t.skipped).padEnd(20))}`+c("cyan"," ║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝")),console.log(""),t.fixed>0&&(console.log(c("green"," Your files have been updated. Changes are saved to disk.")),e.gitCommit||console.log(c("gray"," Tip: use --commit next time to auto-commit each fix for easy rollback.")),console.log(""),console.log(c("gray"," Verify the changes:")),console.log(c("gray"," ")+c("cyan","npx thuban scan .")+c("gray"," Re-scan to see your new score")),console.log(c("gray"," ")+c("cyan","npx thuban dashboard .")+c("gray"," Generate updated report")),console.log("")),t.skipped>0&&(console.log(c("gray",` ${t.skipped} items were skipped (no auto-fix available or pattern not matched).`)),console.log(c("gray"," Review them manually or run: ")+c("cyan","npx thuban dashboard .")),console.log("")),t.flagged>0&&(console.log(c("gray",` ${t.flagged} items need human review (secrets, orphan files, complex patterns).`)),console.log(c("gray"," See details: ")+c("cyan","npx thuban dashboard .")),console.log(""))}}async function cmdHallucinate(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore);e.json||(banner(),console.log(c("bold"," HALLUCINATION SCAN: ")+c("cyan",o)),console.log(c("gray",` Scanning ${n.length} files for AI hallucinations...`)),console.log(""));const l=new HallucinationDetector({rootPath:o}),s=await l.scan(n);e.json?console.log(JSON.stringify(s,null,2)):console.log(l.formatReport(s))}async function cmdWatch(e){const o=path.resolve(e.targetPath),n=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),c=new HallucinationDetector({rootPath:o}),l=new FileWatcher({rootPath:o,ignorePatterns:["node_modules",".git","dist",".next","__pycache__",...e.ignore]});process.on("SIGINT",()=>{l.stop(),process.exit(0)}),l.start(async e=>{const o=[],l=await n.scanFile(e);l&&o.push(...l);const s=await c.scan([e]);if(s.phantomAPIs.length>0)for(const e of s.phantomAPIs)o.push({severity:"high",message:`Phantom API: ${e.name} — ${e.suggestion}`,line:e.line});if(s.phantomImports.length>0)for(const e of s.phantomImports)o.push({severity:"critical",message:`Phantom import: ${e.module} — ${e.reason}`,line:e.line});if(s.aiSmells.length>0)for(const e of s.aiSmells)o.push({severity:"warning",message:`AI smell: ${e.message}`,line:e.line});return o})}async function cmdDashboard(e){const o=path.resolve(e.targetPath),n=collectFiles(o,e.ignore);e.json||(banner(),console.log(c("bold"," GENERATING DASHBOARD: ")+c("cyan",o)),console.log(c("gray",` Scanning ${n.length} files...`)),console.log(""));const l=new TechDebtAnalyzer({rootPath:o}),s=await l.analyze(n),t=new DependencyGraph({rootPath:o});await t.build(n);const a={circular:t.circularDeps||[],orphans:t.getOrphanFiles()||[],critical:t.getMostCriticalFiles(5)||[]},r=new HallucinationDetector({rootPath:o}),i=await r.scan(n),g=new HtmlReportGenerator({rootPath:o,outputDir:o}),d=path.basename(o),h=await g.writeReport(s,a,{projectName:d,hallucinations:i});e.json?console.log(JSON.stringify({outputPath:h,scores:s.scores,stats:s.stats},null,2)):(console.log(c("green"," ✓ Dashboard generated:")),console.log(` ${c("cyan",h)}`),console.log(""),console.log(c("gray"," Open in your browser to view the interactive report")),console.log(""))}async function cmdDiff(e){const o=path.resolve(e.targetPath),{execSync:n}=require("child_process");e.json||(banner(),console.log(c("bold"," DIFF SCAN: ")+c("cyan",o)));let l=[];try{let e="main";try{n("git rev-parse --verify main",{cwd:o,stdio:"pipe"})}catch{try{n("git rev-parse --verify master",{cwd:o,stdio:"pipe"}),e="master"}catch{e="HEAD~1"}}const c=n(`git diff --name-only ${e}...HEAD`,{cwd:o,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim(),s=n("git diff --name-only HEAD",{cwd:o,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim(),t=n("git ls-files --others --exclude-standard",{cwd:o,encoding:"utf-8",stdio:["pipe","pipe","pipe"]}).trim(),a=new Set([...c.split("\n").filter(Boolean),...s.split("\n").filter(Boolean),...t.split("\n").filter(Boolean)]);for(const e of a){const n=path.resolve(o,e),c=path.extname(e).toLowerCase();[".js",".ts",".jsx",".tsx",".mjs",".cjs",".json"].includes(c)&&fs.existsSync(n)&&l.push(n)}}catch(o){e.json||console.log(c("red",` ✗ Not a git repository or git error: ${o.message}`)),process.exit(1)}if(0===l.length)return void(e.json?console.log(JSON.stringify({changedFiles:0,issues:0})):console.log(c("green"," ✓ No changed files to scan")));e.json||console.log(c("gray",` Scanning ${l.length} changed file${1===l.length?"":"s"}...\n`));const s=new CodeScanner({rootPath:o}),t=new HallucinationDetector({rootPath:o}),a=await s.scanFiles(l),r=await t.scan(l);let i=0;const g={};for(const e of a.issues||[]){const n=path.relative(o,e.file);g[n]||(g[n]=[]),g[n].push(e),i++}if(i+=r.stats.totalIssues,e.json)console.log(JSON.stringify({changedFiles:l.length,totalIssues:i,codeIssues:g,hallucinations:r},null,2));else{for(const[e,o]of Object.entries(g)){console.log(` ${c("cyan",e)}`);for(const e of o.slice(0,5)){const o="critical"===e.severity||"high"===e.severity?"red":"warning"===e.severity?"yellow":"gray";console.log(` ${c(o,"✗")} ${c("gray",e.message||e.id||"")}`)}o.length>5&&console.log(` ${c("gray",`... and ${o.length-5} more`)}`),console.log("")}r.stats.totalIssues>0&&console.log(t.formatReport(r));const e=0===i?"green":"yellow";console.log(` ${c(e,`${l.length} files changed, ${i} new issues introduced`)}\n`)}}async function cmdActivate(e){banner();const o=new LicenseManager,n=e.licenseKey;if(!n)return console.log(c("red"," Usage: thuban activate <license-key>")),console.log(""),console.log(c("gray"," Get a key at: ")+c("cyan","https://thuban.dev/pricing")),void console.log("");const l=o.activate(n);if(l.success)if("trial"===l.tier){const e=new Date(l.expiresAt).toLocaleDateString("en-GB",{day:"numeric",month:"long",year:"numeric"});console.log(c("cyan"," ╔══════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," Trial activated — Pro features unlocked! ")+c("cyan","║")),console.log(c("cyan"," ╚══════════════════════════════════════════════╝")),console.log(""),console.log(` Tier: ${c("bold","Trial (Pro features)")}`),console.log(` Status: ${c("green","Active")}`),console.log(` Days left: ${c("cyan",l.daysRemaining+" days")}`),console.log(` Expires: ${c("gray",e)}`),console.log(""),console.log(c("gray"," Unlimited scans, all languages, full security reports.")),console.log(c("gray"," Run ")+c("cyan","npx thuban scan .")+c("gray"," to get started.")),console.log(""),console.log(c("gray"," Love it? Upgrade at: ")+c("cyan","https://thuban.dev/pricing")),console.log("")}else console.log(c("green"," ╔══════════════════════════════════════╗")),console.log(c("green"," ║")+c("bold"," License activated! ")+c("green","║")),console.log(c("green"," ╚══════════════════════════════════════╝")),console.log(""),console.log(` Tier: ${c("bold",l.tierName)}`),console.log(` Status: ${c("green","Active")}`),console.log(""),console.log(c("gray"," All features unlocked. Run ")+c("cyan","thuban scan .")+c("gray"," to get started.")),console.log("");else console.log(c("red"," Invalid license key.")),console.log(""),l.error&&l.error.includes("expired")?console.log(c("yellow",` ${l.error}`)):console.log(c("gray"," Check for typos or get a new key at: ")+c("cyan","https://thuban.dev/pricing")),console.log("")}async function cmdStatus(e){banner();const o=(new LicenseManager).getStatus();if(console.log(c("bold"," LICENSE STATUS")),console.log(""),console.log(` Tier: ${c("bold",o.tierName)}${o.licensed?c("green"," (active)"):""}`),console.log(` Machine ID: ${c("gray",o.machineId)}`),console.log(` Scans this month: ${c("bold",o.scansUsed)}${o.scansLimit<1/0?c("gray"," / "+o.scansLimit):""}`),console.log(` Lifetime scans: ${c("gray",o.totalScans)}`),"trial"===o.tier&&void 0!==o.trialDaysRemaining){const e=new Date(o.trialExpiresAt).toLocaleDateString("en-GB",{day:"numeric",month:"long",year:"numeric"}),n=o.trialDaysRemaining<=3?"red":o.trialDaysRemaining<=7?"yellow":"cyan";console.log(""),console.log(c("cyan"," ╔══════════════════════════════════════════════════════╗")),console.log(c("cyan"," ║")+c("bold"," TRIAL KEY ACTIVE ")+c("cyan","║")),console.log(c("cyan"," ╠══════════════════════════════════════════════════════╣")),console.log(c("cyan"," ║")+` Trial expires: ${c(n,e.padEnd(36))}`+c("cyan"," ║")),console.log(c("cyan"," ║")+` Days remaining: ${c(n,String(o.trialDaysRemaining).padEnd(35))}`+c("cyan","║")),console.log(c("cyan"," ║")+" Love it? Upgrade to keep full access: "+c("cyan","║")),console.log(c("cyan"," ║")+" "+c("bold","https://thuban.dev/pricing")+" "+c("cyan","║")),console.log(c("cyan"," ╚══════════════════════════════════════════════════════╝"))}else o.trialExpired&&(console.log(""),console.log(c("yellow"," Your Thuban trial has expired.")),console.log(c("gray"," Upgrade to keep unlimited scans and full reports:")),console.log(c("cyan"," https://thuban.dev/pricing")));console.log(""),console.log(c("bold"," FEATURES")),console.log(` Full scan details: ${o.features.fullDetails?c("green","Yes"):c("red","No")}`),console.log(` HTML dashboard: ${o.features.dashboard?c("green","Yes"):c("red","No")}`),console.log(` Auto-fix: ${o.features.autoFix?c("green","Yes"):c("red","No")}`),console.log(` CI Action: ${o.features.ciAction?c("green","Yes"):c("red","No")}`),console.log(""),o.licensed||(console.log(c("cyan"," Upgrade at: ")+c("bold","https://thuban.dev/pricing")),console.log(""))}function cmdUpgrade(){banner(),console.log(c("bold"," THUBAN PRICING")),console.log(""),console.log(` ${c("gray","FREE")} ${c("bold","£0/mo")}`),console.log(" 5 scans/month, 100 files, summary only"),console.log(""),console.log(` ${c("cyan","PRO")} ${c("bold","£19/mo")} ${c("gray","(or £149/year — save 35%)")}`),console.log(" Unlimited scans, full details, auto-fix, HTML dashboard"),console.log(""),console.log(` ${c("cyan","TEAM")} ${c("bold","£99/mo")}`),console.log(" Everything in Pro + 5 seats, CI Action, team reports"),console.log(""),console.log(` ${c("cyan","ENTERPRISE")} ${c("bold","£299/mo")}`),console.log(" Everything in Team + unlimited seats, priority support, SLA"),console.log(""),console.log(c("bold"," Subscribe at: ")+c("cyan","https://thuban.dev/pricing")),console.log("");try{const{exec:e}=require("child_process"),o=process.platform;e(("win32"===o?"start":"darwin"===o?"open":"xdg-open")+" https://thuban.dev/pricing")}catch(e){}}async function cmdGate(e){const o=path.resolve(e.targetPath);if(e.fix){const e=PreCommitGate.install(o);return banner(),e.success?(console.log(c("green"," ✓ Pre-commit gate installed!")),console.log(""),console.log(c("gray"," Every commit will now be checked for hallucinated APIs.")),console.log(c("gray"," Commits with phantom APIs will be blocked automatically.")),console.log(""),console.log(` To remove: ${c("cyan","thuban gate --uninstall")}`)):console.log(c("red",` Error: ${e.error}`)),void console.log("")}if(process.argv.includes("--uninstall")){const e=PreCommitGate.uninstall(o);return banner(),console.log(c("green",` ✓ ${e.message}`)),void console.log("")}const n=new PreCommitGate({rootPath:o,strict:!process.argv.includes("--warn")}).run();if(e.json)return void console.log(JSON.stringify(n,null,2));const l=process.argv.includes("--ci");if(l||banner(),0===n.files)return console.log(c("gray"," No staged files to check.")),console.log(""),console.log(` Install the gate: ${c("cyan","thuban gate --fix")}`),void console.log("");if(n.passed)console.log(c("green",` ✓ ${n.message}`)),console.log(c("gray",` ${n.elapsed}ms`)),l||console.log(""),process.exit(0);else{console.log(c("red",` ✗ ${n.message}`)),console.log("");for(const e of n.issues)console.log(` ${c("red",e.file)}:${e.line}`),console.log(` ${c("yellow",e.code)}`),console.log(` ${c("red","^^^")} ${e.message}`),e.fix&&console.log(` ${c("green","Fix:")} ${e.fix}`),console.log("");process.exit(1)}}async function cmdCost(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," SCANNING: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),s=new HallucinationDetector({rootPath:o}),[t,a]=await Promise.all([l.scanFiles(n),s.scan(n)]),r=[];for(const e of t.issues||[])r.push({file:path.relative(o,e.file),...e});for(const e of a.phantomAPIs)r.push({category:"hallucination",severity:"critical",message:e.name});for(const e of a.deprecatedAPIs)r.push({category:"deprecated",severity:"high",message:e.name});const i=new TechDebtCostCalculator,g=i.calculate(r,n.length);e.json?console.log(JSON.stringify(g,null,2)):console.log(i.formatCLI(g))}async function cmdGhosts(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," SCANNING FOR GHOST CODE: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new GhostCodeDetector({rootPath:o}).scan(n);if(e.json)console.log(JSON.stringify(l,null,2));else{if(0===l.ghosts.length)return console.log(c("green"," No ghost code detected. Every function is referenced.")),void console.log("");console.log(c("yellow",c("bold",` GHOST CODE: ${l.totalGhosts} function${l.totalGhosts>1?"s":""} exist but are never called`))),console.log("");for(const o of l.ghosts.slice(0,e.maxIssues)){const e="high"===o.severity?"red":"yellow";console.log(` ${c(e,"⊘")} ${c("cyan",o.file)}:${o.line}`),console.log(` ${c("bold",o.name+"()")} — ${o.wastedLines} lines, ${o.references} references`),console.log("")}console.log(c("bold"," ┌──────────────────────────────────────────────────────┐")),console.log(c("bold"," │")+` Wasted code: ${c("red",l.totalWastedLines+" lines")} (${l.wastedPercent}% of codebase) `+c("bold","│")),console.log(c("bold"," └──────────────────────────────────────────────────────┘")),console.log("")}}async function cmdAIScore(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," AI CONFIDENCE SCORING: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new AIConfidenceScorer({rootPath:o}),s=await l.scan(n);if(e.json)console.log(JSON.stringify(s,null,2));else{if(0===s.highConfidence.length&&0===s.mediumConfidence.length)return console.log(c("green"," No AI-generated code patterns detected.")),void console.log("");console.log(c("red",c("bold",` HIGH CONFIDENCE AI-GENERATED (${s.highConfidence.length} functions)`))),console.log("");for(const e of s.highConfidence.slice(0,15)){console.log(` ${c("red",e.confidence+"%")} AI ${c("cyan",e.file)}:${e.line}`),console.log(` ${c("bold",e.name+"()")} — ${e.lineCount} lines | Test coverage: ${e.testCoverage}`);for(const o of e.signals.slice(0,2))console.log(` ${c("gray","• "+o)}`);console.log("")}if(s.mediumConfidence.length>0){console.log(c("yellow",c("bold",` POSSIBLE AI-GENERATED (${s.mediumConfidence.length} functions)`))),console.log("");for(const e of s.mediumConfidence.slice(0,10))console.log(` ${c("yellow",e.confidence+"%")} AI ${c("cyan",e.file)}:${e.line} ${c("gray",e.name+"()")}`);console.log("")}console.log(c("bold"," ┌──────────────────────────────────────────────────────┐")),console.log(c("bold"," │")+` ${c("red",s.aiPercentage+"%")} of functions are likely AI-generated `+c("bold","│")),console.log(c("bold"," │")+` ${c("yellow",s.aiLikelyCount)} high confidence | ${s.aiPossibleCount} possible `+c("bold","│")),console.log(c("bold"," └──────────────────────────────────────────────────────┘")),console.log("")}}async function cmdClones(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," COPY-PASTE DRIFT DETECTION: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new CopyPasteDriftDetector({rootPath:o}).scan(n);if(e.json)console.log(JSON.stringify(l,null,2));else{if(0===l.clusters.length)return console.log(c("green"," No copy-paste drift detected.")),void console.log("");console.log(c("yellow",c("bold",` COPY-PASTE CLUSTERS: ${l.totalClusters} found`))),console.log("");for(const e of l.clusters.slice(0,10)){const o="high"===e.severity?"red":"yellow";console.log(` ${c(o,"◈")} ${c("bold",e.pattern+" pattern")} — ${e.totalInstances} instances`);for(const o of e.members){const e=o.similarity<100?c("gray",` (${o.similarity}% similar)`):"";console.log(` ${c("cyan",o.file)}:${o.line} ${c("gray",o.name+"()")}${e}`)}console.log(` ${c("green","→")} ${e.recommendation}`),console.log("")}console.log(c("bold",` Total duplicate lines: ${c("red",l.totalDuplicateLines)}`)),console.log(c("gray",` ${l.recommendation}`)),console.log("")}}async function cmdPassport(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," GENERATING CODEBASE PASSPORT: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore),l=new CodebasePassport({rootPath:o}),s=l.generate(n);if(e.json)return void console.log(JSON.stringify(s,null,2));const t=l.save(s),a=s.project,r=s.onboarding;console.log(c("green",` ✓ Passport generated: ${c("bold",path.relative(o,t))}`)),console.log(""),console.log(c("bold"," PROJECT IDENTITY")),console.log(` Name: ${c("cyan",a.name)}`),console.log(` Files: ${c("bold",a.totalFiles)}`),console.log(` Lines: ${c("bold",a.totalLines.toLocaleString())}`),console.log(` Architecture: ${c("bold",a.architecture)}`),console.log(` Frameworks: ${a.frameworks.join(", ")||"vanilla"}`),console.log(""),console.log(c("bold"," LANGUAGES"));for(const e of a.languages.slice(0,5)){const o="█".repeat(Math.max(1,Math.round(e.percentage/5)));console.log(` ${e.extension.padEnd(6)} ${c("cyan",o)} ${e.percentage}% (${e.lines} lines)`)}if(console.log(""),console.log(c("bold"," ONBOARDING")),console.log(` Read time: ~${r.estimatedReadTimeMinutes} minutes`),console.log(` Start here: ${r.startHere.slice(0,3).join(", ")}`),r.doNotTouch.length>0&&console.log(` Do not touch: ${c("red",r.doNotTouch.join(", "))}`),console.log(` Summary: ${r.quickSummary}`),console.log(""),s.sacred.length>0){console.log(c("bold"," SACRED FILES (high impact if changed)"));for(const e of s.sacred.slice(0,5))console.log(` ${c("red","!")} ${c("cyan",e.file)} — imported by ${e.importedBy} files`);console.log("")}console.log(c("gray",` Full passport: ${t}`)),console.log(c("gray"," Share with new team members for instant onboarding.")),console.log("")}async function cmdExecutive(e){const o=path.resolve(e.targetPath);banner(),console.log(c("bold"," GENERATING EXECUTIVE REPORT: ")+c("cyan",o)),console.log("");const n=collectFiles(o,e.ignore);console.log(c("gray",` Scanning ${n.length} files...`));const l=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),s=new HallucinationDetector({rootPath:o}),t=new GhostCodeDetector({rootPath:o}),a=new AIConfidenceScorer({rootPath:o}),r=new CopyPasteDriftDetector({rootPath:o}),[i,g,d,h,y]=await Promise.all([l.scanFiles(n),s.scan(n),t.scan(n),a.scan(n),r.scan(n)]);let u=0;for(const e of n)try{u+=fs.readFileSync(e,"utf-8").split("\n").length}catch{}const p=[];for(const e of i.issues||[])p.push({file:path.relative(o,e.file),...e});for(const e of g.phantomAPIs)p.push({category:"hallucination",severity:"critical",message:e.name});for(const e of g.deprecatedAPIs)p.push({category:"deprecated",severity:"high",message:e.name});const f=(new TechDebtCostCalculator).calculate(p,n.length),m=new ExecutiveReport({rootPath:o,companyName:e.companyName||void 0}),b=m.generate({scanResults:i,hallResults:g,debtCost:f,ghostResults:d,aiScoreResults:h,cloneResults:y,fileCount:n.length,lineCount:u}),$=m.save(b,o);console.log(c("green"," ✓ Executive report generated!")),console.log(""),console.log(` ${c("bold","Report:")} ${c("cyan",$)}`),console.log(""),console.log(c("gray"," Open in browser → Ctrl+P → Save as PDF")),console.log(c("gray",' Or click the "Save as PDF" button in the report.')),console.log("");try{const{exec:e}=require("child_process"),o=process.platform;e(`${"win32"===o?"start":"darwin"===o?"open":"xdg-open"} "${$}"`),console.log(c("green"," ✓ Opened in your default browser.")),console.log("")}catch(e){}}async function cmdBaseline(e){const o=path.resolve(e.targetPath),n=new BaselineManager(o);if(banner(),e.baselineCreate||!n.exists()){console.log(c("bold"," CREATING BASELINE: ")+c("cyan",o)),console.log("");const l=collectFiles(o,e.ignore),s=new CodeScanner({rootPath:o,ignorePatterns:["node_modules/**",".git/**","dist/**",...e.ignore]}),t=new HallucinationDetector({rootPath:o}),[a,r]=await Promise.all([s.scanFiles(l),t.scan(l)]),i=[];for(const e of a.issues||[])i.push({file:path.relative(o,e.file),...e});for(const e of r.phantomAPIs)i.push({file:e.file,category:"hallucination",severity:"critical",id:e.id,message:e.name});for(const e of r.deprecatedAPIs)i.push({file:e.file,category:"deprecated",severity:"high",id:e.id,message:e.name});for(const e of r.aiSmells)i.push({file:e.file,category:"ai_smell",severity:"warning",id:e.id,message:e.message});const g=n.create(i);console.log(c("green",` ✓ Baseline created: ${c("bold",g.count)} known issues recorded`)),console.log(` ${c("cyan",g.path)}`),console.log(""),console.log(c("gray"," Future scans with --baseline will only report NEW issues.")),console.log(c("gray"," Commit .thuban-baseline.json to share baseline with your team.")),console.log("")}else{const e=n.load();console.log(c("bold"," BASELINE STATUS")),console.log(""),console.log(` Created: ${c("cyan",e.created)}`),console.log(` Known issues: ${c("bold",e.issueCount)}`),console.log(""),console.log(c("gray"," Use --baseline-create to update the baseline")),console.log(c("gray"," Use --baseline with scan to only see new issues")),console.log("")}}async function cmdCrucible(e){const o=process.argv.slice(2),n=o[1]||"",l=path.resolve(e.targetPath);if("ci"===n){const e=o[2]||"",n=CIIntegration.listPlatforms();if(!e||!n.includes(e))return console.log(""),console.log(c("bold"," CRUCIBLE CI INTEGRATION")),console.log(""),console.log(c("gray"," Usage:")),console.log(` ${c("cyan","thuban crucible ci github")} → .github/workflows/thuban-crucible.yml`),console.log(` ${c("cyan","thuban crucible ci gitlab")} → .gitlab-ci.yml`),console.log(` ${c("cyan","thuban crucible ci bitbucket")} → bitbucket-pipelines.yml`),console.log(""),e&&(console.log(c("red",` Unknown platform: ${e}`)),console.log(c("gray",` Supported: ${n.join(", ")}`))),void console.log("");const s=o.find(e=>e.startsWith("--threshold=")),t=o.find(e=>e.startsWith("--level=")),a=o.find(e=>e.startsWith("--node=")),r=s?parseInt(s.split("=")[1],10):70,i=t?t.split("=")[1]:"gentle",g=a?a.split("=")[1]:"20";try{const o=CIIntegration.generateCIConfig(e,l,{threshold:r,level:i,nodeVersion:g});console.log(""),console.log(c("green"," ✓ CI config generated")),console.log(` ${c("cyan",o.filename)}`),console.log(""),console.log(c("gray",` Config: threshold=${o.threshold}, level=${o.level}`)),console.log(c("gray"," Commit this file and Crucible will run on every PR.")),console.log("")}catch(e){console.error(c("red",` Error: ${e.message}`))}return}if("badge"===n){const e=o.find(e=>e.startsWith("--score="));let n=e?parseInt(e.split("=")[1],10):null;if(null===n&&(n=BadgeGenerator.readLastScore(l)),null===n)return console.log(""),console.log(c("yellow"," No Crucible report found.")),console.log(c("gray"," Run `thuban crucible run .` first, or pass --score=<n>")),void console.log("");const s=o.find(e=>e.startsWith("--style=")),t=o.find(e=>e.startsWith("--threshold=")),a=s?s.split("=")[1]:"score",r=t?parseInt(t.split("=")[1],10):70,i=BadgeGenerator.generateBadge({score:n,targetPath:l,style:a,threshold:r});return console.log(""),console.log(c("green",` ✓ Badge generated: ${c("cyan",path.relative(l,i.outputPath))}`)),console.log(` Score: ${c("green"===i.color?"green":"yellow"===i.color?"yellow":"red",n+"/100")} (${i.verdict})`),console.log(""),console.log(c("bold"," Add to README.md:")),console.log(` ${c("cyan",i.markdown)}`),void console.log("")}if("leaderboard"!==n&&"lb"!==n){if("run"===n||"seed"===n){const n=parseFlag(o,"level","gentle"),s=parseIntFlag(o,"seed",42),t=parseFlag(o,"lang",null),a=parseFlag(o,"category",null);console.log(Crucible.banner.fullBanner({level:n,version:_CLI_VERSION})),console.log(c("bold"," Running adversarial seed tests...")),console.log(c("gray",` Level: ${n} · PRNG seed: ${s}`)),console.log("");let r=0,i=0,g=0;const d=await Crucible.run({targetPath:l,level:n,seed:s,languages:t?t.split(","):void 0,categories:a?a.split(","):void 0,onProgress(e,o,n){g++,"seed"===e&&(n.detected?(r++,process.stdout.write(c("green","."))):(i++,process.stdout.write(c("red","x"))),g%50==0&&process.stdout.write("\n"))}});process.stdout.write("\n\n"),console.log(c("bold"," Results:")+` ${c("green",d.seedResults.detected+" detected")} ${c("red",d.seedResults.missed+" missed")} (${d.seedResults.total} seeds)`),console.log(Crucible.banner.scoreBanner(d.score));const h=Crucible.compareGolden(d);return h.hasMaster?console.log(h.passed?c("green"," ✓ No regressions vs golden master"):c("red",` ✗ ${h.regressions} regression(s) vs golden master`)):console.log(c("gray"," (No golden master — run `thuban crucible approve` to set one)")),console.log(""),e.json&&console.log(JSON.stringify(d,null,2)),BadgeGenerator.writeLastScore&&BadgeGenerator.writeLastScore(l,d.score),void(Leaderboard.addEntry&&Leaderboard.addEntry(l,{score:d.score,level:n,timestamp:d.timestamp}))}if("approve"===n){console.log(""),console.log(c("bold"," Approving current results as golden master..."));const e=await Crucible.run({targetPath:l,level:"gentle",seed:42}),o=Crucible.approve(e);return console.log(c("green",` ✓ Golden master saved: ${o.count} findings, score ${o.meta.score}`)),console.log(c("gray"," File: .golden/golden-master.json")),void console.log("")}if("kenny"===n){console.log(Crucible.banner.compactBanner("KENNY MODE")),console.log(c("bold",` Kenny's 7-report test battery (${Crucible.KennyMode.testCount} tests)`)),console.log("");const o=await Crucible.kenny({onProgress(e,o){const n=o.passed?c("green","✓"):o.crashed?c("yellow","⚡"):c("red","✗"),l=c("gray",e.id)+" "+e.name,s=o.missingDetections&&o.missingDetections.length?c("red"," missing: "+o.missingDetections.join(", ")):"";console.log(` ${n} ${l}${s}`)}});return console.log(Crucible.banner.summaryLine(o.passed,o.total-o.skipped,o.score)),void(e.json&&console.log(JSON.stringify(o,null,2)))}if("self-test"===n||"selftest"===n){console.log(Crucible.banner.compactBanner("SELF-ATTACK")),console.log(c("bold",` Adversarial self-attack (${Crucible.SelfAttack.payloadCount} payloads)`)),console.log("");const o=await Crucible.selfTest({onProgress(e,o){const n="pass"===o.type?c("green","✓"):"crash"===o.type?c("red","⚡ CRASH"):"hang"===o.type?c("yellow","⏱ HANG"):c("yellow","? MISSED");console.log(` ${n} ${c("gray",e.name)} ${e.description}`)}});console.log("");const n=o.crashes.length,l=o.hangs.length;return 0===n&&0===l?console.log(c("green",` ✓ All ${o.payloadCount} payloads handled gracefully`)):(n&&console.log(c("red",` ✗ ${n} crash(es) detected`)),l&&console.log(c("yellow",` ⚠ ${l} hang(s) detected`))),console.log(Crucible.banner.scoreBanner(o.score)),void(e.json&&console.log(JSON.stringify(o,null,2)))}if("report"===n){console.log(""),console.log(c("bold"," Generating Crucible report..."));const o=await Crucible.run({targetPath:l,level:"gentle",seed:42}),n=await Crucible.kenny(),s=Crucible.compareGolden(o),t={title:"Thuban Crucible Report",level:"gentle",score:o.score,seedResults:o.seedResults,mutationResults:o.mutationResults,kennyResults:n,goldenComparison:s.hasMaster?s:null},a=e.output?path.resolve(e.output):l,{htmlPath:r,jsonPath:i}=Crucible.report(t,{outputDir:a});return console.log(c("green",` ✓ HTML report: ${c("cyan",r)}`)),console.log(c("green",` ✓ JSON summary: ${c("cyan",i)}`)),void console.log(Crucible.banner.scoreBanner(o.score))}if("golden"===n){const e=(new Crucible.GoldenMaster).loadMaster();return console.log(""),e?(console.log(c("bold"," Golden Master Status")),console.log(` Score: ${null!=e.meta.score?e.meta.score:"N/A"}`),console.log(` Findings: ${e.count}`),console.log(` Saved: ${e.timestamp}`),console.log(` Hash: ${e.hash}`)):console.log(c("yellow"," No golden master. Run `thuban crucible approve` to create one.")),void console.log("")}console.log(""),console.log(c("bold"," CRUCIBLE — Adversarial Testing Engine")),console.log(""),console.log(c("gray"," Core commands:")),console.log(` ${c("cyan","thuban crucible run")} [path] [--level gentle|medium|hell]`),console.log(` ${c("cyan","thuban crucible seed")} [path]`),console.log(` ${c("cyan","thuban crucible mutate")} [path]`),console.log(` ${c("cyan","thuban crucible golden")} [path]`),console.log(` ${c("cyan","thuban crucible approve")}`),console.log(` ${c("cyan","thuban crucible self-test")}`),console.log(` ${c("cyan","thuban crucible kenny")}`),console.log(` ${c("cyan","thuban crucible report")}`),console.log(""),console.log(c("gray"," CI/CD & badges:")),console.log(` ${c("cyan","thuban crucible ci github")} Generate GitHub Actions workflow`),console.log(` ${c("cyan","thuban crucible ci gitlab")} Generate GitLab CI config`),console.log(` ${c("cyan","thuban crucible ci bitbucket")} Generate Bitbucket Pipelines config`),console.log(` ${c("cyan","thuban crucible badge")} Generate SVG score badge for README`),console.log(` ${c("cyan","thuban crucible leaderboard")} View score history & trend`),console.log(""),console.log(c("gray"," Options:")),console.log(` ${c("gray","--threshold=<n>")} Fail CI if score drops below n (default 70)`),console.log(` ${c("gray","--level=<l>")} gentle | medium | hell`),console.log(` ${c("gray","--score=<n>")} Explicit score for badge (else reads last report)`),console.log(` ${c("gray","--style=score|verdict")} Badge text style`),console.log("")}else{if(e.json){const e=Leaderboard.getSummary(l);return void console.log(JSON.stringify(e,null,2))}Leaderboard.printLeaderboard(l,{color:!0})}}function getTelemetryConfigPath(){const e=process.env.HOME||process.env.USERPROFILE||os.homedir();return path.join(e,".thuban-telemetry.json")}function isTelemetryEnabled(){try{return!0===JSON.parse(fs.readFileSync(getTelemetryConfigPath(),"utf-8")).enabled}catch{return!1}}function sendAnonymousTelemetry(e){if(isTelemetryEnabled())try{const o=JSON.stringify({v:e.version||_CLI_VERSION,cmd:e.command,files:e.fileCount,score:e.score,grade:e.grade,langs:e.languages,issues:e.issueCount,duration:e.duration,os:process.platform,node:process.version,ts:(new Date).toISOString()}),n=require("https").request({hostname:"thuban-telemetry.silverwingsbenefits.workers.dev",path:"/report",method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(o)},timeout:3e3});n.on("error",()=>{}),n.write(o),n.end()}catch{}}async function cmdTelemetry(e){const o=getTelemetryConfigPath(),n=process.argv.slice(2);if(n.includes("--on"))fs.writeFileSync(o,JSON.stringify({enabled:!0,updated:(new Date).toISOString()})),console.log(""),console.log(c("cyan"," Telemetry enabled")),console.log(c("gray"," Anonymous usage stats will be sent (no code, no paths, no PII)")),console.log(c("gray"," Turn off anytime: npx thuban telemetry --off")),console.log("");else if(n.includes("--off"))fs.writeFileSync(o,JSON.stringify({enabled:!1,updated:(new Date).toISOString()})),console.log(""),console.log(c("cyan"," Telemetry disabled")),console.log(c("gray"," No data will be collected")),console.log("");else{const e=isTelemetryEnabled();console.log(""),console.log(c("bold"," TELEMETRY STATUS")),console.log(""),console.log(` Status: ${e?c("cyan","Enabled"):c("gray","Disabled (default)")}`),console.log(""),console.log(c("gray"," Thuban telemetry is 100% opt-in and anonymous.")),console.log(c("gray"," We collect: file count, score, grade, languages, OS, scan duration.")),console.log(c("gray"," We never collect: code, file names, paths, project names, or PII.")),console.log(""),console.log(` Enable: ${c("cyan","npx thuban telemetry --on")}`),console.log(` Disable: ${c("cyan","npx thuban telemetry --off")}`),console.log("")}}async function runScanSilent(e){const o=/^(https?:\/\/|git@)/.test(e);let n=o?e:path.resolve(e),c=null,l=e;if(o){const o=path.join(os.tmpdir(),"thuban-compare-"+Date.now()+"-"+Math.random().toString(36).slice(2,6)),{execFileSync:s}=require("child_process");s("git",["clone","--depth","1",e,o],{stdio:"pipe",timeout:6e4}),n=o,c=o;const t=e.replace(/\.git$/,"").split("/");l=t[t.length-1]||e}else l=path.basename(path.resolve(e))||e;const s=Date.now(),t=new CodeScanner,a=new DependencyGraph,r=new HallucinationDetector({rootPath:n}),i=collectFiles(n,[]),g={};let d=0,h=0;for(const e of i)try{const o=t.scanFile(e);path.relative(n,e),g[e]=o,a.addFile(e,n),o.motherCode&&o.motherCode.hasDNA&&d++,o.drifts&&o.drifts.length>0&&h++}catch(e){}const y=r.scan(i,n),u=Date.now()-s;let p=0,f=0,m=0;const b={};for(const[,e]of Object.entries(g))if(e.issues){p+=e.issues.length;for(const o of e.issues){"critical"===o.severity&&f++,"high"===o.severity&&m++;const e=o.category||o.type||"other";b[e]=(b[e]||0)+1}}const $=(y.phantomAPIs||[]).length+(y.phantomImports||[]).length+(y.deprecatedAPIs||[]).length;p+=$,f+=(y.phantomAPIs||[]).length+(y.phantomImports||[]).length,m+=(y.deprecatedAPIs||[]).length,$>0&&(b.hallucination=$);const w=a.getOrphanFiles(),v=a.circularDeps||[],S=i.length>0?Math.round(d/i.length*100):0,C=Math.max(0,Math.round(100-15*f-5*m-10*v.length-1*w.length-.1*(100-S)-3*h)),x=C>=90?"A":C>=80?"B":C>=70?"C":C>=60?"D":"F";if(c)try{fs.rmSync(c,{recursive:!0,force:!0})}catch{}return{label:l,files:i.length,score:C,grade:x,elapsed:u,issues:{total:p,critical:f,high:m},categories:b,dependencies:{circular:v.length,orphans:w.length},motherCode:{annotated:d,coverage:S,drifted:h}}}async function cmdCompare(e){const o=process.argv.slice(2).filter(e=>"compare"!==e&&!e.startsWith("--"));o.length<2&&(console.log(""),console.log(c("red"," Compare requires two paths or URLs")),console.log(""),console.log(c("gray"," Usage:")),console.log(` ${c("cyan","npx thuban compare ./project-a ./project-b")}`),console.log(` ${c("cyan","npx thuban compare . https://github.com/user/repo")}`),console.log(` ${c("cyan","npx thuban compare https://github.com/a/repo https://github.com/b/repo")}`),console.log(""),process.exit(1)),console.log(""),console.log(c("bold"," ╔══════════════════════════════════════╗")),console.log(c("bold"," ║ THUBAN — Codebase Comparison ║")),console.log(c("bold"," ╚══════════════════════════════════════╝")),console.log(""),console.log(c("cyan",` Scanning: ${o[0]}`));const n=await runScanSilent(o[0]);console.log(c("cyan",` Scanning: ${o[1]}`));const l=await runScanSilent(o[1]);console.log("");const s=(e,o)=>String(e).padEnd(o),t=(e,o)=>String(e).padStart(o),a=20,r=n.label.length>a?n.label.slice(0,18)+"..":n.label,i=l.label.length>a?l.label.slice(0,18)+"..":l.label,g=n.score>=80?"green":n.score>=60?"yellow":"red",d=l.score>=80?"green":l.score>=60?"yellow":"red";console.log(c("bold"," ┌──────────────────────┬──────────────────────┬──────────────────────┐")),console.log(c("bold",` │ ${s("METRIC",a)} │ ${s(r,a)} │ ${s(i,a)} │`)),console.log(c("bold"," ├──────────────────────┼──────────────────────┼──────────────────────┤"));const h=(e,o,n)=>{console.log(` │ ${c("gray",s(e,a))} │ ${s(o,a)} │ ${s(n,a)} │`)};h("Health Score",`${c(g,n.score+"/100")}`,`${c(d,l.score+"/100")}`),h("Grade",`${c(g,n.grade)}`,`${c(d,l.grade)}`),h("Files Scanned",String(n.files),String(l.files)),h("Total Issues",String(n.issues.total),String(l.issues.total)),h("Critical Issues",String(n.issues.critical),String(l.issues.critical)),h("High Issues",String(n.issues.high),String(l.issues.high)),h("Circular Deps",String(n.dependencies.circular),String(l.dependencies.circular)),h("Orphan Files",String(n.dependencies.orphans),String(l.dependencies.orphans)),h("Mother Code DNA",`${n.motherCode.coverage}%`,`${l.motherCode.coverage}%`),h("Drift Issues",String(n.motherCode.drifted),String(l.motherCode.drifted)),h("Scan Time",`${n.elapsed}ms`,`${l.elapsed}ms`),console.log(c("bold"," └──────────────────────┴──────────────────────┴──────────────────────┘")),console.log("");const y=n.score-l.score;if(0===y)console.log(c("cyan",` VERDICT: Both codebases score identically (${n.score}/100)`));else{const e=y>0?n:l,o=y>0?l:n;console.log(c("cyan",` VERDICT: ${e.label} is healthier`)),console.log(c("gray",` ${e.label} scores ${e.score}/100 (${e.grade}) vs ${o.label} at ${o.score}/100 (${o.grade})`)),console.log(c("gray",` ${Math.abs(y)} point difference — ${Math.abs(y)>20?"significant gap":Math.abs(y)>10?"moderate gap":"close race"}`))}const u=new Set([...Object.keys(n.categories),...Object.keys(l.categories)]);if(u.size>0){console.log(""),console.log(c("bold"," ISSUE BREAKDOWN"));for(const e of u){const o=n.categories[e]||0,a=l.categories[e]||0,g=e.charAt(0).toUpperCase()+e.slice(1);console.log(c("gray",` ${s(g,22)} ${t(o,5)} vs ${t(a,5)} ${o<a?c("green",`← ${r}`):o>a?c("green",`→ ${i}`):c("gray","tied")}`))}}console.log(""),console.log(c("gray",` Full scan: npx thuban scan ${o[0]}`)),console.log(c("gray",` Full scan: npx thuban scan ${o[1]}`)),console.log("")}async function main(){const e=parseArgs(process.argv.slice(2));if(e.command&&"help"!==e.command||(printHelp(),process.exit(0)),"version"===e.command){const e=JSON.parse(fs.readFileSync(path.join(__dirname,"package.json"),"utf-8"));console.log(`thuban v${e.version}`),process.exit(0)}["activate","status","upgrade","telemetry","compare","crucible"].includes(e.command)||e.targetPath.match(/^(https?:\/\/|git@)/)||fs.existsSync(e.targetPath)||(console.error(c("red",` Error: Path not found: ${e.targetPath}`)),process.exit(1));try{switch(e.command){case"scan":await cmdScan(e);break;case"deps":await cmdDeps(e);break;case"drift":await cmdDrift(e);break;case"inject":await cmdInject(e);break;case"health":case"report":await cmdReport(e);break;case"debt":await cmdDebt(e);break;case"fix":await cmdFix(e);break;case"hallucinate":case"hal":await cmdHallucinate(e);break;case"watch":await cmdWatch(e);break;case"monitor":await cmdMonitor(e);break;case"history":await cmdHistory(e);break;case"trend":await cmdTrend(e);break;case"dashboard":case"dash":await cmdDashboard(e);break;case"diff":await cmdDiff(e);break;case"activate":await cmdActivate(e);break;case"status":await cmdStatus(e);break;case"upgrade":cmdUpgrade();break;case"gate":await cmdGate(e);break;case"cost":await cmdCost(e);break;case"ghosts":await cmdGhosts(e);break;case"ai-score":case"aiscore":await cmdAIScore(e);break;case"clones":case"duplicates":await cmdClones(e);break;case"passport":await cmdPassport(e);break;case"executive":case"exec-report":case"pdf":await cmdExecutive(e);break;case"baseline":await cmdBaseline(e);break;case"telemetry":await cmdTelemetry(e);break;case"compare":await cmdCompare(e);break;case"crucible":await cmdCrucible(e);break;default:console.error(""),console.error(c("red",` Unknown command: ${e.command}`)),console.error("");const o=["scan","compare","deps","drift","inject","health","report","debt","fix","hallucinate","watch","dashboard","diff","gate","cost","ghosts","ai-score","clones","passport","executive","baseline","activate","status","upgrade","telemetry"].filter(o=>o.startsWith(e.command.slice(0,2))||o.includes(e.command));o.length>0&&(console.error(c("gray"," Did you mean: ")+c("cyan",o.join(", "))+c("gray","?")),console.error("")),console.error(c("gray"," Quick start:")),console.error(` ${c("cyan","npx thuban scan .")} Scan your project`),console.error(` ${c("cyan","npx thuban --help")} See all commands`),console.error(""),process.exit(1)}}catch(o){console.error(c("red",` Error: ${o.message}`)),e.verbose&&console.error(o.stack),process.exit(1)}}main();
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thuban",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "The safety layer for AI-coded software. Detect hallucinated APIs, ghost code, tech debt, and architecture drift. One command. Minimal dependencies.",
5
5
  "bin": {
6
6
  "thuban": "./dist/cli.js"
@@ -1 +1 @@
1
- "use strict";const NO_COLOR=void 0!==process.env.NO_COLOR,IS_TTY=!!process.stdout.isTTY,COLORTERM=(process.env.COLORTERM||"").toLowerCase(),TERM=(process.env.TERM||"").toLowerCase(),HAS_TRUECOLOR=!NO_COLOR&&IS_TTY&&("truecolor"===COLORTERM||"24bit"===COLORTERM),HAS_256=!NO_COLOR&&IS_TTY&&(HAS_TRUECOLOR||TERM.includes("256color")||"256color"===COLORTERM),HAS_COLOR=!NO_COLOR&&IS_TTY&&(HAS_TRUECOLOR||HAS_256||""!==TERM||""!==COLORTERM);function _c(E,e,r){return HAS_COLOR?HAS_TRUECOLOR&&E?E:HAS_256&&e?e:r||"":""}const RESET=HAS_COLOR?"":"",BOLD=HAS_COLOR?"":"",DIM=HAS_COLOR?"":"",UNDER=HAS_COLOR?"":"",BLUE=_c("","",""),PURPLE=_c("","",""),RED=_c("","",""),GREEN=_c("","",""),AMBER=_c("","",""),WHITE=HAS_COLOR?"":"",DBLUE=HAS_COLOR?HAS_256?DIM+BLUE:DIM+"":"",GRAY=HAS_COLOR?"":"",YELLOW=AMBER,ORANGE=AMBER,CYAN=BLUE,SEP_WIDTH=58,SEP_CHAR="─";function _sep(){return DBLUE+" "+"─".repeat(58)+RESET}function statusTag(E,e){return GRAY+"[ "+RESET+(e||WHITE)+BOLD+E+RESET+GRAY+" ]"+RESET}const TAG_DONE=statusTag("DONE",GREEN),TAG_ACTIVE=statusTag("ACTIVE",AMBER),TAG_RUNNING=statusTag("RUNNING",PURPLE),TAG_WAITING=statusTag("WAITING",GRAY+DIM),TAG_DEFLECTED=statusTag("DEFLECTED",GREEN),TAG_MUTATED=statusTag("MUTATED",RED),TAG_PASS=statusTag("PASS",GREEN),TAG_FAIL=statusTag("FAIL",RED),TAG_BREACH=statusTag("BREACH",RED),TAG_SURVIVED=statusTag("SURVIVED",GREEN),TAG_SKIP=statusTag("SKIP",GRAY+DIM);function renderInit(E={}){const e=E.version||"0.4.0",r=E.target||".",T=E.mode||"standard",n=null!=E.seed?"0x"+Number(E.seed).toString(16).toUpperCase().padStart(9,"0"):"0x000000000",R=[];return R.push(""),R.push(" "+WHITE+BOLD+"🛡️ THUBAN CRUCIBLE"+RESET+DBLUE+" // "+RESET+WHITE+"v"+e+RESET),R.push(_sep()),R.push(" "+BLUE+BOLD+"[INIT]"+RESET+WHITE+" Target: "+RESET+WHITE+r+RESET),R.push(" "+BLUE+BOLD+"[INIT]"+RESET+WHITE+" Mode: "+RESET+WHITE+T+RESET),R.push(" "+BLUE+BOLD+"[INIT]"+RESET+WHITE+" Seed: "+RESET+WHITE+n+RESET+GRAY+" (Deterministic)"+RESET),R.push(""),R.join("\n")}function renderPhaseHeader(E){return"\n 🔨 "+WHITE+BOLD+(E||"THE CRUCIBLE IS BURNING...")+RESET+"\n"}function renderProgress(E,e,r={}){const T=_resolveTag(e),n=r.sub,R=r.last,t=(n||_padPhase(E),r.detail||"");if(n){const E=void 0!==r.emoji?r.emoji:"🧪";return DBLUE+" │ └─ "+RESET+GRAY+E+" "+RESET+WHITE+(r.subLabel||t)+RESET+(T?" "+_rightAlign(T,t?"":t):"")}const s=t?GRAY+t+RESET:"",A=T?" "+T:"";return(R?DBLUE+" └─ "+RESET:DBLUE+" ├─ "+RESET)+_padPhase(BLUE+BOLD+"["+E+"]"+RESET,E.length)+" "+s+A}function renderVerdict(E={}){const e=!1!==E.survived,r=_fmt(E.mutationsAttempted),T=_fmt(E.deflections),n=null!=E.breaches?E.breaches:0,R=E.reportPath||"",t=E.selfHeal||"",s=E.seedHex||"",A=null!=E.score?E.score:null,a=e?GREEN+BOLD+"CODEBASE SURVIVED THE CRUCIBLE."+RESET:RED+BOLD+"CODEBASE BREACHED — CRITICAL FAILURES DETECTED."+RESET,u=[];u.push(""),u.push(" 🏁 "+WHITE+BOLD+"[VERDICT]"+RESET+" "+a),u.push(_sep()),null!==r&&u.push(" 📦 "+WHITE+"Mutations Attempted: "+RESET+WHITE+BOLD+r+RESET),null!==T&&u.push(" 🛡️ "+WHITE+"Deflections: "+RESET+GREEN+BOLD+T+RESET);const o=n>0?RED+BOLD+String(n)+RESET+(t?GRAY+" ("+t+")"+RESET:""):GREEN+BOLD+"0"+RESET;if(u.push(" 💥 "+WHITE+"Critical Breaches: "+RESET+o),null!==A){const E=A>=90?GREEN:A>=70?AMBER:RED;u.push(" 🌟 "+WHITE+"Crucible Score: "+RESET+E+BOLD+A+"/100"+RESET)}return R&&u.push(" 📄 "+WHITE+"Report Generated: "+RESET+BLUE+UNDER+R+RESET),s&&u.push(" "+GRAY+"Seed: "+s+RESET),u.push(""),u.join("\n")}function fullBanner(E={}){return renderInit({version:E.version,target:E.targetPath||".",mode:E.level?"--level "+E.level:"standard",seed:E.seed})}function compactBanner(E){return"\n 🔥 "+WHITE+BOLD+"CRUCIBLE"+RESET+(E?" "+GRAY+"["+RESET+AMBER+E+RESET+GRAY+"]"+RESET:"")+"\n"}function scoreBanner(E,e){const r=scoreColor(E),T=e||scoreLabel(E),n=buildProgressBar(E,40);return[""," "+WHITE+BOLD+"Crucible Score"+RESET," "+r+n+RESET+" "+r+BOLD+E+"/100"+RESET+" "+GRAY+T+RESET,""].join("\n")}function sectionHeader(E,e){return"\n"+AMBER+" "+(e?e+" ":"")+BOLD+E+RESET+"\n"+GRAY+" "+"─".repeat(Math.min(60,E.length+4))+RESET+"\n"}function statusLine(E,e,r){return` ${E?GREEN+"✓"+RESET:RED+"✗"+RESET} ${DIM+e+RESET}${r?GRAY+" "+r+RESET:""}`}function summaryLine(E,e,r){const T=scoreColor(r),n=e-E;return["",` ${BOLD}Results:${RESET} ${GREEN}${E} passed${RESET} `+(n>0?RED+n+" failed"+RESET:DIM+"0 failed"+RESET)+" "+`${GRAY}(${e} total)${RESET}`,` ${BOLD}Score:${RESET} ${T}${BOLD}${r}/100${RESET}`,""].join("\n")}function plainBanner(){return[""," THUBAN CRUCIBLE"," "+"─".repeat(58)," The Fire Test for Security Scanners",""].join("\n")}const PHASE_WIDTH=12;function _padPhase(E,e){const r=null!=e?e:_stripAnsi(E).length,T=Math.max(0,PHASE_WIDTH-r);return E+" ".repeat(T)}function _stripAnsi(E){return E.replace(/\x1b\[[0-9;]*m/g,"")}function _resolveTag(E){switch((E||"").toUpperCase()){case"DONE":return TAG_DONE;case"ACTIVE":return TAG_ACTIVE;case"RUNNING":return TAG_RUNNING;case"WAITING":return TAG_WAITING;case"DEFLECTED":return TAG_DEFLECTED;case"MUTATED":return TAG_MUTATED;case"PASS":return TAG_PASS;case"FAIL":return TAG_FAIL;case"BREACH":return TAG_BREACH;case"SURVIVED":return TAG_SURVIVED;case"SKIP":return TAG_SKIP;default:return statusTag(E||"?",GRAY)}}function _rightAlign(E,e){return E}function _fmt(E){return null==E?null:Number(E).toLocaleString("en-US")}function buildProgressBar(E,e){const r=Math.round(E/100*e),T=e-r;return"["+"█".repeat(r)+"░".repeat(T)+"]"}function scoreColor(E){return HAS_COLOR?E>=90?GREEN:E>=70||E>=50?AMBER:RED:""}function scoreLabel(E){return E>=90?"Excellent":E>=70?"Good":E>=50?"Fair":E>=30?"Poor":"Critical"}function levelColor(E){if(!HAS_COLOR)return"";switch((E||"").toLowerCase()){case"gentle":return GREEN;case"medium":return AMBER;case"hell":return RED;default:return GRAY}}module.exports={renderInit:renderInit,renderPhaseHeader:renderPhaseHeader,renderProgress:renderProgress,renderVerdict:renderVerdict,statusTag:statusTag,fullBanner:fullBanner,compactBanner:compactBanner,scoreBanner:scoreBanner,sectionHeader:sectionHeader,statusLine:statusLine,summaryLine:summaryLine,plainBanner:plainBanner,scoreColor:scoreColor,scoreLabel:scoreLabel,TAG_DONE:TAG_DONE,TAG_ACTIVE:TAG_ACTIVE,TAG_RUNNING:TAG_RUNNING,TAG_WAITING:TAG_WAITING,TAG_DEFLECTED:TAG_DEFLECTED,TAG_MUTATED:TAG_MUTATED,TAG_PASS:TAG_PASS,TAG_FAIL:TAG_FAIL,TAG_BREACH:TAG_BREACH,TAG_SURVIVED:TAG_SURVIVED,TAG_SKIP:TAG_SKIP,colors:{RESET:RESET,BOLD:BOLD,DIM:DIM,UNDER:UNDER,BLUE:BLUE,PURPLE:PURPLE,RED:RED,GREEN:GREEN,AMBER:AMBER,WHITE:WHITE,GRAY:GRAY,DBLUE:DBLUE,YELLOW:AMBER,ORANGE:AMBER,CYAN:BLUE}};
1
+ "use strict";const _fs=require("fs"),_path=require("path");let _PKG_VERSION;try{_PKG_VERSION=JSON.parse(_fs.readFileSync(_path.join(__dirname,"..","..","package.json"),"utf-8")).version}catch(E){_PKG_VERSION="0.4.3"}const NO_COLOR=void 0!==process.env.NO_COLOR,IS_TTY=!!process.stdout.isTTY,COLORTERM=(process.env.COLORTERM||"").toLowerCase(),TERM=(process.env.TERM||"").toLowerCase(),HAS_TRUECOLOR=!NO_COLOR&&IS_TTY&&("truecolor"===COLORTERM||"24bit"===COLORTERM),HAS_256=!NO_COLOR&&IS_TTY&&(HAS_TRUECOLOR||TERM.includes("256color")||"256color"===COLORTERM),HAS_COLOR=!NO_COLOR&&IS_TTY&&(HAS_TRUECOLOR||HAS_256||""!==TERM||""!==COLORTERM);function _c(E,e,r){return HAS_COLOR?HAS_TRUECOLOR&&E?E:HAS_256&&e?e:r||"":""}const RESET=HAS_COLOR?"":"",BOLD=HAS_COLOR?"":"",DIM=HAS_COLOR?"":"",UNDER=HAS_COLOR?"":"",BLUE=_c("","",""),PURPLE=_c("","",""),RED=_c("","",""),GREEN=_c("","",""),AMBER=_c("","",""),WHITE=HAS_COLOR?"":"",DBLUE=HAS_COLOR?HAS_256?DIM+BLUE:DIM+"":"",GRAY=HAS_COLOR?"":"",YELLOW=AMBER,ORANGE=AMBER,CYAN=BLUE,SEP_WIDTH=58,SEP_CHAR="─";function _sep(){return DBLUE+" "+"─".repeat(58)+RESET}function statusTag(E,e){return GRAY+"[ "+RESET+(e||WHITE)+BOLD+E+RESET+GRAY+" ]"+RESET}const TAG_DONE=statusTag("DONE",GREEN),TAG_ACTIVE=statusTag("ACTIVE",AMBER),TAG_RUNNING=statusTag("RUNNING",PURPLE),TAG_WAITING=statusTag("WAITING",GRAY+DIM),TAG_DEFLECTED=statusTag("DEFLECTED",GREEN),TAG_MUTATED=statusTag("MUTATED",RED),TAG_PASS=statusTag("PASS",GREEN),TAG_FAIL=statusTag("FAIL",RED),TAG_BREACH=statusTag("BREACH",RED),TAG_SURVIVED=statusTag("SURVIVED",GREEN),TAG_SKIP=statusTag("SKIP",GRAY+DIM);function renderInit(E={}){const e=E.version||_PKG_VERSION,r=E.target||".",T=E.mode||"standard",n=null!=E.seed?"0x"+Number(E.seed).toString(16).toUpperCase().padStart(9,"0"):"0x000000000",t=[];return t.push(""),t.push(" "+WHITE+BOLD+"🛡️ THUBAN CRUCIBLE"+RESET+DBLUE+" // "+RESET+WHITE+"v"+e+RESET),t.push(_sep()),t.push(" "+BLUE+BOLD+"[INIT]"+RESET+WHITE+" Target: "+RESET+WHITE+r+RESET),t.push(" "+BLUE+BOLD+"[INIT]"+RESET+WHITE+" Mode: "+RESET+WHITE+T+RESET),t.push(" "+BLUE+BOLD+"[INIT]"+RESET+WHITE+" Seed: "+RESET+WHITE+n+RESET+GRAY+" (Deterministic)"+RESET),t.push(""),t.join("\n")}function renderPhaseHeader(E){return"\n 🔨 "+WHITE+BOLD+(E||"THE CRUCIBLE IS BURNING...")+RESET+"\n"}function renderProgress(E,e,r={}){const T=_resolveTag(e),n=r.sub,t=r.last,R=(n||_padPhase(E),r.detail||"");if(n){const E=void 0!==r.emoji?r.emoji:"🧪";return DBLUE+" │ └─ "+RESET+GRAY+E+" "+RESET+WHITE+(r.subLabel||R)+RESET+(T?" "+_rightAlign(T,R?"":R):"")}const s=R?GRAY+R+RESET:"",A=T?" "+T:"";return(t?DBLUE+" └─ "+RESET:DBLUE+" ├─ "+RESET)+_padPhase(BLUE+BOLD+"["+E+"]"+RESET,E.length)+" "+s+A}function renderVerdict(E={}){const e=!1!==E.survived,r=_fmt(E.mutationsAttempted),T=_fmt(E.deflections),n=null!=E.breaches?E.breaches:0,t=E.reportPath||"",R=E.selfHeal||"",s=E.seedHex||"",A=null!=E.score?E.score:null,a=e?GREEN+BOLD+"CODEBASE SURVIVED THE CRUCIBLE."+RESET:RED+BOLD+"CODEBASE BREACHED — CRITICAL FAILURES DETECTED."+RESET,S=[];S.push(""),S.push(" 🏁 "+WHITE+BOLD+"[VERDICT]"+RESET+" "+a),S.push(_sep()),null!==r&&S.push(" 📦 "+WHITE+"Mutations Attempted: "+RESET+WHITE+BOLD+r+RESET),null!==T&&S.push(" 🛡️ "+WHITE+"Deflections: "+RESET+GREEN+BOLD+T+RESET);const o=n>0?RED+BOLD+String(n)+RESET+(R?GRAY+" ("+R+")"+RESET:""):GREEN+BOLD+"0"+RESET;if(S.push(" 💥 "+WHITE+"Critical Breaches: "+RESET+o),null!==A){const E=A>=90?GREEN:A>=70?AMBER:RED;S.push(" 🌟 "+WHITE+"Crucible Score: "+RESET+E+BOLD+A+"/100"+RESET)}return t&&S.push(" 📄 "+WHITE+"Report Generated: "+RESET+BLUE+UNDER+t+RESET),s&&S.push(" "+GRAY+"Seed: "+s+RESET),S.push(""),S.join("\n")}function fullBanner(E={}){return renderInit({version:E.version,target:E.targetPath||".",mode:E.level?"--level "+E.level:"standard",seed:E.seed})}function compactBanner(E){return"\n 🔥 "+WHITE+BOLD+"CRUCIBLE"+RESET+(E?" "+GRAY+"["+RESET+AMBER+E+RESET+GRAY+"]"+RESET:"")+"\n"}function scoreBanner(E,e){const r=scoreColor(E),T=e||scoreLabel(E),n=buildProgressBar(E,40);return[""," "+WHITE+BOLD+"Crucible Score"+RESET," "+r+n+RESET+" "+r+BOLD+E+"/100"+RESET+" "+GRAY+T+RESET,""].join("\n")}function sectionHeader(E,e){return"\n"+AMBER+" "+(e?e+" ":"")+BOLD+E+RESET+"\n"+GRAY+" "+"─".repeat(Math.min(60,E.length+4))+RESET+"\n"}function statusLine(E,e,r){return` ${E?GREEN+"✓"+RESET:RED+"✗"+RESET} ${DIM+e+RESET}${r?GRAY+" "+r+RESET:""}`}function summaryLine(E,e,r){const T=scoreColor(r),n=e-E;return["",` ${BOLD}Results:${RESET} ${GREEN}${E} passed${RESET} `+(n>0?RED+n+" failed"+RESET:DIM+"0 failed"+RESET)+" "+`${GRAY}(${e} total)${RESET}`,` ${BOLD}Score:${RESET} ${T}${BOLD}${r}/100${RESET}`,""].join("\n")}function plainBanner(){return[""," THUBAN CRUCIBLE"," "+"─".repeat(58)," The Fire Test for Security Scanners",""].join("\n")}const PHASE_WIDTH=12;function _padPhase(E,e){const r=null!=e?e:_stripAnsi(E).length,T=Math.max(0,PHASE_WIDTH-r);return E+" ".repeat(T)}function _stripAnsi(E){return E.replace(/\x1b\[[0-9;]*m/g,"")}function _resolveTag(E){switch((E||"").toUpperCase()){case"DONE":return TAG_DONE;case"ACTIVE":return TAG_ACTIVE;case"RUNNING":return TAG_RUNNING;case"WAITING":return TAG_WAITING;case"DEFLECTED":return TAG_DEFLECTED;case"MUTATED":return TAG_MUTATED;case"PASS":return TAG_PASS;case"FAIL":return TAG_FAIL;case"BREACH":return TAG_BREACH;case"SURVIVED":return TAG_SURVIVED;case"SKIP":return TAG_SKIP;default:return statusTag(E||"?",GRAY)}}function _rightAlign(E,e){return E}function _fmt(E){return null==E?null:Number(E).toLocaleString("en-US")}function buildProgressBar(E,e){const r=Math.round(E/100*e),T=e-r;return"["+"█".repeat(r)+"░".repeat(T)+"]"}function scoreColor(E){return HAS_COLOR?E>=90?GREEN:E>=70||E>=50?AMBER:RED:""}function scoreLabel(E){return E>=90?"Excellent":E>=70?"Good":E>=50?"Fair":E>=30?"Poor":"Critical"}function levelColor(E){if(!HAS_COLOR)return"";switch((E||"").toLowerCase()){case"gentle":return GREEN;case"medium":return AMBER;case"hell":return RED;default:return GRAY}}module.exports={renderInit:renderInit,renderPhaseHeader:renderPhaseHeader,renderProgress:renderProgress,renderVerdict:renderVerdict,statusTag:statusTag,fullBanner:fullBanner,compactBanner:compactBanner,scoreBanner:scoreBanner,sectionHeader:sectionHeader,statusLine:statusLine,summaryLine:summaryLine,plainBanner:plainBanner,scoreColor:scoreColor,scoreLabel:scoreLabel,TAG_DONE:TAG_DONE,TAG_ACTIVE:TAG_ACTIVE,TAG_RUNNING:TAG_RUNNING,TAG_WAITING:TAG_WAITING,TAG_DEFLECTED:TAG_DEFLECTED,TAG_MUTATED:TAG_MUTATED,TAG_PASS:TAG_PASS,TAG_FAIL:TAG_FAIL,TAG_BREACH:TAG_BREACH,TAG_SURVIVED:TAG_SURVIVED,TAG_SKIP:TAG_SKIP,colors:{RESET:RESET,BOLD:BOLD,DIM:DIM,UNDER:UNDER,BLUE:BLUE,PURPLE:PURPLE,RED:RED,GREEN:GREEN,AMBER:AMBER,WHITE:WHITE,GRAY:GRAY,DBLUE:DBLUE,YELLOW:AMBER,ORANGE:AMBER,CYAN:BLUE}};
@@ -1 +1 @@
1
- "use strict";const fs=require("fs"),path=require("path"),RULES_DIR=path.join(__dirname,"rules"),MAX_PATTERN_LENGTH=500,MAX_QUANTIFIERS=20,MAX_ALTERNATIONS=30;function _checkRegexComplexity(e){if("string"!=typeof e||!e)return{safe:!1,reason:"pattern must be a non-empty string"};if(e.length>500)return{safe:!1,reason:"pattern exceeds max length of 500"};let t=0,n=0;const r=[];for(let a=0;a<e.length;a++){const s=e[a];if("\\"!==s)if("("!==s){if(")"===s){const n=r.pop();if(!n)continue;const s=/^\{\d+(,\d*)?\}/.exec(e.slice(a+1)),i=e[a+1];if("+"===i||"*"===i||"?"===i||s){if(t++,n.hasQuantifier||n.hasAlternation)return{safe:!1,reason:"nested quantifier detected (potential catastrophic backtracking)"};r.length>0&&(r[r.length-1].hasQuantifier=!0)}continue}if("+"!==s&&"*"!==s&&"?"!==s){if("{"===s){/^\{\d+(,\d*)?\}/.exec(e.slice(a))&&(t++,r.length>0&&(r[r.length-1].hasQuantifier=!0));continue}"|"!==s||(n++,r.length>0&&(r[r.length-1].hasAlternation=!0))}else t++,r.length>0&&(r[r.length-1].hasQuantifier=!0)}else r.push({hasQuantifier:!1,hasAlternation:!1});else a++}return t>20?{safe:!1,reason:`pattern has ${t} quantifiers, exceeding max of 20`}:n>30?{safe:!1,reason:`pattern has ${n} alternations, exceeding max of 30`}:{safe:!0,reason:null}}let _compiledPatterns=null;function getPatterns(){if(_compiledPatterns)return _compiledPatterns;const e=fs.readdirSync(RULES_DIR).filter(e=>e.endsWith(".json")).sort(),t=[];for(const n of e){const e=path.join(RULES_DIR,n);let r;try{const t=fs.readFileSync(e,"utf-8");r=JSON.parse(t)}catch(e){throw new Error(`[rule-loader] Failed to read/parse rule file "${n}": ${e.message}`)}if(!Array.isArray(r))throw new Error(`[rule-loader] Rule file "${n}" must export a JSON array, got: ${typeof r}`);for(const e of r){if(e.pattern){const t=_checkRegexComplexity(e.pattern);if(!t.safe){console.warn(`[rule-loader] WARNING: Skipping rule "${e.id}" in "${n}" — ${t.reason}`);continue}}let r,a;try{r=new RegExp(e.pattern,e.flags||"")}catch(t){throw new Error(`[rule-loader] Failed to compile rule "${e&&e.id}" in file "${n}": ${t.message}\n pattern: ${e&&e.pattern}\n flags: ${e&&e.flags}`)}if(e.context)try{a=new RegExp(e.context,e.contextFlags||"")}catch(t){throw new Error(`[rule-loader] Failed to compile context for rule "${e&&e.id}" in file "${n}": ${t.message}`)}t.push({regex:r,category:e.category,id:e.id,description:e.description,language:e.language,severity:e.severity,name:e.name,type:e.type,message:e.message,context:a,skipIfSafe:e.skipIfSafe,skipInTests:e.skipInTests,sourceFile:n})}}return _compiledPatterns=t,_compiledPatterns}function getCodeScannerPatterns(){return getPatterns().filter(e=>"code-scanner-core.json"===e.sourceFile).map(e=>{const t={id:e.id,name:e.name,type:e.type,pattern:e.regex,severity:e.severity,message:e.message};return e.context&&(t.context=e.context),e.skipIfSafe&&(t.skipIfSafe=!0),e.skipInTests&&(t.skipInTests=!0),t})}module.exports={getPatterns:getPatterns,getCodeScannerPatterns:getCodeScannerPatterns};
1
+ "use strict";const fs=require("fs"),path=require("path"),RULES_DIR=path.join(__dirname,"rules"),MAX_PATTERN_LENGTH=500,MAX_QUANTIFIERS=20,MAX_ALTERNATIONS=30;function _checkRegexComplexity(e){if("string"!=typeof e||!e)return{safe:!1,reason:"pattern must be a non-empty string"};if(e.length>500)return{safe:!1,reason:"pattern exceeds max length of 500"};let t=0,n=0;const r=[];for(let s=0;s<e.length;s++){const a=e[s];if("\\"!==a)if("("!==a){if(")"===a){const n=r.pop();if(!n)continue;const a=/^\{\d+(,\d*)?\}/.exec(e.slice(s+1)),i=e[s+1];if("+"===i||"*"===i||"?"===i||a){if(t++,n.hasQuantifier)return{safe:!1,reason:"nested quantifier detected (potential catastrophic backtracking)"};r.length>0&&(r[r.length-1].hasQuantifier=!0)}continue}if("+"!==a&&"*"!==a&&"?"!==a){if("{"===a){/^\{\d+(,\d*)?\}/.exec(e.slice(s))&&(t++,r.length>0&&(r[r.length-1].hasQuantifier=!0));continue}"|"!==a||(n++,r.length>0&&(r[r.length-1].hasAlternation=!0))}else t++,r.length>0&&(r[r.length-1].hasQuantifier=!0)}else{if("?"===e[s+1]){const t=e[s+2];":"===t||"="===t||"!"===t?s+=2:"<"===t&&(s+=3)}r.push({hasQuantifier:!1,hasAlternation:!1})}else s++}return t>20?{safe:!1,reason:`pattern has ${t} quantifiers, exceeding max of 20`}:n>30?{safe:!1,reason:`pattern has ${n} alternations, exceeding max of 30`}:{safe:!0,reason:null}}let _compiledPatterns=null;function getPatterns(){if(_compiledPatterns)return _compiledPatterns;const e=fs.readdirSync(RULES_DIR).filter(e=>e.endsWith(".json")).sort(),t=[];for(const n of e){const e=path.join(RULES_DIR,n);let r;try{const t=fs.readFileSync(e,"utf-8");r=JSON.parse(t)}catch(e){throw new Error(`[rule-loader] Failed to read/parse rule file "${n}": ${e.message}`)}if(!Array.isArray(r))throw new Error(`[rule-loader] Rule file "${n}" must export a JSON array, got: ${typeof r}`);for(const e of r){if(e.pattern){const t=_checkRegexComplexity(e.pattern);if(!t.safe){console.warn(`[rule-loader] WARNING: Skipping rule "${e.id}" in "${n}" — ${t.reason}`);continue}}let r,s;try{r=new RegExp(e.pattern,e.flags||"")}catch(t){throw new Error(`[rule-loader] Failed to compile rule "${e&&e.id}" in file "${n}": ${t.message}\n pattern: ${e&&e.pattern}\n flags: ${e&&e.flags}`)}if(e.context)try{s=new RegExp(e.context,e.contextFlags||"")}catch(t){throw new Error(`[rule-loader] Failed to compile context for rule "${e&&e.id}" in file "${n}": ${t.message}`)}t.push({regex:r,category:e.category,id:e.id,description:e.description,language:e.language,severity:e.severity,name:e.name,type:e.type,message:e.message,context:s,skipIfSafe:e.skipIfSafe,skipInTests:e.skipInTests,sourceFile:n})}}return _compiledPatterns=t,_compiledPatterns}function getCodeScannerPatterns(){return getPatterns().filter(e=>"code-scanner-core.json"===e.sourceFile).map(e=>{const t={id:e.id,name:e.name,type:e.type,pattern:e.regex,severity:e.severity,message:e.message};return e.context&&(t.context=e.context),e.skipIfSafe&&(t.skipIfSafe=!0),e.skipInTests&&(t.skipInTests=!0),t})}module.exports={getPatterns:getPatterns,getCodeScannerPatterns:getCodeScannerPatterns};
@@ -110,12 +110,12 @@
110
110
  },
111
111
  {
112
112
  "id": "SEC009",
113
- "name": "Shell Execution (PHP/Ruby)",
113
+ "name": "Shell Execution (PHP/Ruby/Python)",
114
114
  "type": "command_injection",
115
- "pattern": "\\bshell_exec\\s*\\(|\\bpassthru\\s*\\(|`[^`]+`",
115
+ "pattern": "\\bshell_exec\\s*\\(|\\bpassthru\\s*\\(|`[^`]+`|\\bos\\.system\\s*\\(|\\bos\\.popen\\s*\\(|\\bsubprocess\\.(?:call|run|Popen|check_output)\\s*\\([^)]*shell\\s*=\\s*True",
116
116
  "flags": "",
117
117
  "severity": "error",
118
- "message": "Shell execution function - command injection risk if input unsanitized"
118
+ "message": "Shell/OS execution function command injection risk if input unsanitized"
119
119
  },
120
120
  {
121
121
  "id": "SEC010",
@@ -125,5 +125,23 @@
125
125
  "flags": "",
126
126
  "severity": "error",
127
127
  "message": "Possible prototype pollution via __proto__/constructor.prototype"
128
+ },
129
+ {
130
+ "id": "SEC011",
131
+ "name": "PHP SQL Injection (dot concatenation)",
132
+ "type": "sql_injection",
133
+ "pattern": "\"SELECT[^\"]*WHERE[^\"]*\"\\s*\\.\\s*\\$[a-zA-Z_]|\"UPDATE[^\"]*SET[^\"]*\"\\s*\\.\\s*\\$[a-zA-Z_]|\"DELETE[^\"]*WHERE[^\"]*\"\\s*\\.\\s*\\$[a-zA-Z_]|\\$(?:query|sql|qry)\\s*\\.=\\s*\\$(?:_GET|_POST|_REQUEST|_COOKIE)",
134
+ "flags": "i",
135
+ "severity": "critical",
136
+ "message": "PHP SQL injection: user input concatenated into SQL query with dot operator — use PDO prepared statements"
137
+ },
138
+ {
139
+ "id": "SEC012",
140
+ "name": "PHP XSS (echo with user input)",
141
+ "type": "xss",
142
+ "pattern": "echo\\s+(?!htmlspecialchars|htmlentities|strip_tags|esc_html).*\\$(?:_GET|_POST|_REQUEST|_COOKIE)|echo\\s+[\"'][^\"']*[\"']\\s*\\.\\s*\\$(?:_GET|_POST|_REQUEST|_COOKIE)|print\\s+\\$(?:_GET|_POST|_REQUEST|_COOKIE)",
143
+ "flags": "i",
144
+ "severity": "error",
145
+ "message": "PHP XSS: unescaped user input echoed to page — use htmlspecialchars()"
128
146
  }
129
147
  ]
@@ -341,7 +341,7 @@
341
341
  },
342
342
  {
343
343
  "id": "SEC018",
344
- "pattern": "(?:private\\s+)?static\\s+final\\s+String\\s+\\w*(?:SECRET|KEY|TOKEN|PASSWORD|SIGNING)\\w*\\s*=\\s*[\"'][^\"']{12,}[\"']",
344
+ "pattern": "private\\s+static\\s+final\\s+String\\s+\\w*(?:SECRET|KEY|TOKEN|PASSWORD|SIGNING)\\w*\\s*=\\s*[\"'][^\"']{12,}[\"']|static\\s+final\\s+String\\s+\\w*(?:SECRET|KEY|TOKEN|PASSWORD|SIGNING)\\w*\\s*=\\s*[\"'][^\"']{12,}[\"']",
345
345
  "flags": "i",
346
346
  "category": "hardcoded_secret",
347
347
  "description": "Java static final String SECRET/KEY/TOKEN with hardcoded value",
@@ -245,7 +245,7 @@
245
245
  },
246
246
  {
247
247
  "id": "CRYPTO019",
248
- "pattern": "(?:private\\s+)?static\\s+final\\s+String\\s+\\w*(?:ALGORITHM|ALGO|CIPHER)\\w*\\s*=\\s*[\"']DES[\"']",
248
+ "pattern": "private\\s+static\\s+final\\s+String\\s+\\w*(?:ALGORITHM|ALGO|CIPHER)\\w*\\s*=\\s*[\"']DES[\"']|static\\s+final\\s+String\\s+\\w*(?:ALGORITHM|ALGO|CIPHER)\\w*\\s*=\\s*[\"']DES[\"']",
249
249
  "flags": "i",
250
250
  "category": "insecure_crypto",
251
251
  "description": "Kotlin/Java: DES cipher usage",
@@ -593,5 +593,27 @@
593
593
  "python"
594
594
  ],
595
595
  "severity": "critical"
596
+ },
597
+ {
598
+ "id": "SQL046",
599
+ "pattern": "\\$(?:query|sql|qry)\\s*\\.=\\s*\\$(?:_GET|_POST|_REQUEST|_COOKIE|[a-zA-Z_]\\w*)",
600
+ "flags": "i",
601
+ "category": "sql_injection",
602
+ "description": "PHP SQL injection: appending user variable to existing query with .= operator",
603
+ "language": [
604
+ "php"
605
+ ],
606
+ "severity": "critical"
607
+ },
608
+ {
609
+ "id": "SQL047",
610
+ "pattern": "\"SELECT[^\"]*WHERE[^\"]*\"\\s*\\.\\s*\\$[a-zA-Z_]|\"UPDATE[^\"]*SET[^\"]*\"\\s*\\.\\s*\\$[a-zA-Z_]|\"DELETE[^\"]*WHERE[^\"]*\"\\s*\\.\\s*\\$[a-zA-Z_]",
611
+ "flags": "i",
612
+ "category": "sql_injection",
613
+ "description": "PHP SQL injection: SQL WHERE/SET clause with dot-concatenated variable",
614
+ "language": [
615
+ "php"
616
+ ],
617
+ "severity": "critical"
596
618
  }
597
619
  ]
@@ -637,5 +637,27 @@
637
637
  "ts"
638
638
  ],
639
639
  "severity": "high"
640
+ },
641
+ {
642
+ "id": "XSS050",
643
+ "pattern": "print\\s+\\$(?:_GET|_POST|_REQUEST|_COOKIE|_SERVER)\\[",
644
+ "flags": "i",
645
+ "category": "xss",
646
+ "description": "XSS — PHP: print with raw superglobal (unescaped user input)",
647
+ "language": [
648
+ "php"
649
+ ],
650
+ "severity": "high"
651
+ },
652
+ {
653
+ "id": "XSS051",
654
+ "pattern": "echo\\s+[\"'][^\"']*(?:href|src|action|value|data)=[\"']?[^\"'>]*[\"']?\\s*\\.\\s*\\$[a-zA-Z_]",
655
+ "flags": "i",
656
+ "category": "xss",
657
+ "description": "XSS — PHP: echo with user variable injected into HTML attribute (href/src/action)",
658
+ "language": [
659
+ "php"
660
+ ],
661
+ "severity": "high"
640
662
  }
641
663
  ]
@@ -1 +1 @@
1
- const fs=require("fs"),path=require("path"),EventEmitter=require("events");class DriftDetector extends EventEmitter{constructor(e={}){super(),this.config={rootPath:e.rootPath||process.cwd(),widgetPattern:e.widgetPattern||/\/\*\*[\s\S]*?@(purpose|module|layer|imports-from|exports-to|depends-on|critical-for)[\s\S]*?\*\//,ignorePatterns:e.ignorePatterns||["node_modules/**",".git/**","dist/**",".orion/snapshots/**"],...e},this.analysisCache=new Map}async detectAll(){const e=Date.now(),t={filesAnalyzed:0,filesWithWidgets:0,filesWithoutWidgets:0,issues:[],moduleMap:{},duration:0};try{const s=await this._discoverFiles(this.config.rootPath);for(const e of s){const s=await this.analyzeFile(e);t.filesAnalyzed++,s.hasWidget?t.filesWithWidgets++:(t.filesWithoutWidgets++,t.issues.push({file:e,category:"drift",severity:"info",id:"DRIFT001",name:"Missing Widget",message:"File has no widget declaration - add @purpose, @module, @layer etc."})),t.issues.push(...s.issues),s.module&&(t.moduleMap[s.module]||(t.moduleMap[s.module]=[]),t.moduleMap[s.module].push(e))}return t.issues.push(...this._analyzeCrossFile(t.moduleMap)),t.duration=Date.now()-e,t}catch(e){return console.error("[DRIFT-DETECTOR] Detection failed"),t.error="Detection failed",t}}async analyzeFile(e){const t={file:e,hasWidget:!1,widget:null,actualExports:[],actualImports:[],module:null,issues:[]};try{const s=fs.readFileSync(e,"utf8");return t.widget=this._parseWidget(s),t.hasWidget=!!t.widget,t.actualExports=this._extractExports(s),t.actualImports=this._extractImports(s),t.widget&&(t.module=t.widget.module,t.issues.push(...this._compareWidgetToCode(e,t.widget,{exports:t.actualExports,imports:t.actualImports}))),t.drifts=t.issues,this.analysisCache.set(e,t),t}catch(s){const i="ENOENT"===s.code?"File not found":"EACCES"===s.code?"Permission denied":"Analysis error";return t.issues.push({file:e,category:"drift",severity:"error",id:"DRIFT000",message:`Failed to analyze file: ${i}`}),t.drifts=t.issues,t}}async checkFileDrift(e){const t=await this.analyzeFile(e);return{hasDrift:t.issues.length>0,issues:t.issues,widget:t.widget}}async suggestWidget(e){try{const t=fs.readFileSync(e,"utf8"),s=this._extractExports(t),i=this._extractImports(t),o=path.relative(this.config.rootPath,e),r=o.split(path.sep)[0];let n="application";return o.includes("routes")||o.includes("api")?n="presentation":o.includes("core")||o.includes("engine")?n="infrastructure":(o.includes("model")||o.includes("domain"))&&(n="domain"),{purpose:s.length>0?`Provides ${s.join(", ")}`:"Unknown purpose - needs description",module:r,layer:n,importsFrom:i.map(e=>`${e.module} - ${e.specifiers.join(", ")}`),exportsTo:"To be determined",dataFlow:"To be documented",sideEffects:t.includes("fs.")?"File system operations":"None documented",criticalFor:"To be documented"}}catch(e){return{error:e.message}}}async _discoverFiles(e,t=[],s=0){if(s>100)return t;try{const i=fs.readdirSync(e,{withFileTypes:!0});for(const o of i){const i=path.join(e,o.name),r=path.relative(this.config.rootPath,i);this._shouldIgnore(r)||(o.isDirectory()?await this._discoverFiles(i,t,s+1):o.isFile()&&o.name.endsWith(".js")&&t.push(i))}}catch(e){}return t}_shouldIgnore(e){const t=e.replace(/\\/g,"/"),s=["node_modules",".git","dist",".vite"];for(const e of s)if(t.includes("/"+e+"/")||t.startsWith(e+"/")||t===e||t.endsWith("/"+e))return!0;for(const e of this.config.ignorePatterns){const s=e.replace(/\\/g,"/"),i=s.split("/")[0].replace(/\*/g,"");if(i&&(t===i||t.startsWith(i+"/")))return!0;let o=s.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*");if(new RegExp("^"+o).test(t))return!0}return!1}_parseWidget(e){const t=e.match(/\/\*\*[\s\S]*?\*\//);if(!t)return null;const s=t[0],i={},o=["purpose","module","layer","imports-from","exports-to","data-flow","side-effects","depends-on-env","critical-for","known-issues","last-audit","last-modified"];for(const e of o){const t=s.match(new RegExp(`@${e}\\s+(.+?)(?=\\n\\s*\\*\\s*@|\\n\\s*\\*\\/|$)`,"s"));t&&(i[e.replace(/-/g,"_")]=t[1].trim().replace(/\n\s*\*\s*/g," ").trim())}return Object.keys(i).length>0?i:null}_extractExports(e){const t=[],s=e.match(/module\.exports\s*=\s*\{([^}]+)\}/);if(s){const e=s[1].match(/\w+/g)||[];t.push(...e)}const i=e.match(/module\.exports\s*=\s*(class\s+)?(\w+)/);i&&i[2]&&t.push(i[2]);const o=e.match(/exports\.(\w+)\s*=/g)||[];for(const e of o){const s=e.match(/exports\.(\w+)/);s&&t.push(s[1])}return[...new Set(t)]}_extractImports(e){const t=[],s=e.matchAll(/(?:const|let|var)\s+(?:\{([^}]+)\}|(\w+))\s*=\s*require\(['"]([^'"]+)['"]\)/g);for(const e of s){const s=e[1]?e[1].split(",").map(e=>e.trim().split(":")[0].trim()):[e[2]];t.push({module:e[3],specifiers:s.filter(Boolean)})}return t}_compareWidgetToCode(e,t,s){const i=[];if(t.exports_to){const o=t.exports_to.split(",").map(e=>e.trim().split(" ")[0]).filter(e=>!s.exports.some(t=>t.toLowerCase().includes(e.toLowerCase())));o.length>0&&i.push({file:e,category:"drift",severity:"warning",id:"DRIFT002",name:"Missing Declared Export",message:`Widget declares exports not found in code: ${o.join(", ")}`})}if(t.imports_from){const o=t.imports_from.split(",").map(e=>e.trim().split(" ")[0]),r=s.imports.map(e=>e.module),n=o.filter(e=>!r.some(t=>t.includes(e)||e.includes(t.split("/").pop())));n.length>0&&i.push({file:e,category:"drift",severity:"warning",id:"DRIFT003",name:"Missing Declared Import",message:`Widget declares imports not found in code: ${n.join(", ")}`})}return!/fs\.(write|append|unlink|mkdir|rmdir)/i.test(fs.readFileSync(e,"utf8"))||t.side_effects&&"None"!==t.side_effects||i.push({file:e,category:"drift",severity:"warning",id:"DRIFT004",name:"Undeclared Side Effects",message:"File has file system operations but widget declares no side effects"}),i}_analyzeCrossFile(e){const t=[];for(const[s,i]of Object.entries(e))1===i.length&&t.push({file:i[0],category:"drift",severity:"info",id:"DRIFT005",name:"Orphaned Module",message:`Module "${s}" only has one file - verify module organization`});return t}}module.exports=DriftDetector;
1
+ const fs=require("fs"),path=require("path"),EventEmitter=require("events");class DriftDetector extends EventEmitter{constructor(e={}){super(),this.config={rootPath:e.rootPath||process.cwd(),widgetPattern:e.widgetPattern||/\/\*\*[\s\S]*?@(purpose|module|layer|imports-from|exports-to|depends-on|critical-for)[\s\S]*?\*\//,ignorePatterns:e.ignorePatterns||["node_modules/**",".git/**","dist/**",".orion/snapshots/**"],...e},this.analysisCache=new Map}async detectAll(){const e=Date.now(),t={filesAnalyzed:0,filesWithWidgets:0,filesWithoutWidgets:0,issues:[],moduleMap:{},duration:0};try{const s=await this._discoverFiles(this.config.rootPath);for(const e of s){const s=await this.analyzeFile(e);t.filesAnalyzed++,s.hasWidget?t.filesWithWidgets++:(t.filesWithoutWidgets++,t.issues.push({file:e,category:"drift",severity:"info",id:"DRIFT001",name:"Missing Widget",message:"File has no widget declaration - add @purpose, @module, @layer etc."})),t.issues.push(...s.issues),s.module&&(t.moduleMap[s.module]||(t.moduleMap[s.module]=[]),t.moduleMap[s.module].push(e))}return t.issues.push(...this._analyzeCrossFile(t.moduleMap)),t.duration=Date.now()-e,t}catch(e){return console.error("[DRIFT-DETECTOR] Detection failed"),t.error="Detection failed",t}}async analyzeFile(e){const t={file:e,hasWidget:!1,widget:null,actualExports:[],actualImports:[],module:null,issues:[]};try{const s=fs.readFileSync(e,"utf8");return t.widget=this._parseWidget(s),t.hasWidget=!!t.widget,t.actualExports=this._extractExports(s),t.actualImports=this._extractImports(s),t.widget&&(t.module=t.widget.module,t.issues.push(...this._compareWidgetToCode(e,t.widget,{exports:t.actualExports,imports:t.actualImports}))),t.drifts=t.issues,this.analysisCache.set(e,t),t}catch(s){const i="ENOENT"===s.code?"File not found":"EACCES"===s.code?"Permission denied":"Analysis error";return t.issues.push({file:e,category:"drift",severity:"error",id:"DRIFT000",message:`Failed to analyze file: ${i}`}),t.drifts=t.issues,t}}async checkFileDrift(e){const t=await this.analyzeFile(e);return{hasDrift:t.issues.length>0,issues:t.issues,widget:t.widget}}async suggestWidget(e){try{const t=fs.readFileSync(e,"utf8"),s=this._extractExports(t),i=this._extractImports(t),o=path.relative(this.config.rootPath,e),r=o.split(path.sep)[0];let n="application";return o.includes("routes")||o.includes("api")?n="presentation":o.includes("core")||o.includes("engine")?n="infrastructure":(o.includes("model")||o.includes("domain"))&&(n="domain"),{purpose:s.length>0?`Provides ${s.join(", ")}`:"Unknown purpose - needs description",module:r,layer:n,importsFrom:i.map(e=>`${e.module} - ${e.specifiers.join(", ")}`),exportsTo:"To be determined",dataFlow:"To be documented",sideEffects:t.includes("fs.")?"File system operations":"None documented",criticalFor:"To be documented"}}catch(e){return{error:e.message}}}async _discoverFiles(e,t=[],s=0){if(s>100)return t;try{const i=fs.readdirSync(e,{withFileTypes:!0});for(const o of i){const i=path.join(e,o.name),r=path.relative(this.config.rootPath,i);this._shouldIgnore(r)||(o.isDirectory()?await this._discoverFiles(i,t,s+1):o.isFile()&&o.name.endsWith(".js")&&t.push(i))}}catch(e){}return t}_shouldIgnore(e){const t=e.replace(/\\/g,"/"),s=["node_modules",".git","dist",".vite"];for(const e of s)if(t.includes("/"+e+"/")||t.startsWith(e+"/")||t===e||t.endsWith("/"+e))return!0;for(const e of this.config.ignorePatterns){const s=e.replace(/\\/g,"/"),i=s.split("/")[0].replace(/\*/g,"");if(i&&(t===i||t.startsWith(i+"/")))return!0;let o=s.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*");if(new RegExp("^"+o).test(t))return!0}return!1}_parseWidget(e){const t=e.match(/\/\*\*[\s\S]*?\*\//);if(t){const e=t[0],s=this._extractWidgetFields(e,t=>{const s=e.match(new RegExp(`@${t}\\s+(.+?)(?=\\n\\s*\\*\\s*@|\\n\\s*\\*\\/|$)`,"s"));return s?s[1].trim().replace(/\n\s*\*\s*/g," ").trim():null});if(s)return s}const s=e.match(/"{3}[\s\S]*?@(?:purpose|module|layer)[\s\S]*?"{3}|'{3}[\s\S]*?@(?:purpose|module|layer)[\s\S]*?'{3}/);if(s){const e=s[0],t=this._extractWidgetFields(e,t=>{const s=e.match(new RegExp(`@${t}\\s+(.+?)(?=\\n\\s*@|$)`,"s"));return s?s[1].trim():null});if(t)return t}const i=e.match(/(?:^|\n)((?:\s*#[^\n]*\n)+)/);if(i){const e=i[0];if(/@(?:purpose|module|layer)/.test(e)){const t=this._extractWidgetFields(e,t=>{const s=e.match(new RegExp(`@${t}\\s+(.+?)(?=\\n\\s*#\\s*@|\\n[^#]|$)`));return s?s[1].trim():null});if(t)return t}}const o=e.match(/(?:^|\n)((?:\s*\/\/[^\n]*\n)+)/);if(o){const e=o[0];if(/@(?:purpose|module|layer)/.test(e)){const t=this._extractWidgetFields(e,t=>{const s=e.match(new RegExp(`@${t}\\s+(.+?)(?=\\n\\s*//\\s*@|\\n[^/]|$)`));return s?s[1].trim():null});if(t)return t}}return null}_extractWidgetFields(e,t){const s=["purpose","module","layer","imports-from","exports-to","data-flow","side-effects","depends-on-env","critical-for","known-issues","last-audit","last-modified"],i={};for(const e of s){const s=t(e);s&&(i[e.replace(/-/g,"_")]=s)}return Object.keys(i).length>0?i:null}_extractExports(e){const t=[],s=e.match(/module\.exports\s*=\s*\{([^}]+)\}/);if(s){const e=s[1].match(/\w+/g)||[];t.push(...e)}const i=e.match(/module\.exports\s*=\s*(class\s+)?(\w+)/);i&&i[2]&&t.push(i[2]);const o=e.match(/exports\.(\w+)\s*=/g)||[];for(const e of o){const s=e.match(/exports\.(\w+)/);s&&t.push(s[1])}const r=e.match(/^(?:def|class)\s+([a-zA-Z_]\w*)/gm)||[];for(const e of r){const s=e.match(/^(?:def|class)\s+([a-zA-Z_]\w*)/);s&&!s[1].startsWith("_")&&t.push(s[1])}const n=e.match(/^func\s+([A-Z][a-zA-Z0-9]*)/gm)||[];for(const e of n){const s=e.match(/^func\s+([A-Z][a-zA-Z0-9]*)/);s&&t.push(s[1])}const a=e.match(/(?:public\s+function|class)\s+([a-zA-Z_]\w*)/g)||[];for(const e of a){const s=e.match(/(?:public\s+function|class)\s+([a-zA-Z_]\w*)/);s&&t.push(s[1])}return[...new Set(t)]}_extractImports(e){const t=[],s=e.matchAll(/(?:const|let|var)\s+(?:\{([^}]+)\}|(\w+))\s*=\s*require\(['"]([^'"]+)['"]\)/g);for(const e of s){const s=e[1]?e[1].split(",").map(e=>e.trim().split(":")[0].trim()):[e[2]];t.push({module:e[3],specifiers:s.filter(Boolean)})}const i=e.matchAll(/^(?:import\s+(\S+)|from\s+(\S+)\s+import)/gm);for(const e of i)t.push({module:e[1]||e[2],specifiers:[]});const o=e.matchAll(/"([^"]+)"/g);for(const e of o)!e[1].includes("/")&&e[1].includes(" ")||t.push({module:e[1],specifiers:[]});return t}_compareWidgetToCode(e,t,s){const i=[];if(t.exports_to){const o=t.exports_to.split(",").map(e=>e.trim().split(" ")[0]).filter(e=>!s.exports.some(t=>t.toLowerCase().includes(e.toLowerCase())));o.length>0&&i.push({file:e,category:"drift",severity:"warning",id:"DRIFT002",name:"Missing Declared Export",message:`Widget declares exports not found in code: ${o.join(", ")}`})}if(t.imports_from){const o=t.imports_from.split(",").map(e=>e.trim().split(" ")[0]),r=s.imports.map(e=>e.module),n=o.filter(e=>!r.some(t=>t.includes(e)||e.includes(t.split("/").pop())));n.length>0&&i.push({file:e,category:"drift",severity:"warning",id:"DRIFT003",name:"Missing Declared Import",message:`Widget declares imports not found in code: ${n.join(", ")}`})}return!/fs\.(write|append|unlink|mkdir|rmdir)/i.test(fs.readFileSync(e,"utf8"))||t.side_effects&&"None"!==t.side_effects||i.push({file:e,category:"drift",severity:"warning",id:"DRIFT004",name:"Undeclared Side Effects",message:"File has file system operations but widget declares no side effects"}),i}_analyzeCrossFile(e){const t=[];for(const[s,i]of Object.entries(e))1===i.length&&t.push({file:i[0],category:"drift",severity:"info",id:"DRIFT005",name:"Orphaned Module",message:`Module "${s}" only has one file - verify module organization`});return t}}module.exports=DriftDetector;
@@ -1 +1 @@
1
- "use strict";const fs=require("fs"),path=require("path"),nodeModule=require("module"),parser=require("@babel/parser"),{_internal:_internal}=require("./ast-analyzer"),{walk:walk}=_internal,BABEL_PLUGINS=["estree","typescript","jsx","classProperties","decorators-legacy"];function parseProgram(e){return parser.parse(e,{sourceType:"unambiguous",plugins:BABEL_PLUGINS,allowReturnOutsideFunction:!0,allowImportExportEverywhere:!0,allowAwaitOutsideFunction:!0}).program}function extractNamedImports(e){const t=[];let r;try{r=parseProgram(e)}catch(e){return t}for(const e of r.body)if("ImportDeclaration"===e.type&&e.source){const r=e.source.value;for(const n of e.specifiers||[])if("ImportSpecifier"===n.type){const i=n.imported.name||n.imported.value;t.push({imported:i,local:n.local.name,source:r,line:e.loc?e.loc.start.line:0,kind:"import"})}}return walk(r,e=>{if("VariableDeclarator"===e.type&&e.init&&"CallExpression"===e.init.type&&"Identifier"===e.init.callee.type&&"require"===e.init.callee.name&&e.init.arguments[0]&&("Literal"===e.init.arguments[0].type||"StringLiteral"===e.init.arguments[0].type)&&"ObjectPattern"===e.id.type){const r=e.init.arguments[0].value,n=e.loc?e.loc.start.line:0;for(const i of e.id.properties)if(("RestElement"!==i.type&&("ObjectProperty"===i.type!=0||"Property"===i.type)||"RestElement"!==i.type)&&i.key&&i.value){const e=void 0!==i.key.name?i.key.name:i.key.value,o="Identifier"===i.value.type?i.value.name:e;t.push({imported:e,local:o,source:r,line:n,kind:"require"})}}}),t}const builtinModuleCache=new Map;function getBuiltinExports(e){if(builtinModuleCache.has(e))return builtinModuleCache.get(e);let t;try{const r=require(e);t={names:new Set(Object.keys(r)),uncertain:!1}}catch(e){t={names:null,uncertain:!0}}return builtinModuleCache.set(e,t),t}function isBuiltinSpecifier(e){const t=e.startsWith("node:")?e.slice(5):e;return nodeModule.builtinModules.includes(t)||nodeModule.builtinModules.includes(e)}function collectObjectKeys(e,t,r){for(const n of e.properties||[]){if("SpreadElement"===n.type||"SpreadProperty"===n.type||"ExperimentalSpreadProperty"===n.type){r.value=!0;continue}if(n.computed){r.value=!0;continue}const e=n.key;e&&("Identifier"===e.type?t.add(e.name):"Literal"!==e.type&&"StringLiteral"!==e.type||t.add(String(e.value)))}}const moduleExportsCache=new Map;function getModuleExports(e,t){t=t||new Set;const r=e;if(moduleExportsCache.has(r))return moduleExportsCache.get(r);if(t.has(e))return{names:new Set,uncertain:!0};let n;t.add(e);const i=path.extname(e).toLowerCase();if(".json"===i){try{const t=JSON.parse(fs.readFileSync(e,"utf-8"));n={names:t&&"object"==typeof t?new Set(Object.keys(t)):new Set,uncertain:!1}}catch(e){n={names:null,uncertain:!0}}return moduleExportsCache.set(r,n),n}if(".node"===i||![".js",".jsx",".ts",".tsx",".mjs",".cjs"].includes(i)&&""!==i)return n={names:null,uncertain:!0},moduleExportsCache.set(r,n),n;let o,a;try{o=fs.readFileSync(e,"utf-8")}catch(e){return n={names:null,uncertain:!0},moduleExportsCache.set(r,n),n}try{a=parseProgram(o)}catch(e){return n={names:null,uncertain:!0},moduleExportsCache.set(r,n),n}const s=new Set,l={value:!1},c=path.dirname(e);function u(e,r){const n=function(e){try{return require.resolve(e,{paths:[c]})}catch(e){return null}}(e);if(!n)return void(l.value=!0);const i=getModuleExports(n,t);if(i.uncertain||null===i.names)l.value=!0;else if(r)for(const e of r)i.names.has(e)&&s.add(e);else for(const e of i.names)s.add(e)}let p=null;for(const e of a.body){if("ExportNamedDeclaration"===e.type)if(e.declaration){const t=e.declaration;if("VariableDeclaration"===t.type)for(const e of t.declarations)e.id&&"Identifier"===e.id.type?s.add(e.id.name):e.id&&"ObjectPattern"===e.id.type&&(l.value=!0);else"FunctionDeclaration"!==t.type&&"ClassDeclaration"!==t.type||!t.id||s.add(t.id.name)}else if(e.source){const t=(e.specifiers||[]).map(e=>e.local.name);u(e.source.value,t);for(const t of e.specifiers||[])s.add(t.exported.name||t.exported.value)}else for(const t of e.specifiers||[])s.add(t.exported.name||t.exported.value);if("ExportAllDeclaration"===e.type&&e.source&&u(e.source.value,null),"ExportDefaultDeclaration"===e.type&&s.add("default"),"ExpressionStatement"===e.type){const t=e.expression;if(t&&"AssignmentExpression"===t.type&&"="===t.operator){const e=t.left;"MemberExpression"===e.type&&"Identifier"===e.object.type&&"module"===e.object.name&&"Identifier"===e.property.type&&"exports"===e.property.name?"ObjectExpression"===t.right.type?collectObjectKeys(t.right,s,l):"CallExpression"!==t.right.type||"Identifier"!==t.right.callee.type||"require"!==t.right.callee.name||!t.right.arguments[0]||"Literal"!==t.right.arguments[0].type&&"StringLiteral"!==t.right.arguments[0].type?("Identifier"===t.right.type||"FunctionExpression"===t.right.type||t.right.type,l.value=!0):p=t.right.arguments[0].value:"MemberExpression"===e.type&&!e.computed&&"Identifier"===e.property.type&&("Identifier"===e.object.type&&"exports"===e.object.name||"MemberExpression"===e.object.type&&"Identifier"===e.object.object.type&&"module"===e.object.object.name&&"Identifier"===e.object.property.type&&"exports"===e.object.property.name)&&s.add(e.property.name)}t&&"CallExpression"===t.type&&"MemberExpression"===t.callee.type&&"Identifier"===t.callee.object.type&&"Object"===t.callee.object.name&&"Identifier"===t.callee.property.type&&"defineProperty"===t.callee.property.name&&t.arguments[0]&&"Identifier"===t.arguments[0].type&&"exports"===t.arguments[0].name&&t.arguments[1]&&("Literal"===t.arguments[1].type||"StringLiteral"===t.arguments[1].type)&&s.add(t.arguments[1].value)}}return p&&u(p,null),n={names:s,uncertain:l.value},moduleExportsCache.set(r,n),n}function verifyFile(e,t){const r=[],n=extractNamedImports(t);if(0===n.length)return r;const i=path.dirname(e);for(const t of n){if(t.source.startsWith(".")||t.source.startsWith("/"))continue;if("default"===t.imported)continue;let n;if(isBuiltinSpecifier(t.source))n=getBuiltinExports(t.source.startsWith("node:")?t.source.slice(5):t.source);else{let e;try{e=require.resolve(t.source,{paths:[i]})}catch(e){continue}n=getModuleExports(e,new Set)}n&&null!==n.names&&!n.uncertain&&(n.names.has(t.imported)||r.push({file:e,line:t.line,source:t.source,importedName:t.imported,message:`'${t.source}' does not export '${t.imported}' — this import will be undefined at runtime`}))}return r}module.exports={verifyFile:verifyFile,extractNamedImports:extractNamedImports,getModuleExports:getModuleExports,getBuiltinExports:getBuiltinExports,isBuiltinSpecifier:isBuiltinSpecifier,_internal:{parseProgram:parseProgram,collectObjectKeys:collectObjectKeys}};
1
+ "use strict";const fs=require("fs"),path=require("path"),nodeModule=require("module"),parser=require("@babel/parser"),{_internal:_internal}=require("./ast-analyzer"),{walk:walk}=_internal,BABEL_PLUGINS=["estree","typescript","jsx","classProperties","decorators-legacy"];function parseProgram(e){return parser.parse(e,{sourceType:"unambiguous",plugins:BABEL_PLUGINS,allowReturnOutsideFunction:!0,allowImportExportEverywhere:!0,allowAwaitOutsideFunction:!0}).program}function extractNamedImports(e){const t=[];let r;try{r=parseProgram(e)}catch(e){return t}for(const e of r.body)if("ImportDeclaration"===e.type&&e.source){const r=e.source.value;for(const n of e.specifiers||[])if("ImportSpecifier"===n.type){const i=n.imported.name||n.imported.value;t.push({imported:i,local:n.local.name,source:r,line:e.loc?e.loc.start.line:0,kind:"import"})}}return walk(r,e=>{if("VariableDeclarator"===e.type&&e.init&&"CallExpression"===e.init.type&&"Identifier"===e.init.callee.type&&"require"===e.init.callee.name&&e.init.arguments[0]&&("Literal"===e.init.arguments[0].type||"StringLiteral"===e.init.arguments[0].type)&&"ObjectPattern"===e.id.type){const r=e.init.arguments[0].value,n=e.loc?e.loc.start.line:0;for(const i of e.id.properties)if(("RestElement"!==i.type&&("ObjectProperty"===i.type!=0||"Property"===i.type)||"RestElement"!==i.type)&&i.key&&i.value){const e=void 0!==i.key.name?i.key.name:i.key.value,o="Identifier"===i.value.type?i.value.name:e;t.push({imported:e,local:o,source:r,line:n,kind:"require"})}}}),t}const builtinModuleCache=new Map;function getBuiltinExports(e){if(builtinModuleCache.has(e))return builtinModuleCache.get(e);let t;try{const r=require(e);t={names:new Set(Object.keys(r)),uncertain:!1}}catch(e){t={names:null,uncertain:!0}}return builtinModuleCache.set(e,t),t}function isBuiltinSpecifier(e){const t=e.startsWith("node:")?e.slice(5):e;return nodeModule.builtinModules.includes(t)||nodeModule.builtinModules.includes(e)}function collectObjectKeys(e,t,r){for(const n of e.properties||[]){if("SpreadElement"===n.type||"SpreadProperty"===n.type||"ExperimentalSpreadProperty"===n.type){r.value=!0;continue}if(n.computed){r.value=!0;continue}const e=n.key;e&&("Identifier"===e.type?t.add(e.name):"Literal"!==e.type&&"StringLiteral"!==e.type||t.add(String(e.value)))}}const moduleExportsCache=new Map;function getModuleExports(e,t){t=t||new Set;const r=e;if(moduleExportsCache.has(r))return moduleExportsCache.get(r);if(t.has(e))return{names:new Set,uncertain:!0};let n;t.add(e);const i=path.extname(e).toLowerCase();if(".json"===i){try{const t=JSON.parse(fs.readFileSync(e,"utf-8"));n={names:t&&"object"==typeof t?new Set(Object.keys(t)):new Set,uncertain:!1}}catch(e){n={names:null,uncertain:!0}}return moduleExportsCache.set(r,n),n}if(".node"===i||![".js",".jsx",".ts",".tsx",".mjs",".cjs"].includes(i)&&""!==i)return n={names:null,uncertain:!0},moduleExportsCache.set(r,n),n;let o,a;try{o=fs.readFileSync(e,"utf-8")}catch(e){return n={names:null,uncertain:!0},moduleExportsCache.set(r,n),n}try{a=parseProgram(o)}catch(e){return n={names:null,uncertain:!0},moduleExportsCache.set(r,n),n}const s=new Set,l={value:!1},c=path.dirname(e);function u(e,r){const n=function(e){try{return require.resolve(e,{paths:[c]})}catch(e){return null}}(e);if(!n)return void(l.value=!0);let i;try{i=getModuleExports(n,t)}catch(e){return void(l.value=!0)}if(i&&!i.uncertain&&null!==i.names&&"function"==typeof i.names.has)if(r)for(const e of r)i.names.has(e)&&s.add(e);else for(const e of i.names)s.add(e);else l.value=!0}let p=null;for(const e of a.body){if("ExportNamedDeclaration"===e.type)if(e.declaration){const t=e.declaration;if("VariableDeclaration"===t.type)for(const e of t.declarations)e.id&&"Identifier"===e.id.type?s.add(e.id.name):e.id&&"ObjectPattern"===e.id.type&&(l.value=!0);else"FunctionDeclaration"!==t.type&&"ClassDeclaration"!==t.type||!t.id||s.add(t.id.name)}else if(e.source){const t=(e.specifiers||[]).map(e=>e.local.name);u(e.source.value,t);for(const t of e.specifiers||[])s.add(t.exported.name||t.exported.value)}else for(const t of e.specifiers||[])s.add(t.exported.name||t.exported.value);if("ExportAllDeclaration"===e.type&&e.source&&u(e.source.value,null),"ExportDefaultDeclaration"===e.type&&s.add("default"),"ExpressionStatement"===e.type){const t=e.expression;if(t&&"AssignmentExpression"===t.type&&"="===t.operator){const e=t.left;"MemberExpression"===e.type&&"Identifier"===e.object.type&&"module"===e.object.name&&"Identifier"===e.property.type&&"exports"===e.property.name?"ObjectExpression"===t.right.type?collectObjectKeys(t.right,s,l):"CallExpression"!==t.right.type||"Identifier"!==t.right.callee.type||"require"!==t.right.callee.name||!t.right.arguments[0]||"Literal"!==t.right.arguments[0].type&&"StringLiteral"!==t.right.arguments[0].type?("Identifier"===t.right.type||"FunctionExpression"===t.right.type||t.right.type,l.value=!0):p=t.right.arguments[0].value:"MemberExpression"===e.type&&!e.computed&&"Identifier"===e.property.type&&("Identifier"===e.object.type&&"exports"===e.object.name||"MemberExpression"===e.object.type&&"Identifier"===e.object.object.type&&"module"===e.object.object.name&&"Identifier"===e.object.property.type&&"exports"===e.object.property.name)&&s.add(e.property.name)}t&&"CallExpression"===t.type&&"MemberExpression"===t.callee.type&&"Identifier"===t.callee.object.type&&"Object"===t.callee.object.name&&"Identifier"===t.callee.property.type&&"defineProperty"===t.callee.property.name&&t.arguments[0]&&"Identifier"===t.arguments[0].type&&"exports"===t.arguments[0].name&&t.arguments[1]&&("Literal"===t.arguments[1].type||"StringLiteral"===t.arguments[1].type)&&s.add(t.arguments[1].value)}}return p&&u(p,null),n={names:s,uncertain:l.value},moduleExportsCache.set(r,n),n}function verifyFile(e,t){const r=[],n=extractNamedImports(t);if(0===n.length)return r;const i=path.dirname(e);for(const t of n){if(t.source.startsWith(".")||t.source.startsWith("/"))continue;if("default"===t.imported)continue;let n;if(isBuiltinSpecifier(t.source))n=getBuiltinExports(t.source.startsWith("node:")?t.source.slice(5):t.source);else{let e;try{e=require.resolve(t.source,{paths:[i]})}catch(e){continue}try{n=getModuleExports(e,new Set)}catch(e){continue}}n&&null!==n.names&&!n.uncertain&&"function"==typeof n.names.has&&(n.names.has(t.imported)||r.push({file:e,line:t.line,source:t.source,importedName:t.imported,message:`'${t.source}' does not export '${t.imported}' — this import will be undefined at runtime`}))}return r}module.exports={verifyFile:verifyFile,extractNamedImports:extractNamedImports,getModuleExports:getModuleExports,getBuiltinExports:getBuiltinExports,isBuiltinSpecifier:isBuiltinSpecifier,_internal:{parseProgram:parseProgram,collectObjectKeys:collectObjectKeys}};
@@ -1 +1 @@
1
- const fs=require("fs"),path=require("path"),os=require("os"),crypto=require("crypto"),THUBAN_DIR=path.join(os.homedir(),".thuban"),USAGE_FILE=path.join(THUBAN_DIR,"usage.json"),LICENSE_FILE=path.join(THUBAN_DIR,"license.json"),ED25519_PUBLIC_KEY_DER_B64="MCowBQYDK2VwAyEA9/1g/P5f3OwZ/u1LNS1MY6ytTg7GLn3VIWOMxm4qwMQ=",TIERS={free:{name:"Free",maxFiles:100,scansPerMonth:5,showFullDetails:!1,showDashboard:!1,showAutoFix:!1,showCIAction:!1,teaserIssues:3,maxHallucinationDetail:3},pro:{name:"Pro",maxFiles:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!1,teaserIssues:1/0,maxHallucinationDetail:1/0},team:{name:"Team",maxFiles:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!0,teaserIssues:1/0,maxHallucinationDetail:1/0},enterprise:{name:"Enterprise",maxFiles:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!0,teaserIssues:1/0,maxHallucinationDetail:1/0}};class LicenseManager{constructor(){this._ensureDir(),this.machineId=this._getMachineId(),this.usage=this._loadUsage(),this.license=this._loadLicense()}getTier(){return"true"!==process.env.THUBAN_DEV_MODE||__filename.includes("node_modules")?this.license&&this.license.key&&this._validateKey(this.license.key)&&this.license.tier||"free":"enterprise"}getTierConfig(){return"true"===process.env.THUBAN_DEV_MODE?TIERS.pro:TIERS[this.getTier()]||TIERS.free}canScan(){if("true"===process.env.THUBAN_DEV_MODE&&!__filename.includes("node_modules"))return{allowed:!0,remaining:1/0,devMode:!0};const e=this.getTierConfig();if(e.scansPerMonth===1/0)return{allowed:!0,remaining:1/0};const t=this._currentMonth(),s=this.usage.scans?.[t]||0,n=Math.max(0,e.scansPerMonth-s);return{allowed:n>0,remaining:n,limit:e.scansPerMonth,used:s,resetsOn:this._nextMonthDate()}}recordScan(){if("true"===process.env.THUBAN_DEV_MODE&&!__filename.includes("node_modules"))return;const e=this._currentMonth();this.usage.scans||(this.usage.scans={}),this.usage.scans[e]=(this.usage.scans[e]||0)+1,this.usage.lastScan=(new Date).toISOString(),this.usage.totalScans=(this.usage.totalScans||0)+1,this._saveUsage()}activate(e){const t=this._validateKey(e);return t?(this.license={key:e,tier:t.tier,activatedAt:(new Date).toISOString(),machineId:this.machineId},this._saveLicense(),{success:!0,tier:t.tier,tierName:TIERS[t.tier]?.name||t.tier}):{success:!1,error:"Invalid license key format"}}deactivate(){return this.license={},this._saveLicense(),{success:!0}}getStatus(){const e=this.getTier(),t=this.getTierConfig(),s=this.canScan();let n=!1;return n="true"===process.env.THUBAN_DEV_MODE&&!__filename.includes("node_modules"),{tier:e,tierName:t.name,machineId:this.machineId.substring(0,8)+"...",licensed:"free"!==e,devMode:n,scansUsed:n?0:s.used||0,scansRemaining:s.remaining,scansLimit:n?1/0:s.limit||t.scansPerMonth,totalScans:this.usage.totalScans||0,features:{fullDetails:t.showFullDetails,dashboard:t.showDashboard,autoFix:t.showAutoFix,ciAction:t.showCIAction,scheduledScans:"free"!==e}}}canUseProFeature(e="This feature"){const t=this.getTier();return"free"===t?{allowed:!1,message:`${e} is available on Thuban Pro and above.`}:{allowed:!0,tier:t}}_ensureDir(){try{fs.existsSync(THUBAN_DIR)||fs.mkdirSync(THUBAN_DIR,{recursive:!0})}catch(e){}}_getMachineId(){const e=[os.hostname(),os.userInfo().username,os.platform(),os.arch()].join("|");try{const t=os.networkInterfaces(),s=Object.values(t).flat().filter(e=>!e.internal&&e.mac&&"00:00:00:00:00:00"!==e.mac).map(e=>e.mac).sort();if(s.length>0)return crypto.createHash("sha256").update(e+"|"+s[0]).digest("hex")}catch(e){}return crypto.createHash("sha256").update(e).digest("hex")}_loadUsage(){try{return JSON.parse(fs.readFileSync(USAGE_FILE,"utf-8"))}catch(e){return{scans:{},totalScans:0}}}_atomicWriteSync(e,t){const s=e+".tmp."+process.pid+"."+Date.now();try{fs.writeFileSync(s,t,"utf-8"),fs.renameSync(s,e)}catch(e){try{fs.unlinkSync(s)}catch{}}}_saveUsage(){try{this._atomicWriteSync(USAGE_FILE,JSON.stringify(this.usage,null,2))}catch(e){}}_loadLicense(){try{return JSON.parse(fs.readFileSync(LICENSE_FILE,"utf-8"))}catch(e){return{}}}_saveLicense(){try{this._atomicWriteSync(LICENSE_FILE,JSON.stringify(this.license,null,2))}catch(e){}}_validateKey(e){if(!e||"string"!=typeof e)return null;const t=e.match(/^THUBAN-(FREE|PRO|TEAM|ENT)-([A-Za-z0-9]{16})-([A-Fa-f0-9]{8,})$/);if(!t)return null;const s=t[1],n=t[2],a=t[3],r={FREE:"free",PRO:"pro",TEAM:"team",ENT:"enterprise"};try{const e=crypto.createPublicKey({key:Buffer.from(ED25519_PUBLIC_KEY_DER_B64,"base64"),format:"der",type:"spki"}),t=Buffer.from(`THUBAN-${s}-${n}`),i=Buffer.from(a,"hex");if(crypto.verify(null,t,e,i))return{tier:r[s]||"free",valid:!0,method:"ed25519"}}catch(e){}return null}_currentMonth(){const e=new Date;return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}`}_nextMonthDate(){const e=new Date;return new Date(e.getFullYear(),e.getMonth()+1,1).toISOString().split("T")[0]}static generateKey(e="PRO"){const t=process.env.THUBAN_SIGNING_PRIVATE_KEY;if(!t)throw new Error("THUBAN_SIGNING_PRIVATE_KEY environment variable is required to generate license keys. This keeps the signing key out of source control and off end-user machines.");const s=crypto.createPrivateKey({key:Buffer.from(t,"base64"),format:"der",type:"pkcs8"}),n=crypto.randomBytes(8).toString("hex"),a=Buffer.from(`THUBAN-${e}-${n}`);return`THUBAN-${e}-${n}-${crypto.sign(null,a,s).toString("hex")}`}}module.exports=LicenseManager;
1
+ const fs=require("fs"),path=require("path"),os=require("os"),crypto=require("crypto"),TRIAL_HMAC_SECRET_DEFAULT="thuban-trial-silverwings-2026",THUBAN_DIR=path.join(os.homedir(),".thuban"),USAGE_FILE=path.join(THUBAN_DIR,"usage.json"),LICENSE_FILE=path.join(THUBAN_DIR,"license.json"),ED25519_PUBLIC_KEY_DER_B64="MCowBQYDK2VwAyEA9/1g/P5f3OwZ/u1LNS1MY6ytTg7GLn3VIWOMxm4qwMQ=";function _trialBase64urlDecode(e){const t=e+"===".slice((e.length+3)%4);return Buffer.from(t.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf-8")}function _trialHmac8(e){const t=process.env.TRIAL_HMAC_SECRET||TRIAL_HMAC_SECRET_DEFAULT;return crypto.createHmac("sha256",t).update(e).digest("hex").substring(0,8)}function validateTrialKey(e){const t=e&&e.match(/^THUBAN-TRIAL-([A-Za-z0-9_-]+)-([A-Fa-f0-9]{8})$/);if(!t)return null;const s=t[1],a=t[2],r=_trialHmac8(`THUBAN-TRIAL-${s}`);if(a.toLowerCase()!==r.toLowerCase())return null;let i;try{i=JSON.parse(_trialBase64urlDecode(s))}catch(e){return null}if(!i||!i.x||!i.e)return null;const n=Math.floor(Date.now()/1e3),o=n>i.x,c=Math.max(0,Math.ceil((i.x-n)/86400));return{valid:!o,expired:o,email:i.e,name:i.n,invitedBy:i.i,expiresAt:new Date(1e3*i.x).toISOString(),daysRemaining:c,tier:"trial"}}const TIERS={free:{name:"Free",maxFiles:100,scansPerMonth:5,showFullDetails:!1,showDashboard:!1,showAutoFix:!1,showCIAction:!1,teaserIssues:3,maxHallucinationDetail:3},pro:{name:"Pro",maxFiles:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!1,teaserIssues:1/0,maxHallucinationDetail:1/0},team:{name:"Team",maxFiles:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!0,teaserIssues:1/0,maxHallucinationDetail:1/0},enterprise:{name:"Enterprise",maxFiles:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!0,teaserIssues:1/0,maxHallucinationDetail:1/0},trial:{name:"Trial",maxFiles:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!1,teaserIssues:1/0,maxHallucinationDetail:1/0}};class LicenseManager{constructor(){this._ensureDir(),this.machineId=this._getMachineId(),this.usage=this._loadUsage(),this.license=this._loadLicense()}getTier(){if("true"===process.env.THUBAN_DEV_MODE&&!__filename.includes("node_modules"))return"enterprise";if(!this.license||!this.license.key)return"free";if(this.license.key.startsWith("THUBAN-TRIAL-")){const e=validateTrialKey(this.license.key);return e?e.expired?(this._trialExpired=!0,"free"):(this._trialInfo=e,"trial"):"free"}return this._validateKey(this.license.key)&&this.license.tier||"free"}getTierConfig(){return"true"===process.env.THUBAN_DEV_MODE?TIERS.pro:TIERS[this.getTier()]||TIERS.free}canScan(){if("true"===process.env.THUBAN_DEV_MODE&&!__filename.includes("node_modules"))return{allowed:!0,remaining:1/0,devMode:!0};const e=this.getTierConfig();if(e.scansPerMonth===1/0)return{allowed:!0,remaining:1/0};const t=this._currentMonth(),s=this.usage.scans?.[t]||0,a=Math.max(0,e.scansPerMonth-s);return{allowed:a>0,remaining:a,limit:e.scansPerMonth,used:s,resetsOn:this._nextMonthDate()}}recordScan(){if("true"===process.env.THUBAN_DEV_MODE&&!__filename.includes("node_modules"))return;const e=this._currentMonth();this.usage.scans||(this.usage.scans={}),this.usage.scans[e]=(this.usage.scans[e]||0)+1,this.usage.lastScan=(new Date).toISOString(),this.usage.totalScans=(this.usage.totalScans||0)+1,this._saveUsage()}activate(e){if(e&&e.startsWith("THUBAN-TRIAL-")){const t=validateTrialKey(e);return t?t.expired?{success:!1,error:`Trial key expired on ${new Date(t.expiresAt).toLocaleDateString("en-GB")}. Upgrade at thuban.dev/pricing`}:(this.license={key:e,tier:"trial",activatedAt:(new Date).toISOString(),machineId:this.machineId,trialExpiresAt:t.expiresAt,trialEmail:t.email,trialName:t.name},this._saveLicense(),{success:!0,tier:"trial",tierName:"Trial (Pro features)",daysRemaining:t.daysRemaining,expiresAt:t.expiresAt}):{success:!1,error:"Invalid trial key format or signature"}}const t=this._validateKey(e);return t?(this.license={key:e,tier:t.tier,activatedAt:(new Date).toISOString(),machineId:this.machineId},this._saveLicense(),{success:!0,tier:t.tier,tierName:TIERS[t.tier]?.name||t.tier}):{success:!1,error:"Invalid license key format"}}deactivate(){return this.license={},this._saveLicense(),{success:!0}}getStatus(){const e=this.getTier(),t=this.getTierConfig(),s=this.canScan();let a=!1;return a="true"===process.env.THUBAN_DEV_MODE&&!__filename.includes("node_modules"),{tier:e,tierName:t.name,machineId:this.machineId.substring(0,8)+"...",licensed:"free"!==e,devMode:a,scansUsed:a?0:s.used||0,scansRemaining:s.remaining,scansLimit:a?1/0:s.limit||t.scansPerMonth,totalScans:this.usage.totalScans||0,..."trial"===e&&this._trialInfo?{trialDaysRemaining:this._trialInfo.daysRemaining,trialExpiresAt:this._trialInfo.expiresAt,trialName:this._trialInfo.name,trialEmail:this._trialInfo.email}:{},...this._trialExpired?{trialExpired:!0}:{},features:{fullDetails:t.showFullDetails,dashboard:t.showDashboard,autoFix:t.showAutoFix,ciAction:t.showCIAction,scheduledScans:"free"!==e&&"trial"!==e}}}canUseProFeature(e="This feature"){const t=this.getTier();return"free"===t?{allowed:!1,message:`${e} is available on Thuban Pro and above.`}:{allowed:!0,tier:t}}_ensureDir(){try{fs.existsSync(THUBAN_DIR)||fs.mkdirSync(THUBAN_DIR,{recursive:!0})}catch(e){}}_getMachineId(){const e=[os.hostname(),os.userInfo().username,os.platform(),os.arch()].join("|");try{const t=os.networkInterfaces(),s=Object.values(t).flat().filter(e=>!e.internal&&e.mac&&"00:00:00:00:00:00"!==e.mac).map(e=>e.mac).sort();if(s.length>0)return crypto.createHash("sha256").update(e+"|"+s[0]).digest("hex")}catch(e){}return crypto.createHash("sha256").update(e).digest("hex")}_loadUsage(){try{return JSON.parse(fs.readFileSync(USAGE_FILE,"utf-8"))}catch(e){return{scans:{},totalScans:0}}}_atomicWriteSync(e,t){const s=e+".tmp."+process.pid+"."+Date.now();try{fs.writeFileSync(s,t,"utf-8"),fs.renameSync(s,e)}catch(e){try{fs.unlinkSync(s)}catch{}}}_saveUsage(){try{this._atomicWriteSync(USAGE_FILE,JSON.stringify(this.usage,null,2))}catch(e){}}_loadLicense(){try{return JSON.parse(fs.readFileSync(LICENSE_FILE,"utf-8"))}catch(e){return{}}}_saveLicense(){try{this._atomicWriteSync(LICENSE_FILE,JSON.stringify(this.license,null,2))}catch(e){}}_validateKey(e){if(!e||"string"!=typeof e)return null;const t=e.match(/^THUBAN-(FREE|PRO|TEAM|ENT)-([A-Za-z0-9]{16})-([A-Fa-f0-9]{8,})$/);if(!t)return null;const s=t[1],a=t[2],r=t[3],i={FREE:"free",PRO:"pro",TEAM:"team",ENT:"enterprise"};try{const e=crypto.createPublicKey({key:Buffer.from(ED25519_PUBLIC_KEY_DER_B64,"base64"),format:"der",type:"spki"}),t=Buffer.from(`THUBAN-${s}-${a}`),n=Buffer.from(r,"hex");if(crypto.verify(null,t,e,n))return{tier:i[s]||"free",valid:!0,method:"ed25519"}}catch(e){}return null}_currentMonth(){const e=new Date;return`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,"0")}`}_nextMonthDate(){const e=new Date;return new Date(e.getFullYear(),e.getMonth()+1,1).toISOString().split("T")[0]}static generateKey(e="PRO"){const t=process.env.THUBAN_SIGNING_PRIVATE_KEY;if(!t)throw new Error("THUBAN_SIGNING_PRIVATE_KEY environment variable is required to generate license keys. This keeps the signing key out of source control and off end-user machines.");const s=crypto.createPrivateKey({key:Buffer.from(t,"base64"),format:"der",type:"pkcs8"}),a=crypto.randomBytes(8).toString("hex"),r=Buffer.from(`THUBAN-${e}-${a}`);return`THUBAN-${e}-${a}-${crypto.sign(null,r,s).toString("hex")}`}}module.exports=LicenseManager;
@@ -1 +1 @@
1
- const fs=require("fs"),path=require("path"),AlertManager=require("./alert-manager.js"),MonitorStore=require("./monitor-store.js"),MonitorNotifier=require("./monitor-notifier.js"),{runScanPipeline:runScanPipeline}=require("./scan-runner.js"),{diffRuns:diffRuns}=require("./scan-diff.js"),{collectFiles:collectFiles}=require("./file-collector.js"),MIN_INTERVAL_MS=3e5;function resolveIntervalMs(e,t){return"weekly"===e?6048e5:"custom"===e?Math.max(3e5,Number(t)||864e5):864e5}class MonitorService{constructor(e={}){this.store=e.store||new MonitorStore,this.timers=new Map}configureRepo({repoPath:e,frequency:t="daily",intervalMs:r,notification:o}){const n=path.resolve(e);try{if(!fs.statSync(n).isDirectory())throw new Error(`[MonitorService] repoPath is not a directory: ${n}`)}catch(e){if("ENOENT"===e.code)throw new Error(`[MonitorService] repoPath does not exist: ${n}`);throw e}const s=this.store.getRepoId(n),i=resolveIntervalMs(t,r);return this.store.upsertRepo({repoId:s,repoPath:n,frequency:t,intervalMs:i,notification:o,nextRunAt:new Date(Date.now()+i).toISOString()})}async runRepoScan(e){const t=path.resolve(e.repoPath),r=this._collectFiles(t),o=Date.now(),n=await runScanPipeline({rootPath:t,files:r}),s={repoId:e.repoId,repoPath:t,timestamp:(new Date).toISOString(),durationMs:Date.now()-o,frequency:e.frequency,metrics:n.metrics,summary:n.summary,issues:n.issues},i=this.store.getLatestRun(e.repoId),a=diffRuns(i,s);s.diff=a;const c=this.store.saveRun(e.repoId,s),l=this.store.upsertRepo({...e,lastRunAt:s.timestamp,nextRunAt:new Date(Date.now()+e.intervalMs).toISOString()}),u=new MonitorNotifier({alertManager:new AlertManager({rootPath:t,consoleOutput:!0,fileOutput:!0}),store:this.store,repoConfig:l});return s.notifications=await u.notify(a,s),{run:s,runPath:c,previousRun:i,diff:a}}start(e,{once:t=!1}={}){const r=async()=>{try{await this.runRepoScan(e)}catch(e){console.error("[THUBAN MONITOR] Scheduled scan failed")}};if(r(),t)return;const o=setInterval(r,e.intervalMs);return this.timers.set(e.repoId,o),o}stopAll(){for(const e of this.timers.values())clearInterval(e);this.timers.clear()}_collectFiles(e){return collectFiles(e)}}module.exports={MonitorService:MonitorService,resolveIntervalMs:resolveIntervalMs};
1
+ const fs=require("fs"),path=require("path"),AlertManager=require("./alert-manager.js"),MonitorStore=require("./monitor-store.js"),MonitorNotifier=require("./monitor-notifier.js"),{runScanPipeline:runScanPipeline}=require("./scan-runner.js"),{diffRuns:diffRuns}=require("./scan-diff.js"),{collectFiles:collectFiles}=require("./file-collector.js"),MIN_INTERVAL_MS=3e5;function resolveIntervalMs(e,t){return"weekly"===e?6048e5:"custom"===e?Math.max(3e5,Number(t)||864e5):864e5}class MonitorService{constructor(e={}){this.store=e.store||new MonitorStore,this.timers=new Map}configureRepo({repoPath:e,frequency:t="daily",intervalMs:r,notification:o}){const s=path.resolve(e);try{if(!fs.statSync(s).isDirectory())throw new Error(`[MonitorService] repoPath is not a directory: ${s}`)}catch(e){if("ENOENT"===e.code)throw new Error(`[MonitorService] repoPath does not exist: ${s}`);throw e}const i=this.store.getRepoId(s),n=resolveIntervalMs(t,r);return this.store.upsertRepo({repoId:i,repoPath:s,frequency:t,intervalMs:n,notification:o,nextRunAt:new Date(Date.now()+n).toISOString()})}async runRepoScan(e){const t=path.resolve(e.repoPath),r=this._collectFiles(t),o=Date.now(),s=await runScanPipeline({rootPath:t,files:r}),i=s.metrics.issueCount||0,n=s.summary.bySeverity&&s.summary.bySeverity.critical||0,a=s.summary.bySeverity&&s.summary.bySeverity.high||0,c=s.summary.bySeverity&&s.summary.bySeverity.warning||0,u=s.metrics.filesScanned||1,l=n/u,f=a/u,h=c/u,m=Math.max(0,Math.min(100,Math.round(100-20*l-10*f-2*h))),p={repoId:e.repoId,repoPath:t,timestamp:(new Date).toISOString(),durationMs:Date.now()-o,frequency:e.frequency,metrics:{...s.metrics,trustScore:m,totalIssues:i},summary:s.summary,issues:s.issues},y=this.store.getLatestRun(e.repoId),v=diffRuns(y,p);p.diff=v;const M=this.store.saveRun(e.repoId,p),S=this.store.upsertRepo({...e,lastRunAt:p.timestamp,nextRunAt:new Date(Date.now()+e.intervalMs).toISOString()}),d=new MonitorNotifier({alertManager:new AlertManager({rootPath:t,consoleOutput:!0,fileOutput:!0}),store:this.store,repoConfig:S});return p.notifications=await d.notify(v,p),{run:p,runPath:M,previousRun:y,diff:v}}start(e,{once:t=!1}={}){const r=async()=>{try{await this.runRepoScan(e)}catch(e){console.error("[THUBAN MONITOR] Scheduled scan failed")}};if(r(),t)return;const o=setInterval(r,e.intervalMs);return this.timers.set(e.repoId,o),o}stopAll(){for(const e of this.timers.values())clearInterval(e);this.timers.clear()}_collectFiles(e){return collectFiles(e)}}module.exports={MonitorService:MonitorService,resolveIntervalMs:resolveIntervalMs};
@@ -1 +1 @@
1
- "use strict";const parser=require("@babel/parser"),path=require("path"),fs=require("fs"),{joinLogicalStatements:joinLogicalStatements,extractBalanced:extractBalanced,splitTopLevelArgs:splitTopLevelArgs,isDynamicPyString:isDynamicPyString}=require("./ast-analyzer")._internal,BABEL_PLUGINS=["estree","typescript","jsx","classProperties","decorators-legacy"],MAX_TAINT_FILE_SIZE=2e4,MAX_FIXPOINT_ITERATIONS=10;function walk(e,t){if(e&&"object"==typeof e&&"string"==typeof e.type){t(e);for(const n in e){if("loc"===n||"range"===n||"start"===n||"end"===n)continue;const i=e[n];if(Array.isArray(i))for(const e of i)e&&"object"==typeof e&&"string"==typeof e.type&&walk(e,t);else i&&"object"==typeof i&&"string"==typeof i.type&&walk(i,t)}}}function lineOf(e){return e&&e.loc?e.loc.start.line:0}function snippet(e,t){return!t||t<1||t>e.length?"":e[t-1].trim().substring(0,100)}function mkTaintIssue(e,t,n,i){const r={file:e,line:t,category:"security",type:i.type,severity:i.severity||"critical",id:i.id,name:i.name,message:i.message,code:snippet(n,t),source:"taint",taintPath:i.taintPath};return i.crossFile&&(r.crossFile=!0,r.sourceFile=i.sourceFile),r}function fileTooLargeIssue(e,t){return{file:e,line:0,category:"quality",type:"file_too_large",severity:"info",id:"TAINT003",name:"Taint Analysis Skipped",message:`File too large (${t} chars) - taint analysis skipped (cap: 20000 chars)`,code:"",source:"taint"}}const SANITIZER_FUNCS=new Set(["parseInt","parseFloat","Number","Boolean","encodeURIComponent","encodeURI","escape","sanitize","sanitizeHtml","stripTags"]),SANITIZER_MEMBER=new Set(["sanitize","escape","escapeHtml","stripTags","clean"]),SANITIZER_RECEIVER_NAMES=new Set(["dompurify","sanitizehtml","sanitize-html","html","xss","validator","bleach","markupsafe","purify","sqlstring","sanitizer","striptags","xssfilters","insane"]);function hasKnownSanitizerReceiver(e){let t=e,n=0;for(;t&&n++<20;){if("Identifier"===t.type)return SANITIZER_RECEIVER_NAMES.has(t.name.toLowerCase());if("MemberExpression"!==t.type)return!1;t=t.object}return!1}const JS_SOURCE_PATH_PATTERNS=[{re:/^req\.(params|query|body|headers)(\.|$)/,severity:"critical"},{re:/^request\.(params|query|body|headers)(\.|$)/,severity:"critical"},{re:/^process\.argv/,severity:"critical"},{re:/^process\.env/,severity:"warning"},{re:/^document\.location/,severity:"critical"},{re:/^window\.location/,severity:"critical"},{re:/^location\.(search|hash|href)/,severity:"critical"}];function memberChainToPath(e){const t=[];let n=e,i=0;for(;n&&i++<20;){if("Identifier"===n.type){t.unshift(n.name);break}if("ThisExpression"===n.type){t.unshift("this");break}if("MemberExpression"!==n.type)return null;n.computed||"Identifier"!==n.property.type?t.unshift("*"):t.unshift(n.property.name),n=n.object}return t.length?t.join("."):null}function jsSourceSeverity(e){if(!e)return null;if("CallExpression"===e.type&&"Identifier"===e.callee.type&&"prompt"===e.callee.name)return"critical";if("NewExpression"===e.type&&"Identifier"===e.callee.type&&("URLSearchParams"===e.callee.name||"FormData"===e.callee.name))return"critical";const t=memberChainToPath(e);if(t)for(const e of JS_SOURCE_PATH_PATTERNS)if(e.re.test(t))return e.severity;return null}function isJSSourceExpr(e){return null!==jsSourceSeverity(e)}function describeJSSource(e){return memberChainToPath(e)||("CallExpression"===e.type&&"Identifier"===e.callee.type&&"prompt"===e.callee.name?"prompt()":"NewExpression"===e.type&&"Identifier"===e.callee.type?`new ${e.callee.name}()`:"tainted source")}function isSanitizerCall(e){return!(!e||"CallExpression"!==e.type)&&(!("Identifier"!==e.callee.type||!SANITIZER_FUNCS.has(e.callee.name))||!("MemberExpression"!==e.callee.type||"Identifier"!==e.callee.property.type||!SANITIZER_MEMBER.has(e.callee.property.name))&&hasKnownSanitizerReceiver(e.callee.object))}function isTaintedExpr(e,t,n){if(n=n||0,!e||n>12)return!1;if("Identifier"===e.type)return t.has(e.name);if("CallExpression"===e.type){if(isSanitizerCall(e))return!1;if(isJSSourceExpr(e))return!0;if("MemberExpression"===e.callee.type&&isTaintedExpr(e.callee.object,t,n+1))return!0;for(const i of e.arguments)if(isTaintedExpr(i,t,n+1))return!0;return!1}return"MemberExpression"===e.type?!!isJSSourceExpr(e)||isTaintedExpr(e.object,t,n+1):"NewExpression"===e.type?isJSSourceExpr(e):"BinaryExpression"===e.type&&"+"===e.operator?isTaintedExpr(e.left,t,n+1)||isTaintedExpr(e.right,t,n+1):"TemplateLiteral"===e.type?(e.expressions||[]).some(e=>isTaintedExpr(e,t,n+1)):"AssignmentExpression"===e.type?isTaintedExpr(e.right,t,n+1):"ConditionalExpression"===e.type?isTaintedExpr(e.consequent,t,n+1)||isTaintedExpr(e.alternate,t,n+1):"LogicalExpression"===e.type?isTaintedExpr(e.left,t,n+1)||isTaintedExpr(e.right,t,n+1):!("SequenceExpression"!==e.type||!e.expressions.length)&&isTaintedExpr(e.expressions[e.expressions.length-1],t,n+1)}function isDynamicTaintedBuild(e,t){return!!e&&("TemplateLiteral"===e.type?(e.expressions||[]).some(e=>isTaintedExpr(e,t)):"BinaryExpression"===e.type&&"+"===e.operator&&(isTaintedExpr(e.left,t)||isTaintedExpr(e.right,t)))}function collectFunctionReturnNodes(e){const t=[];return e.body&&"BlockStatement"===e.body.type?function n(i){if(i&&"object"==typeof i&&"string"==typeof i.type&&(i===e||"FunctionDeclaration"!==i.type&&"FunctionExpression"!==i.type&&"ArrowFunctionExpression"!==i.type)){"ReturnStatement"===i.type&&i.argument&&t.push(i.argument);for(const e in i){if("loc"===e||"range"===e||"start"===e||"end"===e)continue;const t=i[e];if(Array.isArray(t))for(const e of t)e&&"object"==typeof e&&"string"==typeof e.type&&n(e);else t&&"object"==typeof t&&"string"==typeof t.type&&n(t)}}}(e.body):e.body&&t.push(e.body),t}function findTaintOrigin(e,t){let n=null;return walk(e,e=>{if(!n)if("Identifier"===e.type&&t.has(e.name))n=t.get(e.name);else{const t=jsSourceSeverity(e);t&&(n={desc:describeJSSource(e),line:lineOf(e),severity:t})}}),n}function collectTaintedVars(e,t){const n=new Set,i=new Map,r=new Set,a=new Map;if(t)for(const[e,r]of t)n.add(e),i.set(e,r);let s=!0,o=0;function l(e){return!(!e||"CallExpression"!==e.type||"Identifier"!==e.callee.type||!r.has(e.callee.name))}for(;s&&o<10;)s=!1,o++,walk(e,e=>{if("FunctionDeclaration"!==e.type&&"FunctionExpression"!==e.type&&"ArrowFunctionExpression"!==e.type)return;const t=e.id&&e.id.name;if(t&&!r.has(t))for(const o of collectFunctionReturnNodes(e))if(isTaintedExpr(o,n)||l(o)){r.add(t),a.set(t,findTaintOrigin(o,i)||(l(o)?a.get(o.callee.name):null)||{desc:describeJSSource(o),line:lineOf(o),severity:jsSourceSeverity(o)||"critical"}),s=!0;break}}),walk(e,e=>{let t=null,r=null;if("VariableDeclarator"===e.type&&e.init&&"Identifier"===e.id.type?(t=e.id.name,r=e.init):"AssignmentExpression"===e.type&&"="===e.operator&&"Identifier"===e.left.type&&(t=e.left.name,r=e.right),!t||n.has(t))return;const o=l(r);if(isTaintedExpr(r,n)||o){n.add(t),s=!0;const l=findTaintOrigin(r,i)||(o?a.get(r.callee.name):null)||{desc:describeJSSource(r),line:lineOf(e),severity:jsSourceSeverity(r)||"critical"};i.set(t,l)}});return{taintedVars:n,origins:i}}const SQL_METHODS=new Set(["query","execute","raw","where"]);function parseJS(e){try{return parser.parse(e,{sourceType:"unambiguous",plugins:BABEL_PLUGINS,allowReturnOutsideFunction:!0,allowImportExportEverywhere:!0,allowAwaitOutsideFunction:!0,allowSuperOutsideMethod:!0,allowUndeclaredExports:!0,errorRecovery:!1}).program}catch(e){return null}}function analyzeJSTaint(e,t,n){const i=[],r=t.split("\n"),a=parseJS(t);if(!a)return i;const{taintedVars:s,origins:o}=collectTaintedVars(a,n),l=new Set;function c(t,n){const a=lineOf(t),s=`${a}:${n.id}`;if(l.has(s))return;l.add(s);const c=findTaintOrigin(t,o);c&&(n=Object.assign({},n,{severity:c.severity||"critical"}),c.crossFile&&(n=Object.assign({},n,{crossFile:!0,sourceFile:c.sourceFile}))),i.push(mkTaintIssue(e,a,r,n))}function p(e){const t=findTaintOrigin(e,o);if(t)return`${t.crossFile?`${t.sourceFile}: `:""}${t.desc} (line ${t.line}) -> sink (line ${lineOf(e)})`}return walk(a,e=>{if("CallExpression"===e.type&&"MemberExpression"===e.callee.type&&"Identifier"===e.callee.property.type&&SQL_METHODS.has(e.callee.property.name)){const t=e.arguments[0];t&&(isDynamicTaintedBuild(t,s)||isTaintedExpr(t,s))&&c(e,{type:"sql_injection",id:"TAINT001",name:"Tainted SQL Injection",message:`Tainted user input flows into .${e.callee.property.name}() without sanitization - SQL injection risk`,taintPath:p(e)})}if("AssignmentExpression"!==e.type||"MemberExpression"!==e.left.type||"Identifier"!==e.left.property.type||"innerHTML"!==e.left.property.name&&"outerHTML"!==e.left.property.name||(isTaintedExpr(e.right,s)||isDynamicTaintedBuild(e.right,s))&&c(e,{type:"xss",id:"TAINT002",name:"Tainted XSS via "+e.left.property.name,message:`Tainted user input flows into ${e.left.property.name} without sanitization - XSS risk`,taintPath:p(e)}),"CallExpression"===e.type&&"MemberExpression"===e.callee.type&&"Identifier"===e.callee.object.type&&"document"===e.callee.object.name&&"Identifier"===e.callee.property.type&&("write"===e.callee.property.name||"writeln"===e.callee.property.name)){const t=e.arguments[0];t&&(isTaintedExpr(t,s)||isDynamicTaintedBuild(t,s))&&c(e,{type:"xss",id:"TAINT002",name:`Tainted XSS via document.${e.callee.property.name}()`,message:`Tainted user input flows into document.${e.callee.property.name}() without sanitization - XSS risk`,taintPath:p(e)})}if("JSXAttribute"===e.type&&e.name&&"dangerouslySetInnerHTML"===e.name.name&&e.value&&"JSXExpressionContainer"===e.value.type){const t=e.value.expression;if(t&&"ObjectExpression"===t.type){const n=t.properties.find(e=>e.key&&("__html"===e.key.name||"__html"===e.key.value));n&&(isTaintedExpr(n.value,s)||isDynamicTaintedBuild(n.value,s))&&c(e,{type:"xss",id:"TAINT002",name:"Tainted XSS via dangerouslySetInnerHTML",message:"Tainted user input flows into dangerouslySetInnerHTML without sanitization - XSS risk",taintPath:p(e)})}}if("CallExpression"===e.type&&"MemberExpression"===e.callee.type&&"Identifier"===e.callee.object.type&&/^res(ponse)?$/.test(e.callee.object.name)&&"Identifier"===e.callee.property.type&&("send"===e.callee.property.name||"write"===e.callee.property.name)){const t=e.arguments[0];t&&"ObjectExpression"!==t.type&&"ArrayExpression"!==t.type&&(isTaintedExpr(t,s)||isDynamicTaintedBuild(t,s))&&c(e,{type:"xss",id:"TAINT002",name:`Reflected XSS via ${e.callee.object.name}.${e.callee.property.name}()`,message:`Tainted user input is reflected via ${e.callee.object.name}.${e.callee.property.name}() without sanitization - reflected XSS risk`,taintPath:p(e)})}}),i}const PY_SOURCE_PATTERNS=[/\brequest\.args\b/,/\brequest\.form\b/,/\brequest\.json\b/,/\brequest\.data\b/,/\brequest\.GET\b/,/\brequest\.POST\b/,/\bsys\.argv\b/,/\bos\.environ\b/,/\binput\s*\(/],PY_SANITIZER_WRAP_RE=/^\s*(int|float|re\.escape|html\.escape|escape|markupsafe\.escape|bleach\.clean)\s*\(/;function describePySource(e){for(const t of PY_SOURCE_PATTERNS){const n=e.match(t);if(n)return n[0]}return"tainted source"}function isPySourceText(e){return PY_SOURCE_PATTERNS.some(t=>t.test(e))}function isDynamicPyText(e){return isDynamicPyString(e)}function collectPyTaintedVars(e){const t=new Set,n=new Map;let i=!0,r=0;const a=/^\s*([A-Za-z_]\w*)\s*=(?!=)\s*([\s\S]+?)\s*$/;for(;i&&r<10;){i=!1,r++;for(const r of e){const e=r.text.split("\n")[0].match(a)||r.text.match(a);if(!e)continue;const s=e[1];if(t.has(s))continue;const o=r.text.slice(r.text.indexOf("=")+1);if(PY_SANITIZER_WRAP_RE.test(o.trim()))continue;let l=!1,c=null;if(isPySourceText(o))l=!0,c=describePySource(o);else for(const e of t)if(new RegExp("\\b"+e+"\\b").test(o)){l=!0,c=n.has(e)?n.get(e).desc:e;break}l&&(t.add(s),n.set(s,{desc:c,line:r.startLine}),i=!0)}}return{taintedVars:t,origins:n}}function textReferencesTaint(e,t){if(isPySourceText(e))return!0;for(const n of t)if(new RegExp("\\b"+n+"\\b").test(e))return!0;return!1}function analyzePythonTaint(e,t){const n=[],i=t.split("\n"),r=joinLogicalStatements(t),{taintedVars:a,origins:s}=collectPyTaintedVars(r),o=new Set;function l(t,r){const a=`${t}:${r.id}`;o.has(a)||(o.add(a),n.push(mkTaintIssue(e,t,i,r)))}function c(e,t){for(const n of a)if(new RegExp("\\b"+n+"\\b").test(e)&&s.has(n)){const e=s.get(n);return`${e.desc} (line ${e.line}) -> sink (line ${t})`}if(isPySourceText(e))return`${describePySource(e)} -> sink (line ${t})`}for(const e of r){const t=e.text,n=e.startLine,i=t.match(/\b(?:cursor|db|conn|connection)\.execute\s*\(/);if(i){const e=t.indexOf("(",i.index),r=extractBalanced(t,e),s=splitTopLevelArgs(r)[0]||"";isDynamicPyText(s)&&textReferencesTaint(s,a)&&l(n,{type:"sql_injection",id:"TAINT001",name:"Tainted SQL Injection",message:"Tainted user input flows into .execute() without sanitization - SQL injection risk",taintPath:c(s,n)})}const r=t.match(/\.(raw|extra)\s*\(|\bRawSQL\s*\(/);if(r){const e=t.indexOf("(",r.index),i=extractBalanced(t,e),s=splitTopLevelArgs(i)[0]||"";(isDynamicPyText(s)||isPySourceText(s))&&textReferencesTaint(s,a)&&l(n,{type:"sql_injection",id:"TAINT001",name:"Tainted SQL Injection (Django)",message:"Tainted user input flows into raw SQL construct without sanitization - SQL injection risk",taintPath:c(s,n)})}const s=t.match(/\btext\s*\(/);if(s){const e=t.indexOf("(",s.index),i=extractBalanced(t,e);isDynamicPyText(i)&&textReferencesTaint(i,a)&&l(n,{type:"sql_injection",id:"TAINT001",name:"Tainted SQL Injection (SQLAlchemy text())",message:"Tainted user input flows into SQLAlchemy text() without sanitization - SQL injection risk",taintPath:c(i,n)})}const o=t.match(/\bmark_safe\s*\(/);if(o){const e=t.indexOf("(",o.index),i=extractBalanced(t,e);textReferencesTaint(i,a)&&l(n,{type:"xss",id:"TAINT002",name:"Tainted XSS via mark_safe()",message:"Tainted user input flows into mark_safe() without sanitization - XSS risk",taintPath:c(i,n)})}const p=t.match(/\{\{\s*([A-Za-z_][\w.]*)[^}]*\|\s*safe\b/);p&&textReferencesTaint(p[1],a)&&l(n,{type:"xss",id:"TAINT002",name:"Tainted XSS via Jinja2 |safe filter",message:"Tainted user input is rendered with the |safe filter without sanitization - XSS risk",taintPath:c(p[1],n)})}return n}const JS_RESOLVE_EXTS=["",".js",".jsx",".ts",".tsx",".mjs",".cjs","/index.js","/index.ts","/index.jsx","/index.tsx"];function resolveImportPath(e,t,n){if(!t||!t.startsWith(".")&&!t.startsWith("/"))return null;const i=path.dirname(e),r=t.startsWith("/")?t:path.resolve(i,t);for(const e of JS_RESOLVE_EXTS){const t=r+e;if(n(t))return t}return null}function defaultFileExists(e){try{return fs.statSync(e).isFile()}catch(e){return!1}}function extractImportBindings(e){const t=[];return walk(e,e=>{if("ImportDeclaration"===e.type&&e.source&&"string"==typeof e.source.value){const n=e.source.value;for(const i of e.specifiers||[])if("ImportDefaultSpecifier"===i.type)t.push({localName:i.local.name,importedName:"default",source:n});else if("ImportNamespaceSpecifier"===i.type)t.push({localName:i.local.name,importedName:"*",source:n});else if("ImportSpecifier"===i.type){const e=i.imported&&(i.imported.name||i.imported.value);t.push({localName:i.local.name,importedName:e||i.local.name,source:n})}}if("VariableDeclarator"===e.type&&e.init){const n=unwrapRequireCall(e.init);if(n)if("Identifier"===e.id.type)t.push({localName:e.id.name,importedName:"*",source:n});else if("ObjectPattern"===e.id.type)for(const i of e.id.properties)if("ObjectProperty"===i.type||"Property"===i.type){const e=i.key&&(i.key.name||i.key.value),r=i.value&&"Identifier"===i.value.type?i.value.name:e;e&&r&&t.push({localName:r,importedName:e,source:n})}}if("AssignmentExpression"===e.type&&"="===e.operator&&"Identifier"===e.left.type){const n=unwrapRequireCall(e.right);n&&t.push({localName:e.left.name,importedName:"*",source:n})}if("CallExpression"===e.type&&"MemberExpression"===e.callee.type&&"Identifier"===e.callee.property.type&&"then"===e.callee.property.name&&"ImportExpression"===e.callee.object.type&&e.callee.object.source&&"string"==typeof e.callee.object.source.value){const n=e.callee.object.source.value,i=e.arguments[0];i&&("ArrowFunctionExpression"===i.type||"FunctionExpression"===i.type)&&i.params[0]&&"Identifier"===i.params[0].type&&t.push({localName:i.params[0].name,importedName:"*",source:n})}}),t}function unwrapRequireCall(e){if(!e||"CallExpression"!==e.type)return null;if("Identifier"!==e.callee.type||"require"!==e.callee.name)return null;const t=e.arguments[0];return t&&"string"==typeof t.value?t.value:null}function extractReExports(e){const t=[];return walk(e,e=>{if("ExportNamedDeclaration"===e.type&&e.source&&"string"==typeof e.source.value){const n=e.source.value;for(const i of e.specifiers||[]){const e=i.local&&(i.local.name||i.local.value),r=i.exported&&(i.exported.name||i.exported.value);e&&r&&t.push({importedName:e,exportedName:r,source:n})}}if("ExportAllDeclaration"===e.type&&e.source&&"string"==typeof e.source.value&&t.push({importedName:"*",exportedName:"*",source:e.source.value}),"AssignmentExpression"===e.type&&"="===e.operator&&"MemberExpression"===e.left.type&&"module.exports"===memberChainToPath(e.left)){const n=unwrapRequireCall(e.right);n&&t.push({importedName:"*",exportedName:"*",source:n})}}),t}function extractLocalExports(e){const t=[];return walk(e,e=>{if("AssignmentExpression"===e.type&&"="===e.operator&&"MemberExpression"===e.left.type&&!e.left.computed&&"Identifier"===e.left.property.type){const n=memberChainToPath(e.left.object);"module.exports"!==n&&"exports"!==n||t.push({exportedName:e.left.property.name,localName:null,inlineNode:e.right})}if("AssignmentExpression"===e.type&&"="===e.operator&&"MemberExpression"===e.left.type&&"module.exports"===memberChainToPath(e.left)&&"ObjectExpression"===e.right.type)for(const n of e.right.properties){if("ObjectProperty"!==n.type&&"Property"!==n.type)continue;const e=n.key&&(n.key.name||n.key.value);e&&(n.value&&"Identifier"===n.value.type?t.push({exportedName:e,localName:n.value.name,inlineNode:null}):t.push({exportedName:e,localName:null,inlineNode:n.value}))}if("ExportNamedDeclaration"===e.type&&e.declaration){const n=e.declaration;if("VariableDeclaration"===n.type)for(const e of n.declarations)"Identifier"===e.id.type&&t.push({exportedName:e.id.name,localName:e.id.name,inlineNode:e.init||null});else"FunctionDeclaration"!==n.type&&"ClassDeclaration"!==n.type||!n.id||t.push({exportedName:n.id.name,localName:n.id.name,inlineNode:null})}if("ExportNamedDeclaration"===e.type&&!e.source)for(const n of e.specifiers||[]){const e=n.local&&(n.local.name||n.local.value),i=n.exported&&(n.exported.name||n.exported.value);e&&i&&t.push({exportedName:i,localName:e,inlineNode:null})}if("ExportDefaultDeclaration"===e.type){const n=e.declaration;n&&("FunctionDeclaration"===n.type||"ClassDeclaration"===n.type)&&n.id?t.push({exportedName:"default",localName:n.id.name,inlineNode:null}):n&&"Identifier"===n.type?t.push({exportedName:"default",localName:n.name,inlineNode:null}):t.push({exportedName:"default",localName:null,inlineNode:n})}}),t}function buildExportTaintMap(e,t){const n=(t=t||{}).readFile||(e=>fs.readFileSync(e,"utf8")),i=t.fileExists||defaultFileExists,r=t.parsedCache||null,a=new Map;for(const t of e){const e=path.extname(t).toLowerCase();if(!JS_EXTS.has(e))continue;let i;try{i=n(t)}catch(e){continue}if(r&&r.set(t,{content:i,ast:null}),i.length>2e4)continue;const s=parseJS(i);r&&(r.get(t).ast=s),s&&a.set(t,{ast:s,content:i})}const s=new Map;for(const e of a.keys())s.set(e,new Map);const o=Math.max(10,a.size+1);let l=!0,c=0;for(;l&&c<o;){l=!1,c++;for(const[e,{ast:t}]of a){const n=s.get(e),r=extractImportBindings(t),a=new Map;for(const t of r){const n=resolveImportPath(e,t.source,i);if(!n||!s.has(n))continue;const r=s.get(n);if("*"===t.importedName)for(const[,e]of r)a.has(t.localName)||a.set(t.localName,e);else r.has(t.importedName)&&a.set(t.localName,r.get(t.importedName))}const{taintedVars:o,origins:c}=collectTaintedVars(t,a),p=extractLocalExports(t);for(const t of p){if(n.has(t.exportedName))continue;let i=null;if(t.localName&&o.has(t.localName)?i=c.get(t.localName)||{desc:t.localName,line:0}:t.inlineNode&&(isTaintedExpr(t.inlineNode,o)||isDynamicTaintedBuild(t.inlineNode,o))&&(i=findTaintOrigin(t.inlineNode,c)||{desc:describeJSSource(t.inlineNode),line:lineOf(t.inlineNode)}),i){const r=i.crossFile?i.sourceFile:e;n.set(t.exportedName,{desc:i.desc,line:i.line,sourceFile:r,crossFile:!0}),l=!0}}const u=extractReExports(t);for(const t of u){const r=resolveImportPath(e,t.source,i);if(!r||!s.has(r))continue;const a=s.get(r);if("*"===t.importedName)for(const[e,t]of a)n.has(e)||(n.set(e,t),l=!0);else a.has(t.importedName)&&!n.has(t.exportedName)&&(n.set(t.exportedName,a.get(t.importedName)),l=!0)}}}return s}function buildImportSeeds(e,t,n,i){const r=new Map,a=extractImportBindings(t);for(const t of a){const a=resolveImportPath(e,t.source,i);if(!a||!n.has(a))continue;const s=n.get(a);if("*"===t.importedName)for(const[,e]of s)r.has(t.localName)||r.set(t.localName,e);else s.has(t.importedName)&&r.set(t.localName,s.get(t.importedName))}return r}function analyzeProject(e,t){const n=(t=t||{}).readFile||(e=>fs.readFileSync(e,"utf8")),i=t.fileExists||defaultFileExists,r=new Map,a=buildExportTaintMap(e,{readFile:n,fileExists:i,parsedCache:r}),s=[];for(const t of e){const e=path.extname(t).toLowerCase(),o=r.get(t);let l;if(o)l=o.content;else try{l=n(t)}catch(e){continue}if(l.length>2e4)(JS_EXTS.has(e)||PY_EXTS.has(e))&&s.push(fileTooLargeIssue(t,l.length));else try{if(JS_EXTS.has(e)){const e=o?o.ast:parseJS(l);if(!e)continue;const n=buildImportSeeds(t,e,a,i);s.push(...analyzeJSTaint(t,l,n.size?n:void 0))}else PY_EXTS.has(e)&&s.push(...analyzePythonTaint(t,l))}catch(e){}}return s}const JS_EXTS=new Set([".js",".jsx",".ts",".tsx",".mjs",".cjs"]),PY_EXTS=new Set([".py",".pyw"]);function analyze(e,t){if(t.length>2e4)return[fileTooLargeIssue(e,t.length)];const n=path.extname(e).toLowerCase();try{if(JS_EXTS.has(n))return analyzeJSTaint(e,t);if(PY_EXTS.has(n))return analyzePythonTaint(e,t)}catch(e){return[]}return[]}module.exports={analyze:analyze,analyzeProject:analyzeProject,analyzeJSTaint:analyzeJSTaint,analyzePythonTaint:analyzePythonTaint,buildExportTaintMap:buildExportTaintMap,buildImportSeeds:buildImportSeeds,resolveImportPath:resolveImportPath,_internal:{walk:walk,isTaintedExpr:isTaintedExpr,isJSSourceExpr:isJSSourceExpr,isSanitizerCall:isSanitizerCall,collectTaintedVars:collectTaintedVars,memberChainToPath:memberChainToPath,isPySourceText:isPySourceText,isDynamicPyText:isDynamicPyText,collectPyTaintedVars:collectPyTaintedVars,resolveImportPath:resolveImportPath,extractImportBindings:extractImportBindings,extractReExports:extractReExports,extractLocalExports:extractLocalExports,parseJS:parseJS}};
1
+ "use strict";const parser=require("@babel/parser"),path=require("path"),fs=require("fs"),{joinLogicalStatements:joinLogicalStatements,extractBalanced:extractBalanced,splitTopLevelArgs:splitTopLevelArgs,isDynamicPyString:isDynamicPyString}=require("./ast-analyzer")._internal,BABEL_PLUGINS=["estree","typescript","jsx","classProperties","decorators-legacy"],MAX_TAINT_FILE_SIZE=2e4,MAX_FIXPOINT_ITERATIONS=10;function walk(e,t){if(e&&"object"==typeof e&&"string"==typeof e.type){t(e);for(const n in e){if("loc"===n||"range"===n||"start"===n||"end"===n)continue;const i=e[n];if(Array.isArray(i))for(const e of i)e&&"object"==typeof e&&"string"==typeof e.type&&walk(e,t);else i&&"object"==typeof i&&"string"==typeof i.type&&walk(i,t)}}}function lineOf(e){return e&&e.loc?e.loc.start.line:0}function snippet(e,t){return!t||t<1||t>e.length?"":e[t-1].trim().substring(0,100)}function mkTaintIssue(e,t,n,i){const r={file:e,line:t,category:"security",type:i.type,severity:i.severity||"critical",id:i.id,name:i.name,message:i.message,code:snippet(n,t),source:"taint",taintPath:i.taintPath};return i.crossFile&&(r.crossFile=!0,r.sourceFile=i.sourceFile),r}function fileTooLargeIssue(e,t){return{file:e,line:0,category:"quality",type:"file_too_large",severity:"info",id:"TAINT003",name:"Taint Analysis Skipped",message:`File too large (${t} chars) - taint analysis skipped (cap: 20000 chars)`,code:"",source:"taint"}}const SANITIZER_FUNCS=new Set(["parseInt","parseFloat","Number","Boolean","encodeURIComponent","encodeURI","escape","sanitize","sanitizeHtml","stripTags"]),SANITIZER_MEMBER=new Set(["sanitize","escape","escapeHtml","stripTags","clean"]),SANITIZER_RECEIVER_NAMES=new Set(["dompurify","sanitizehtml","sanitize-html","html","xss","validator","bleach","markupsafe","purify","sqlstring","sanitizer","striptags","xssfilters","insane"]);function hasKnownSanitizerReceiver(e){let t=e,n=0;for(;t&&n++<20;){if("Identifier"===t.type)return SANITIZER_RECEIVER_NAMES.has(t.name.toLowerCase());if("MemberExpression"!==t.type)return!1;t=t.object}return!1}const JS_SOURCE_PATH_PATTERNS=[{re:/^req\.(params|query|body|headers)(\.|$)/,severity:"critical"},{re:/^request\.(params|query|body|headers)(\.|$)/,severity:"critical"},{re:/^process\.argv/,severity:"critical"},{re:/^process\.env/,severity:"warning"},{re:/^document\.location/,severity:"critical"},{re:/^window\.location/,severity:"critical"},{re:/^location\.(search|hash|href)/,severity:"critical"}];function memberChainToPath(e){const t=[];let n=e,i=0;for(;n&&i++<20;){if("Identifier"===n.type){t.unshift(n.name);break}if("ThisExpression"===n.type){t.unshift("this");break}if("MemberExpression"!==n.type)return null;n.computed||"Identifier"!==n.property.type?t.unshift("*"):t.unshift(n.property.name),n=n.object}return t.length?t.join("."):null}function jsSourceSeverity(e){if(!e)return null;if("CallExpression"===e.type&&"Identifier"===e.callee.type&&"prompt"===e.callee.name)return"critical";if("NewExpression"===e.type&&"Identifier"===e.callee.type&&("URLSearchParams"===e.callee.name||"FormData"===e.callee.name))return"critical";const t=memberChainToPath(e);if(t)for(const e of JS_SOURCE_PATH_PATTERNS)if(e.re.test(t))return e.severity;return null}function isJSSourceExpr(e){return null!==jsSourceSeverity(e)}function describeJSSource(e){return memberChainToPath(e)||("CallExpression"===e.type&&"Identifier"===e.callee.type&&"prompt"===e.callee.name?"prompt()":"NewExpression"===e.type&&"Identifier"===e.callee.type?`new ${e.callee.name}()`:"tainted source")}function isSanitizerCall(e){return!(!e||"CallExpression"!==e.type)&&(!("Identifier"!==e.callee.type||!SANITIZER_FUNCS.has(e.callee.name))||!("MemberExpression"!==e.callee.type||"Identifier"!==e.callee.property.type||!SANITIZER_MEMBER.has(e.callee.property.name))&&hasKnownSanitizerReceiver(e.callee.object))}function isTaintedExpr(e,t,n){if(n=n||0,!e||n>12)return!1;if("Identifier"===e.type)return t.has(e.name);if("CallExpression"===e.type){if(isSanitizerCall(e))return!1;if(isJSSourceExpr(e))return!0;if("MemberExpression"===e.callee.type&&isTaintedExpr(e.callee.object,t,n+1))return!0;for(const i of e.arguments)if(isTaintedExpr(i,t,n+1))return!0;return!1}return"MemberExpression"===e.type?!!isJSSourceExpr(e)||isTaintedExpr(e.object,t,n+1):"NewExpression"===e.type?isJSSourceExpr(e):"BinaryExpression"===e.type&&"+"===e.operator?isTaintedExpr(e.left,t,n+1)||isTaintedExpr(e.right,t,n+1):"TemplateLiteral"===e.type?(e.expressions||[]).some(e=>isTaintedExpr(e,t,n+1)):"AssignmentExpression"===e.type?isTaintedExpr(e.right,t,n+1):"ConditionalExpression"===e.type?isTaintedExpr(e.consequent,t,n+1)||isTaintedExpr(e.alternate,t,n+1):"LogicalExpression"===e.type?isTaintedExpr(e.left,t,n+1)||isTaintedExpr(e.right,t,n+1):!("SequenceExpression"!==e.type||!e.expressions.length)&&isTaintedExpr(e.expressions[e.expressions.length-1],t,n+1)}function isDynamicTaintedBuild(e,t){return!!e&&("TemplateLiteral"===e.type?(e.expressions||[]).some(e=>isTaintedExpr(e,t)):"BinaryExpression"===e.type&&"+"===e.operator&&(isTaintedExpr(e.left,t)||isTaintedExpr(e.right,t)))}function collectFunctionReturnNodes(e){const t=[];return e.body&&"BlockStatement"===e.body.type?function n(i){if(i&&"object"==typeof i&&"string"==typeof i.type&&(i===e||"FunctionDeclaration"!==i.type&&"FunctionExpression"!==i.type&&"ArrowFunctionExpression"!==i.type)){"ReturnStatement"===i.type&&i.argument&&t.push(i.argument);for(const e in i){if("loc"===e||"range"===e||"start"===e||"end"===e)continue;const t=i[e];if(Array.isArray(t))for(const e of t)e&&"object"==typeof e&&"string"==typeof e.type&&n(e);else t&&"object"==typeof t&&"string"==typeof t.type&&n(t)}}}(e.body):e.body&&t.push(e.body),t}function findTaintOrigin(e,t){let n=null;return walk(e,e=>{if(!n)if("Identifier"===e.type&&t.has(e.name))n=t.get(e.name);else{const t=jsSourceSeverity(e);t&&(n={desc:describeJSSource(e),line:lineOf(e),severity:t})}}),n}function collectTaintedVars(e,t){const n=new Set,i=new Map,r=new Set,a=new Map;if(t)for(const[e,r]of t)n.add(e),i.set(e,r);let s=!0,o=0;function l(e){return!(!e||"CallExpression"!==e.type||"Identifier"!==e.callee.type||!r.has(e.callee.name))}for(;s&&o<10;)s=!1,o++,walk(e,e=>{if("FunctionDeclaration"!==e.type&&"FunctionExpression"!==e.type&&"ArrowFunctionExpression"!==e.type)return;const t=e.id&&e.id.name;if(t&&!r.has(t))for(const o of collectFunctionReturnNodes(e))if(isTaintedExpr(o,n)||l(o)){r.add(t),a.set(t,findTaintOrigin(o,i)||(l(o)?a.get(o.callee.name):null)||{desc:describeJSSource(o),line:lineOf(o),severity:jsSourceSeverity(o)||"critical"}),s=!0;break}}),walk(e,e=>{let t=null,r=null;if("VariableDeclarator"===e.type&&e.init&&"Identifier"===e.id.type?(t=e.id.name,r=e.init):"AssignmentExpression"===e.type&&"="===e.operator&&"Identifier"===e.left.type&&(t=e.left.name,r=e.right),!t||n.has(t))return;const o=l(r);if(isTaintedExpr(r,n)||o){n.add(t),s=!0;const l=findTaintOrigin(r,i)||(o?a.get(r.callee.name):null)||{desc:describeJSSource(r),line:lineOf(e),severity:jsSourceSeverity(r)||"critical"};i.set(t,l)}});return{taintedVars:n,origins:i}}const SQL_METHODS=new Set(["query","execute","raw","where"]);function parseJS(e){try{return parser.parse(e,{sourceType:"unambiguous",plugins:BABEL_PLUGINS,allowReturnOutsideFunction:!0,allowImportExportEverywhere:!0,allowAwaitOutsideFunction:!0,allowSuperOutsideMethod:!0,allowUndeclaredExports:!0,errorRecovery:!1}).program}catch(e){return null}}function analyzeJSTaint(e,t,n){const i=[],r=t.split("\n"),a=parseJS(t);if(!a)return i;const{taintedVars:s,origins:o}=collectTaintedVars(a,n),l=new Set;function c(t,n){const a=lineOf(t),s=`${a}:${n.id}`;if(l.has(s))return;l.add(s);const c=findTaintOrigin(t,o);c&&(n=Object.assign({},n,{severity:c.severity||"critical"}),c.crossFile&&(n=Object.assign({},n,{crossFile:!0,sourceFile:c.sourceFile}))),i.push(mkTaintIssue(e,a,r,n))}function p(e){const t=findTaintOrigin(e,o);if(t)return`${t.crossFile?`${t.sourceFile}: `:""}${t.desc} (line ${t.line}) -> sink (line ${lineOf(e)})`}return walk(a,e=>{if("CallExpression"===e.type&&"MemberExpression"===e.callee.type&&"Identifier"===e.callee.property.type&&SQL_METHODS.has(e.callee.property.name)){const t=e.arguments[0];t&&(isDynamicTaintedBuild(t,s)||isTaintedExpr(t,s))&&c(e,{type:"sql_injection",id:"TAINT001",name:"Tainted SQL Injection",message:`Tainted user input flows into .${e.callee.property.name}() without sanitization - SQL injection risk`,taintPath:p(e)})}if("AssignmentExpression"!==e.type||"MemberExpression"!==e.left.type||"Identifier"!==e.left.property.type||"innerHTML"!==e.left.property.name&&"outerHTML"!==e.left.property.name||(isTaintedExpr(e.right,s)||isDynamicTaintedBuild(e.right,s))&&c(e,{type:"xss",id:"TAINT002",name:"Tainted XSS via "+e.left.property.name,message:`Tainted user input flows into ${e.left.property.name} without sanitization - XSS risk`,taintPath:p(e)}),"CallExpression"===e.type&&"MemberExpression"===e.callee.type&&"Identifier"===e.callee.object.type&&"document"===e.callee.object.name&&"Identifier"===e.callee.property.type&&("write"===e.callee.property.name||"writeln"===e.callee.property.name)){const t=e.arguments[0];t&&(isTaintedExpr(t,s)||isDynamicTaintedBuild(t,s))&&c(e,{type:"xss",id:"TAINT002",name:`Tainted XSS via document.${e.callee.property.name}()`,message:`Tainted user input flows into document.${e.callee.property.name}() without sanitization - XSS risk`,taintPath:p(e)})}if("JSXAttribute"===e.type&&e.name&&"dangerouslySetInnerHTML"===e.name.name&&e.value&&"JSXExpressionContainer"===e.value.type){const t=e.value.expression;if(t&&"ObjectExpression"===t.type){const n=t.properties.find(e=>e.key&&("__html"===e.key.name||"__html"===e.key.value));n&&(isTaintedExpr(n.value,s)||isDynamicTaintedBuild(n.value,s))&&c(e,{type:"xss",id:"TAINT002",name:"Tainted XSS via dangerouslySetInnerHTML",message:"Tainted user input flows into dangerouslySetInnerHTML without sanitization - XSS risk",taintPath:p(e)})}}if("CallExpression"===e.type&&"MemberExpression"===e.callee.type&&"Identifier"===e.callee.object.type&&/^res(ponse)?$/.test(e.callee.object.name)&&"Identifier"===e.callee.property.type&&("send"===e.callee.property.name||"write"===e.callee.property.name)){const t=e.arguments[0];t&&"ObjectExpression"!==t.type&&"ArrayExpression"!==t.type&&(isTaintedExpr(t,s)||isDynamicTaintedBuild(t,s))&&c(e,{type:"xss",id:"TAINT002",name:`Reflected XSS via ${e.callee.object.name}.${e.callee.property.name}()`,message:`Tainted user input is reflected via ${e.callee.object.name}.${e.callee.property.name}() without sanitization - reflected XSS risk`,taintPath:p(e)})}}),i}const PY_SOURCE_PATTERNS=[/\brequest\.args\b/,/\brequest\.form\b/,/\brequest\.json\b/,/\brequest\.data\b/,/\brequest\.GET\b/,/\brequest\.POST\b/,/\bsys\.argv\b/,/\bos\.environ\b/,/\binput\s*\(/],PY_SANITIZER_WRAP_RE=/^\s*(int|float|re\.escape|html\.escape|escape|markupsafe\.escape|bleach\.clean)\s*\(/;function describePySource(e){for(const t of PY_SOURCE_PATTERNS){const n=e.match(t);if(n)return n[0]}return"tainted source"}function isPySourceText(e){return PY_SOURCE_PATTERNS.some(t=>t.test(e))}function isDynamicPyText(e){return isDynamicPyString(e)}function collectPyTaintedVars(e){const t=new Set,n=new Map;let i=!0,r=0;const a=/^\s*([A-Za-z_]\w*)\s*=(?!=)\s*([\s\S]+?)\s*$/;for(;i&&r<10;){i=!1,r++;for(const r of e){const e=r.text.split("\n")[0].match(a)||r.text.match(a);if(!e)continue;const s=e[1],o=r.text.slice(r.text.indexOf("=")+1);if(PY_SANITIZER_WRAP_RE.test(o.trim())){t.has(s)&&(t.delete(s),n.delete(s),i=!0);continue}if(t.has(s))continue;let l=!1,c=null;if(isPySourceText(o))l=!0,c=describePySource(o);else for(const e of t)if(new RegExp("\\b"+e+"\\b").test(o)){l=!0,c=n.has(e)?n.get(e).desc:e;break}l&&(t.add(s),n.set(s,{desc:c,line:r.startLine}),i=!0)}}return{taintedVars:t,origins:n}}function textReferencesTaint(e,t){if(isPySourceText(e))return!0;for(const n of t)if(new RegExp("\\b"+n+"\\b").test(e))return!0;return!1}function analyzePythonTaint(e,t){const n=[],i=t.split("\n"),r=joinLogicalStatements(t),{taintedVars:a,origins:s}=collectPyTaintedVars(r),o=new Set;function l(t,r){const a=`${t}:${r.id}`;o.has(a)||(o.add(a),n.push(mkTaintIssue(e,t,i,r)))}function c(e,t){for(const n of a)if(new RegExp("\\b"+n+"\\b").test(e)&&s.has(n)){const e=s.get(n);return`${e.desc} (line ${e.line}) -> sink (line ${t})`}if(isPySourceText(e))return`${describePySource(e)} -> sink (line ${t})`}for(const e of r){const t=e.text,n=e.startLine,i=t.match(/\b(?:cursor|db|conn|connection)\.execute\s*\(/);if(i){const e=t.indexOf("(",i.index),r=extractBalanced(t,e),s=splitTopLevelArgs(r)[0]||"";isDynamicPyText(s)&&textReferencesTaint(s,a)&&l(n,{type:"sql_injection",id:"TAINT001",name:"Tainted SQL Injection",message:"Tainted user input flows into .execute() without sanitization - SQL injection risk",taintPath:c(s,n)})}const r=t.match(/\.(raw|extra)\s*\(|\bRawSQL\s*\(/);if(r){const e=t.indexOf("(",r.index),i=extractBalanced(t,e),s=splitTopLevelArgs(i)[0]||"";(isDynamicPyText(s)||isPySourceText(s))&&textReferencesTaint(s,a)&&l(n,{type:"sql_injection",id:"TAINT001",name:"Tainted SQL Injection (Django)",message:"Tainted user input flows into raw SQL construct without sanitization - SQL injection risk",taintPath:c(s,n)})}const s=t.match(/\btext\s*\(/);if(s){const e=t.indexOf("(",s.index),i=extractBalanced(t,e);isDynamicPyText(i)&&textReferencesTaint(i,a)&&l(n,{type:"sql_injection",id:"TAINT001",name:"Tainted SQL Injection (SQLAlchemy text())",message:"Tainted user input flows into SQLAlchemy text() without sanitization - SQL injection risk",taintPath:c(i,n)})}const o=t.match(/\bmark_safe\s*\(/);if(o){const e=t.indexOf("(",o.index),i=extractBalanced(t,e);textReferencesTaint(i,a)&&l(n,{type:"xss",id:"TAINT002",name:"Tainted XSS via mark_safe()",message:"Tainted user input flows into mark_safe() without sanitization - XSS risk",taintPath:c(i,n)})}const p=t.match(/\{\{\s*([A-Za-z_][\w.]*)[^}]*\|\s*safe\b/);p&&textReferencesTaint(p[1],a)&&l(n,{type:"xss",id:"TAINT002",name:"Tainted XSS via Jinja2 |safe filter",message:"Tainted user input is rendered with the |safe filter without sanitization - XSS risk",taintPath:c(p[1],n)})}return n}const JS_RESOLVE_EXTS=["",".js",".jsx",".ts",".tsx",".mjs",".cjs","/index.js","/index.ts","/index.jsx","/index.tsx"];function resolveImportPath(e,t,n){if(!t||!t.startsWith(".")&&!t.startsWith("/"))return null;const i=path.dirname(e),r=t.startsWith("/")?t:path.resolve(i,t);for(const e of JS_RESOLVE_EXTS){const t=r+e;if(n(t))return t}return null}function defaultFileExists(e){try{return fs.statSync(e).isFile()}catch(e){return!1}}function extractImportBindings(e){const t=[];return walk(e,e=>{if("ImportDeclaration"===e.type&&e.source&&"string"==typeof e.source.value){const n=e.source.value;for(const i of e.specifiers||[])if("ImportDefaultSpecifier"===i.type)t.push({localName:i.local.name,importedName:"default",source:n});else if("ImportNamespaceSpecifier"===i.type)t.push({localName:i.local.name,importedName:"*",source:n});else if("ImportSpecifier"===i.type){const e=i.imported&&(i.imported.name||i.imported.value);t.push({localName:i.local.name,importedName:e||i.local.name,source:n})}}if("VariableDeclarator"===e.type&&e.init){const n=unwrapRequireCall(e.init);if(n)if("Identifier"===e.id.type)t.push({localName:e.id.name,importedName:"*",source:n});else if("ObjectPattern"===e.id.type)for(const i of e.id.properties)if("ObjectProperty"===i.type||"Property"===i.type){const e=i.key&&(i.key.name||i.key.value),r=i.value&&"Identifier"===i.value.type?i.value.name:e;e&&r&&t.push({localName:r,importedName:e,source:n})}}if("AssignmentExpression"===e.type&&"="===e.operator&&"Identifier"===e.left.type){const n=unwrapRequireCall(e.right);n&&t.push({localName:e.left.name,importedName:"*",source:n})}if("CallExpression"===e.type&&"MemberExpression"===e.callee.type&&"Identifier"===e.callee.property.type&&"then"===e.callee.property.name&&"ImportExpression"===e.callee.object.type&&e.callee.object.source&&"string"==typeof e.callee.object.source.value){const n=e.callee.object.source.value,i=e.arguments[0];i&&("ArrowFunctionExpression"===i.type||"FunctionExpression"===i.type)&&i.params[0]&&"Identifier"===i.params[0].type&&t.push({localName:i.params[0].name,importedName:"*",source:n})}}),t}function unwrapRequireCall(e){if(!e||"CallExpression"!==e.type)return null;if("Identifier"!==e.callee.type||"require"!==e.callee.name)return null;const t=e.arguments[0];return t&&"string"==typeof t.value?t.value:null}function extractReExports(e){const t=[];return walk(e,e=>{if("ExportNamedDeclaration"===e.type&&e.source&&"string"==typeof e.source.value){const n=e.source.value;for(const i of e.specifiers||[]){const e=i.local&&(i.local.name||i.local.value),r=i.exported&&(i.exported.name||i.exported.value);e&&r&&t.push({importedName:e,exportedName:r,source:n})}}if("ExportAllDeclaration"===e.type&&e.source&&"string"==typeof e.source.value&&t.push({importedName:"*",exportedName:"*",source:e.source.value}),"AssignmentExpression"===e.type&&"="===e.operator&&"MemberExpression"===e.left.type&&"module.exports"===memberChainToPath(e.left)){const n=unwrapRequireCall(e.right);n&&t.push({importedName:"*",exportedName:"*",source:n})}}),t}function extractLocalExports(e){const t=[];return walk(e,e=>{if("AssignmentExpression"===e.type&&"="===e.operator&&"MemberExpression"===e.left.type&&!e.left.computed&&"Identifier"===e.left.property.type){const n=memberChainToPath(e.left.object);"module.exports"!==n&&"exports"!==n||t.push({exportedName:e.left.property.name,localName:null,inlineNode:e.right})}if("AssignmentExpression"===e.type&&"="===e.operator&&"MemberExpression"===e.left.type&&"module.exports"===memberChainToPath(e.left)&&"ObjectExpression"===e.right.type)for(const n of e.right.properties){if("ObjectProperty"!==n.type&&"Property"!==n.type)continue;const e=n.key&&(n.key.name||n.key.value);e&&(n.value&&"Identifier"===n.value.type?t.push({exportedName:e,localName:n.value.name,inlineNode:null}):t.push({exportedName:e,localName:null,inlineNode:n.value}))}if("ExportNamedDeclaration"===e.type&&e.declaration){const n=e.declaration;if("VariableDeclaration"===n.type)for(const e of n.declarations)"Identifier"===e.id.type&&t.push({exportedName:e.id.name,localName:e.id.name,inlineNode:e.init||null});else"FunctionDeclaration"!==n.type&&"ClassDeclaration"!==n.type||!n.id||t.push({exportedName:n.id.name,localName:n.id.name,inlineNode:null})}if("ExportNamedDeclaration"===e.type&&!e.source)for(const n of e.specifiers||[]){const e=n.local&&(n.local.name||n.local.value),i=n.exported&&(n.exported.name||n.exported.value);e&&i&&t.push({exportedName:i,localName:e,inlineNode:null})}if("ExportDefaultDeclaration"===e.type){const n=e.declaration;n&&("FunctionDeclaration"===n.type||"ClassDeclaration"===n.type)&&n.id?t.push({exportedName:"default",localName:n.id.name,inlineNode:null}):n&&"Identifier"===n.type?t.push({exportedName:"default",localName:n.name,inlineNode:null}):t.push({exportedName:"default",localName:null,inlineNode:n})}}),t}function buildExportTaintMap(e,t){const n=(t=t||{}).readFile||(e=>fs.readFileSync(e,"utf8")),i=t.fileExists||defaultFileExists,r=t.parsedCache||null,a=new Map;for(const t of e){const e=path.extname(t).toLowerCase();if(!JS_EXTS.has(e))continue;let i;try{i=n(t)}catch(e){continue}if(r&&r.set(t,{content:i,ast:null}),i.length>2e4)continue;const s=parseJS(i);r&&(r.get(t).ast=s),s&&a.set(t,{ast:s,content:i})}const s=new Map;for(const e of a.keys())s.set(e,new Map);const o=Math.max(10,a.size+1);let l=!0,c=0;for(;l&&c<o;){l=!1,c++;for(const[e,{ast:t}]of a){const n=s.get(e),r=extractImportBindings(t),a=new Map;for(const t of r){const n=resolveImportPath(e,t.source,i);if(!n||!s.has(n))continue;const r=s.get(n);if("*"===t.importedName)for(const[,e]of r)a.has(t.localName)||a.set(t.localName,e);else r.has(t.importedName)&&a.set(t.localName,r.get(t.importedName))}const{taintedVars:o,origins:c}=collectTaintedVars(t,a),p=extractLocalExports(t);for(const t of p){if(n.has(t.exportedName))continue;let i=null;if(t.localName&&o.has(t.localName)?i=c.get(t.localName)||{desc:t.localName,line:0}:t.inlineNode&&(isTaintedExpr(t.inlineNode,o)||isDynamicTaintedBuild(t.inlineNode,o))&&(i=findTaintOrigin(t.inlineNode,c)||{desc:describeJSSource(t.inlineNode),line:lineOf(t.inlineNode)}),i){const r=i.crossFile?i.sourceFile:e;n.set(t.exportedName,{desc:i.desc,line:i.line,sourceFile:r,crossFile:!0}),l=!0}}const u=extractReExports(t);for(const t of u){const r=resolveImportPath(e,t.source,i);if(!r||!s.has(r))continue;const a=s.get(r);if("*"===t.importedName)for(const[e,t]of a)n.has(e)||(n.set(e,t),l=!0);else a.has(t.importedName)&&!n.has(t.exportedName)&&(n.set(t.exportedName,a.get(t.importedName)),l=!0)}}}return s}function buildImportSeeds(e,t,n,i){const r=new Map,a=extractImportBindings(t);for(const t of a){const a=resolveImportPath(e,t.source,i);if(!a||!n.has(a))continue;const s=n.get(a);if("*"===t.importedName)for(const[,e]of s)r.has(t.localName)||r.set(t.localName,e);else s.has(t.importedName)&&r.set(t.localName,s.get(t.importedName))}return r}function analyzeProject(e,t){const n=(t=t||{}).readFile||(e=>fs.readFileSync(e,"utf8")),i=t.fileExists||defaultFileExists,r=new Map,a=buildExportTaintMap(e,{readFile:n,fileExists:i,parsedCache:r}),s=[];for(const t of e){const e=path.extname(t).toLowerCase(),o=r.get(t);let l;if(o)l=o.content;else try{l=n(t)}catch(e){continue}if(l.length>2e4)(JS_EXTS.has(e)||PY_EXTS.has(e))&&s.push(fileTooLargeIssue(t,l.length));else try{if(JS_EXTS.has(e)){const e=o?o.ast:parseJS(l);if(!e)continue;const n=buildImportSeeds(t,e,a,i);s.push(...analyzeJSTaint(t,l,n.size?n:void 0))}else PY_EXTS.has(e)&&s.push(...analyzePythonTaint(t,l))}catch(e){}}return s}const JS_EXTS=new Set([".js",".jsx",".ts",".tsx",".mjs",".cjs"]),PY_EXTS=new Set([".py",".pyw"]);function analyze(e,t){if(t.length>2e4)return[fileTooLargeIssue(e,t.length)];const n=path.extname(e).toLowerCase();try{if(JS_EXTS.has(n))return analyzeJSTaint(e,t);if(PY_EXTS.has(n))return analyzePythonTaint(e,t)}catch(e){return[]}return[]}module.exports={analyze:analyze,analyzeProject:analyzeProject,analyzeJSTaint:analyzeJSTaint,analyzePythonTaint:analyzePythonTaint,buildExportTaintMap:buildExportTaintMap,buildImportSeeds:buildImportSeeds,resolveImportPath:resolveImportPath,_internal:{walk:walk,isTaintedExpr:isTaintedExpr,isJSSourceExpr:isJSSourceExpr,isSanitizerCall:isSanitizerCall,collectTaintedVars:collectTaintedVars,memberChainToPath:memberChainToPath,isPySourceText:isPySourceText,isDynamicPyText:isDynamicPyText,collectPyTaintedVars:collectPyTaintedVars,resolveImportPath:resolveImportPath,extractImportBindings:extractImportBindings,extractReExports:extractReExports,extractLocalExports:extractLocalExports,parseJS:parseJS}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thuban",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "The safety layer for AI-coded software. Detect hallucinated APIs, ghost code, tech debt, and architecture drift. One command. Minimal dependencies.",
5
5
  "bin": {
6
6
  "thuban": "./dist/cli.js"