thuban 0.4.6 → 0.4.8
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 +1 -1
- package/dist/package.json +4 -3
- package/dist/packages/scanner/ai-confidence-scorer.js +1 -1
- package/dist/packages/scanner/code-scanner.js +1 -1
- package/dist/packages/scanner/drift-detector.js +1 -1
- package/dist/packages/scanner/feedback-client.js +1 -0
- package/dist/packages/scanner/hallucination-detector.js +1 -1
- package/dist/packages/scanner/investor-report.js +1 -0
- package/dist/packages/scanner/license-manager.js +1 -1
- package/dist/packages/scanner/python_ast_helper.py +426 -0
- package/dist/packages/scanner/slack-notifier.js +1 -0
- package/dist/packages/scanner/support-bot.js +1 -0
- package/dist/packages/scanner/support-knowledge.js +1 -0
- package/dist/packages/scanner/taint-tracker.js +1 -1
- package/dist/packages/scanner/widget-generator.js +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),crypto=require("crypto");class InvestorReport{constructor(e={}){this.rootPath=e.rootPath||process.cwd(),this.companyName=e.companyName||this._detectProjectName(),this.generatedAt=new Date,this.reportKey=e.reportKey||null,this.tier=this._validateKey(this.reportKey)}_validateKey(e){if(!e||"string"!=typeof e)return"free";try{const n=e.split(":");if(n.length<3)return"free";const t=n[0],s=n[1],r=n.slice(2).join(":");if(!["snapshot","full"].includes(t))return"free";const o=parseInt(s,10);if(isNaN(o)||Date.now()-o>7776e6)return"free";const a="thuban-investor-salt-2026";return r===crypto.createHmac("sha256",a).update(`thuban-investor-${t}:${s}`).digest("hex").slice(0,16)?t:"free"}catch{return"free"}}static generateKey(e="full"){const n=Date.now().toString();return`${e}:${n}:${crypto.createHmac("sha256","thuban-investor-salt-2026").update(`thuban-investor-${e}:${n}`).digest("hex").slice(0,16)}`}_detectProjectName(){try{return JSON.parse(fs.readFileSync(path.join(this.rootPath,"package.json"),"utf-8")).name||path.basename(this.rootPath)}catch{return path.basename(this.rootPath)}}generate(e){const{scanResults:n={},hallResults:t={},debtCost:s={},ghostResults:r={},aiScoreResults:o={},cloneResults:a={},passportData:i={},fileCount:l=0,lineCount:d=0}=e,c=`THB-INV-${Date.now().toString(36).toUpperCase()}`,m=this._calculateOverallScore(e),u=this._scoreToGrade(m),g=this._gradeColor(u);return this._renderHTML({reportId:c,score:m,grade:u,gradeColor:g,scanResults:n,hallResults:t,debtCost:s,ghostResults:r,aiScoreResults:o,cloneResults:a,passportData:i,fileCount:l,lineCount:d,tier:this.tier})}_calculateOverallScore(e){let n=100;const t=e.hallResults||{},s=e.ghostResults||{},r=e.cloneResults||{},o=e.aiScoreResults||{},a=e.scanResults||{};n-=15*(t.phantomAPIs?.length||0),n-=5*(t.deprecatedAPIs?.length||0),n-=2*(s.totalGhosts||0),n-=3*(r.totalClusters||0),n-=1*(o.aiLikelyCount||0);let i=0;if(a&&"object"==typeof a)for(const[,e]of Object.entries(a))e?.issues&&(i+=e.issues.length);return n-=Math.min(.5*i,30),Math.max(0,Math.min(100,Math.round(n)))}_scoreToGrade(e){return e>=90?"A":e>=80?"B":e>=65?"C":e>=50?"D":"F"}_gradeColor(e){return{A:"#00ff88",B:"#88ff00",C:"#ffd93d",D:"#ff8844",F:"#ff4444"}[e]||"#ff4444"}_investmentVerdict(e){return e>=80?{label:"PASS — Low Risk Investment",color:"#00ff88",bg:"rgba(0,255,136,0.1)",border:"rgba(0,255,136,0.3)"}:e>=65?{label:"CONDITIONAL PASS — Moderate Risk, Remediation Required",color:"#ffd93d",bg:"rgba(255,217,61,0.1)",border:"rgba(255,217,61,0.3)"}:e>=50?{label:"HIGH RISK — Significant Technical Debt",color:"#ff8844",bg:"rgba(255,136,68,0.1)",border:"rgba(255,136,68,0.3)"}:{label:"DO NOT INVEST — Critical Code Quality Issues",color:"#ff4444",bg:"rgba(255,68,68,0.1)",border:"rgba(255,68,68,0.3)"}}_aiDependencyAssessment(e){return e<10?{level:"Minimal",description:"Minimal AI dependency — traditional development team",color:"#00ff88"}:e<30?{level:"Moderate",description:"Moderate AI usage — within normal range for modern development",color:"#ffd93d"}:e<60?{level:"Heavy",description:"Heavy AI dependency — verify team can maintain without AI tools",color:"#ff8844"}:{level:"AI-Dominant",description:"AI-dominant codebase — high bus-factor risk if AI tools become unavailable",color:"#ff4444"}}_buildRemediationEstimate(e){const n=[],t=e.hallResults||{},s=e.ghostResults||{},r=e.cloneResults||{},o=e.aiScoreResults||{},a=t.phantomAPIs?.length||0,i=t.deprecatedAPIs?.length||0,l=s.totalGhosts||0,d=r.totalClusters||0,c=o.aiLikelyCount||0;if(a>0){const e=2*a;n.push({category:"Hallucinated APIs",issues:a,hours:e,cost80:80*e,cost120:120*e,priority:"Critical"})}if(i>0){const e=Math.ceil(1.5*i);n.push({category:"Deprecated APIs",issues:i,hours:e,cost80:80*e,cost120:120*e,priority:"High"})}if(l>0){const e=Math.ceil(.5*l);n.push({category:"Ghost / Dead Code",issues:l,hours:e,cost80:80*e,cost120:120*e,priority:"Medium"})}if(d>0){const e=3*d;n.push({category:"Code Clones / Duplication",issues:d,hours:e,cost80:80*e,cost120:120*e,priority:"Medium"})}if(c>0){const e=1*c;n.push({category:"AI-Generated Code Review",issues:c,hours:e,cost80:80*e,cost120:120*e,priority:"Low"})}let m=0;const u=e.scanResults||{};if(u&&"object"==typeof u)for(const[,e]of Object.entries(u))e?.issues&&(m+=e.issues.length);if(m>0){const e=Math.ceil(.3*m);n.push({category:"Code Quality Issues",issues:m,hours:e,cost80:80*e,cost120:120*e,priority:"Low"})}return n}_assessTeamCompetency(e){const n=e.scanResults||{},t=e.ghostResults||{},s=(e.cloneResults,e.aiScoreResults,e.lineCount,e.fileCount||1);let r=0,o=0;if(n&&"object"==typeof n)for(const[,e]of Object.entries(n))if(e?.issues){o+=e.issues.length;for(const n of e.issues){const e=(n.message||n.rule||"").toLowerCase();(e.includes("error")||e.includes("catch")||e.includes("exception")||e.includes("try"))&&r++}}const a=o>0?1-r/o:1,i=o/s,l=t.wastedPercent||0,d=(e.testFileCount||0)/s,c=e.commentRatio||0,m=[];return a>=.9?m.push({name:"Error Handling Patterns",rating:"Strong",color:"#00ff88"}):a>=.7?m.push({name:"Error Handling Patterns",rating:"Adequate",color:"#ffd93d"}):a>=.4?m.push({name:"Error Handling Patterns",rating:"Needs Improvement",color:"#ff8844"}):m.push({name:"Error Handling Patterns",rating:"Concerning",color:"#ff4444"}),i<=1?m.push({name:"Naming Consistency",rating:"Strong",color:"#00ff88"}):i<=3?m.push({name:"Naming Consistency",rating:"Adequate",color:"#ffd93d"}):i<=6?m.push({name:"Naming Consistency",rating:"Needs Improvement",color:"#ff8844"}):m.push({name:"Naming Consistency",rating:"Concerning",color:"#ff4444"}),l<=3?m.push({name:"Function Size Distribution",rating:"Strong",color:"#00ff88"}):l<=8?m.push({name:"Function Size Distribution",rating:"Adequate",color:"#ffd93d"}):l<=15?m.push({name:"Function Size Distribution",rating:"Needs Improvement",color:"#ff8844"}):m.push({name:"Function Size Distribution",rating:"Concerning",color:"#ff4444"}),d>=.3?m.push({name:"Test Coverage Indicators",rating:"Strong",color:"#00ff88"}):d>=.15?m.push({name:"Test Coverage Indicators",rating:"Adequate",color:"#ffd93d"}):d>=.05?m.push({name:"Test Coverage Indicators",rating:"Needs Improvement",color:"#ff8844"}):m.push({name:"Test Coverage Indicators",rating:"Concerning",color:"#ff4444"}),c>=.15?m.push({name:"Documentation Quality",rating:"Strong",color:"#00ff88"}):c>=.08?m.push({name:"Documentation Quality",rating:"Adequate",color:"#ffd93d"}):c>=.03?m.push({name:"Documentation Quality",rating:"Needs Improvement",color:"#ff8844"}):m.push({name:"Documentation Quality",rating:"Concerning",color:"#ff4444"}),m}_buildExecutiveSummary(e,n){const t=n.hallResults||{},s=n.ghostResults||{},r=n.aiScoreResults||{},o=t.phantomAPIs?.length||0,a=s.totalGhosts||0,i=r.aiPercentage||0,l=this._buildRemediationEstimate(n),d=l.reduce((e,n)=>e+n.hours,0),c=l.reduce((e,n)=>e+n.cost80,0);let m;m=e>=80?"demonstrates strong engineering practices":e>=65?"demonstrates competent engineering practices with areas requiring attention":e>=50?"shows foundational engineering capability but carries significant technical debt":"exhibits critical code quality deficiencies that pose material risk";let u=[`This codebase ${m} with a trust score of ${e}/100.`];return o>0?u.push(`${o} critical AI-hallucinated API call${o>1?"s":""} require${1===o?"s":""} immediate remediation (estimated ${2*o} dev-hours).`):a>0&&u.push(`${a} dead code function${a>1?"s":""} should be removed to reduce maintenance surface area.`),i>30?u.push(`${i}% of the codebase shows AI-generation signals, indicating elevated dependency on AI tooling.`):e>=65?u.push("The development team shows strong architectural discipline with manageable technical debt."):u.push("The development team would benefit from structured code review processes and automated quality gates."),d>0&&u.push(`Estimated remediation cost to reach investment-grade: £${c.toLocaleString()}.`),u.join(" ")}_buildRiskMatrix(e){const n=e.hallResults||{},t=e.ghostResults||{},s=e.cloneResults||{},r=e.scanResults||{},o=n.phantomAPIs?.length||0,a=n.deprecatedAPIs?.length||0,i=t.totalGhosts||0,l=s.totalClusters||0;let d=0,c=0;if(r&&"object"==typeof r)for(const[,e]of Object.entries(r))if(e?.issues)for(const n of e.issues){const e=(n.message||n.rule||"").toLowerCase();(e.includes("inject")||e.includes("sql")||e.includes("xss")||e.includes("taint"))&&d++,(e.includes("style")||e.includes("format")||e.includes("indent")||e.includes("naming"))&&c++}return{criticalLikely:{items:[],count:o+d},criticalUnlikely:{items:[],count:0},moderateLikely:{items:[],count:i+l},moderateUnlikely:{items:[],count:a+c},phantomCount:o,injectionCount:d,ghostCount:i,cloneCount:l,deprecatedCount:a,styleIssueCount:c}}_renderHTML(e){const n=this.generatedAt,t=n.toLocaleDateString("en-GB",{day:"numeric",month:"long",year:"numeric"}),s=n.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"}),r=e.hallResults.phantomAPIs?.length||0,o=e.hallResults.deprecatedAPIs?.length||0,a=e.ghostResults.totalGhosts||0,i=e.ghostResults.totalWastedLines||0,l=(e.cloneResults.totalClusters,e.cloneResults.totalDuplicateLines,e.aiScoreResults.aiLikelyCount||0),d=e.aiScoreResults.aiPossibleCount||0,c=e.aiScoreResults.aiPercentage||0,m=e.debtCost.totalHoursManual||0,u=e.debtCost.totalCostManual||0,g=e.debtCost.totalHoursThuban||0,h=e.debtCost.totalSaving||0,p=e.debtCost.savingPercent||0,v=this._investmentVerdict(e.score),f=this._aiDependencyAssessment(c),b=this._buildExecutiveSummary(e.score,{hallResults:e.hallResults,ghostResults:e.ghostResults,aiScoreResults:e.aiScoreResults,debtCost:e.debtCost,scanResults:e.scanResults,fileCount:e.fileCount,lineCount:e.lineCount}),y=this._buildRemediationEstimate({hallResults:e.hallResults,ghostResults:e.ghostResults,aiScoreResults:e.aiScoreResults,cloneResults:e.cloneResults,scanResults:e.scanResults}),x=y.reduce((e,n)=>e+n.hours,0),C=y.reduce((e,n)=>e+n.cost80,0),w=y.reduce((e,n)=>e+n.cost120,0),$=y.reduce((e,n)=>e+n.issues,0),R=this._buildRiskMatrix({hallResults:e.hallResults,ghostResults:e.ghostResults,cloneResults:e.cloneResults,scanResults:e.scanResults}),k=this._assessTeamCompetency({scanResults:e.scanResults,ghostResults:e.ghostResults,cloneResults:e.cloneResults,aiScoreResults:e.aiScoreResults,fileCount:e.fileCount,lineCount:e.lineCount,testFileCount:e.passportData?.testFileCount||0,commentRatio:e.passportData?.commentRatio||0});let I="";if(e.hallResults.phantomAPIs)for(const n of e.hallResults.phantomAPIs.slice(0,8))I+=`<tr>\n <td class="sev-critical">CRITICAL</td>\n <td>${this._esc(n.file||"")}:${n.line||"?"}</td>\n <td>${this._esc(n.name||n.code||"Phantom API")}</td>\n <td>Will crash at runtime — method does not exist</td>\n </tr>`;if(e.hallResults.deprecatedAPIs)for(const n of e.hallResults.deprecatedAPIs.slice(0,5))I+=`<tr>\n <td class="sev-high">HIGH</td>\n <td>${this._esc(n.file||"")}:${n.line||"?"}</td>\n <td>${this._esc(n.name||n.code||"Deprecated API")}</td>\n <td>Deprecated — will break in future versions</td>\n </tr>`;let A="";if(e.ghostResults.ghosts)for(const n of e.ghostResults.ghosts.slice(0,8))A+=`<tr>\n <td>${this._esc(n.file)}:${n.line}</td>\n <td>${this._esc(n.name)}()</td>\n <td>${n.wastedLines} lines</td>\n <td>${n.references} references</td>\n </tr>`;let S="";for(const e of y){const n="Critical"===e.priority?"var(--red)":"High"===e.priority?"var(--yellow)":"Medium"===e.priority?"var(--cyan)":"var(--dim)";S+=`<tr>\n <td>${this._esc(e.category)}</td>\n <td style="text-align:center">${e.issues}</td>\n <td style="text-align:center">${e.hours}</td>\n <td style="text-align:right">£${e.cost80.toLocaleString()}</td>\n <td style="text-align:right">£${e.cost120.toLocaleString()}</td>\n <td style="color:${n}; font-weight:600">${e.priority}</td>\n </tr>`}let z="";for(const e of k)z+=`<tr>\n <td>${this._esc(e.name)}</td>\n <td style="color:${e.color}; font-weight:600">${e.rating}</td>\n </tr>`;const T=[{label:"Pre-seed typical",min:40,max:55},{label:"Seed typical",min:50,max:65},{label:"Series A typical",min:60,max:75},{label:"Series B typical",min:70,max:80},{label:"Series C+",min:80,max:90}];let P="";for(const n of T)P+=`\n <div class="benchmark-row">\n <div class="benchmark-label">${n.label}</div>\n <div class="benchmark-track">\n <div class="benchmark-range" style="left:${n.min}%; width:${n.max-n.min}%"></div>\n <div class="benchmark-marker" style="left:${e.score}%" title="This codebase: ${e.score}"></div>\n </div>\n <div class="benchmark-values">${n.min}–${n.max}</div>\n </div>`;return`<!DOCTYPE html>\n<html lang="en">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>Thuban Investor Due Diligence Report — ${this._esc(this.companyName)}</title>\n<style>\n @page { size: A4; margin: 15mm; }\n @media print {\n body { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }\n .no-print { display: none !important; }\n .page-break { page-break-before: always; }\n .verdict-banner { break-inside: avoid; }\n }\n\n :root {\n --bg: #0a0a1a;\n --surface: #12122a;\n --border: #2a2a5a;\n --text: #e0e0f0;\n --dim: #8888aa;\n --cyan: #00f6ff;\n --green: #00ff88;\n --yellow: #ffd93d;\n --red: #ff4444;\n --purple: #a855f7;\n --orange: #ff8844;\n --mono: 'JetBrains Mono', 'Cascadia Code', 'Consolas', monospace;\n --sans: 'Segoe UI', system-ui, -apple-system, sans-serif;\n }\n\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n background: var(--bg);\n color: var(--text);\n font-family: var(--sans);\n font-size: 11pt;\n line-height: 1.5;\n max-width: 210mm;\n margin: 0 auto;\n padding: 20mm 15mm;\n }\n\n /* Header */\n .report-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n border-bottom: 2px solid var(--cyan);\n padding-bottom: 1.5rem;\n margin-bottom: 2rem;\n }\n\n .report-header .logo {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n }\n\n .report-header .logo svg { width: 36px; height: 36px; }\n\n .report-header .logo-text {\n font-size: 1.5rem;\n font-weight: 300;\n letter-spacing: 0.3em;\n color: var(--cyan);\n }\n\n .report-header .meta {\n text-align: right;\n font-size: 0.8rem;\n color: var(--dim);\n line-height: 1.8;\n }\n\n .report-title {\n text-align: center;\n margin-bottom: 2rem;\n }\n\n .report-title h1 {\n font-size: 1.8rem;\n font-weight: 700;\n margin-bottom: 0.25rem;\n }\n\n .report-title .subtitle {\n font-size: 0.85rem;\n color: var(--dim);\n text-transform: uppercase;\n letter-spacing: 0.15em;\n margin-bottom: 0.25rem;\n }\n\n .report-title .project-name {\n color: var(--cyan);\n font-size: 1.1rem;\n font-family: var(--mono);\n }\n\n /* Verdict Banner */\n .verdict-banner {\n text-align: center;\n padding: 1.5rem 2rem;\n border-radius: 12px;\n margin-bottom: 2rem;\n font-size: 1.3rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n }\n\n /* Executive Summary */\n .exec-summary {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 1.5rem 2rem;\n margin-bottom: 2rem;\n font-size: 0.9rem;\n line-height: 1.7;\n color: var(--text);\n }\n\n .exec-summary .summary-label {\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n color: var(--dim);\n margin-bottom: 0.5rem;\n font-weight: 600;\n }\n\n /* Score Ring */\n .score-section {\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 3rem;\n margin-bottom: 2.5rem;\n padding: 2rem;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n }\n\n .score-ring {\n position: relative;\n width: 140px;\n height: 140px;\n }\n\n .score-ring svg { transform: rotate(-90deg); }\n\n .score-ring .score-value {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n text-align: center;\n }\n\n .score-ring .grade {\n font-size: 2.5rem;\n font-weight: 700;\n font-family: var(--mono);\n line-height: 1;\n }\n\n .score-ring .number {\n font-size: 0.85rem;\n color: var(--dim);\n }\n\n .score-summary { flex: 1; }\n\n .score-summary h2 {\n font-size: 1.3rem;\n margin-bottom: 0.75rem;\n }\n\n .stat-row {\n display: flex;\n gap: 2rem;\n flex-wrap: wrap;\n margin-top: 1rem;\n }\n\n .stat {\n text-align: center;\n min-width: 80px;\n }\n\n .stat .stat-value {\n font-size: 1.5rem;\n font-weight: 700;\n font-family: var(--mono);\n }\n\n .stat .stat-label {\n font-size: 0.7rem;\n color: var(--dim);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n }\n\n .stat-critical .stat-value { color: var(--red); }\n .stat-warning .stat-value { color: var(--yellow); }\n .stat-good .stat-value { color: var(--green); }\n .stat-info .stat-value { color: var(--cyan); }\n\n /* Sections */\n .section {\n margin-bottom: 2rem;\n }\n\n .section-header {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-bottom: 1rem;\n padding-bottom: 0.5rem;\n border-bottom: 1px solid var(--border);\n }\n\n .section-header h3 {\n font-size: 1rem;\n font-weight: 600;\n }\n\n .section-header .icon { font-size: 1.2rem; }\n\n /* Tables */\n table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.8rem;\n margin-bottom: 1rem;\n }\n\n th {\n text-align: left;\n padding: 0.5rem;\n background: var(--surface);\n border-bottom: 1px solid var(--border);\n font-weight: 600;\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--dim);\n }\n\n td {\n padding: 0.4rem 0.5rem;\n border-bottom: 1px solid rgba(42,42,90,0.3);\n font-family: var(--mono);\n font-size: 0.75rem;\n word-break: break-all;\n }\n\n .sev-critical { color: var(--red); font-weight: 700; }\n .sev-high { color: var(--yellow); font-weight: 600; }\n\n /* Risk Matrix */\n .risk-matrix {\n display: grid;\n grid-template-columns: auto 1fr 1fr;\n grid-template-rows: auto 1fr 1fr;\n gap: 0;\n margin-bottom: 1rem;\n border: 1px solid var(--border);\n border-radius: 12px;\n overflow: hidden;\n }\n\n .risk-matrix .rm-corner {\n background: var(--surface);\n padding: 0.5rem;\n }\n\n .risk-matrix .rm-col-header {\n background: var(--surface);\n padding: 0.6rem 0.75rem;\n text-align: center;\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--dim);\n border-bottom: 1px solid var(--border);\n }\n\n .risk-matrix .rm-row-header {\n background: var(--surface);\n padding: 0.6rem 0.75rem;\n font-size: 0.7rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--dim);\n display: flex;\n align-items: center;\n border-right: 1px solid var(--border);\n }\n\n .risk-matrix .rm-cell {\n padding: 0.75rem;\n font-size: 0.75rem;\n border-right: 1px solid rgba(42,42,90,0.3);\n border-bottom: 1px solid rgba(42,42,90,0.3);\n }\n\n .rm-cell-critical-likely { background: rgba(255,68,68,0.12); }\n .rm-cell-critical-unlikely { background: rgba(255,136,68,0.08); }\n .rm-cell-moderate-likely { background: rgba(255,217,61,0.08); }\n .rm-cell-moderate-unlikely { background: rgba(0,246,255,0.05); }\n\n .rm-cell .rm-count {\n font-family: var(--mono);\n font-size: 1.2rem;\n font-weight: 700;\n margin-bottom: 0.25rem;\n }\n\n .rm-cell .rm-items {\n font-size: 0.7rem;\n color: var(--dim);\n line-height: 1.4;\n }\n\n /* AI Dependency */\n .ai-dep-bar {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 1.5rem 2rem;\n margin-bottom: 1rem;\n }\n\n .ai-dep-meter {\n height: 24px;\n background: rgba(42,42,90,0.5);\n border-radius: 12px;\n position: relative;\n margin: 1rem 0;\n overflow: visible;\n }\n\n .ai-dep-fill {\n height: 100%;\n border-radius: 12px;\n transition: width 0.3s;\n }\n\n .ai-dep-zones {\n display: flex;\n justify-content: space-between;\n font-size: 0.65rem;\n color: var(--dim);\n margin-top: 0.5rem;\n }\n\n .ai-dep-assessment {\n margin-top: 1rem;\n font-size: 0.85rem;\n line-height: 1.6;\n }\n\n /* Cost Box */\n .cost-box {\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 12px;\n padding: 1.5rem 2rem;\n margin-bottom: 1rem;\n }\n\n .cost-grid {\n display: grid;\n grid-template-columns: 1fr 1fr 1fr;\n gap: 1.5rem;\n text-align: center;\n }\n\n .cost-item .cost-value {\n font-size: 1.8rem;\n font-weight: 700;\n font-family: var(--mono);\n }\n\n .cost-item .cost-label {\n font-size: 0.75rem;\n color: var(--dim);\n margin-top: 0.25rem;\n }\n\n .cost-manual .cost-value { color: var(--red); }\n .cost-thuban .cost-value { color: var(--cyan); }\n .cost-saving .cost-value { color: var(--green); }\n\n /* Benchmarks */\n .benchmark-row {\n display: flex;\n align-items: center;\n gap: 1rem;\n margin-bottom: 0.75rem;\n }\n\n .benchmark-label {\n width: 130px;\n font-size: 0.75rem;\n color: var(--dim);\n text-align: right;\n flex-shrink: 0;\n }\n\n .benchmark-track {\n flex: 1;\n height: 20px;\n background: rgba(42,42,90,0.4);\n border-radius: 10px;\n position: relative;\n }\n\n .benchmark-range {\n position: absolute;\n top: 3px;\n height: 14px;\n background: rgba(0,246,255,0.15);\n border: 1px solid rgba(0,246,255,0.3);\n border-radius: 7px;\n }\n\n .benchmark-marker {\n position: absolute;\n top: -2px;\n width: 4px;\n height: 24px;\n background: var(--cyan);\n border-radius: 2px;\n box-shadow: 0 0 8px rgba(0,246,255,0.5);\n transform: translateX(-2px);\n }\n\n .benchmark-values {\n width: 50px;\n font-size: 0.7rem;\n color: var(--dim);\n font-family: var(--mono);\n flex-shrink: 0;\n }\n\n /* Team Competency */\n .competency-table td:first-child {\n font-family: var(--sans);\n font-size: 0.8rem;\n word-break: normal;\n }\n\n .competency-table td:last-child {\n text-align: center;\n font-size: 0.8rem;\n width: 140px;\n }\n\n /* Callouts */\n .callout {\n padding: 1rem 1.5rem;\n border-radius: 8px;\n margin-bottom: 1rem;\n font-size: 0.85rem;\n }\n\n .callout-danger {\n background: rgba(255,68,68,0.08);\n border: 1px solid rgba(255,68,68,0.2);\n color: #ffaaaa;\n }\n\n .callout-warning {\n background: rgba(255,217,61,0.08);\n border: 1px solid rgba(255,217,61,0.2);\n color: #ffe088;\n }\n\n /* Paywall Gate */\n .paywall-section { position: relative; }\n .paywall-overlay {\n padding: 3rem 2rem;\n text-align: center;\n background: linear-gradient(135deg, rgba(18,18,42,0.95), rgba(10,10,26,0.98));\n border: 1px dashed var(--border);\n border-radius: 12px;\n }\n .paywall-message { max-width: 420px; margin: 0 auto; }\n .paywall-message h4 { font-size: 1.1rem; }\n .paywall-message code {\n background: rgba(0,246,255,0.1);\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 0.8rem;\n }\n .paywall-message a { text-decoration: none; }\n .paywall-message a:hover { text-decoration: underline; }\n\n /* Confidentiality Footer */\n .confidentiality {\n margin-top: 3rem;\n padding: 1.25rem 1.5rem;\n background: var(--surface);\n border: 1px solid var(--border);\n border-radius: 8px;\n font-size: 0.7rem;\n color: var(--dim);\n line-height: 1.6;\n text-align: center;\n }\n\n .confidentiality strong {\n color: var(--yellow);\n letter-spacing: 0.1em;\n }\n\n /* Footer */\n .report-footer {\n margin-top: 1.5rem;\n padding-top: 1.5rem;\n border-top: 1px solid var(--border);\n display: flex;\n justify-content: space-between;\n font-size: 0.75rem;\n color: var(--dim);\n }\n\n .report-footer a { color: var(--cyan); text-decoration: none; }\n\n /* Print button */\n .print-btn {\n position: fixed;\n bottom: 2rem;\n right: 2rem;\n background: var(--cyan);\n color: var(--bg);\n border: none;\n padding: 0.75rem 1.5rem;\n border-radius: 8px;\n font-weight: 600;\n font-size: 0.9rem;\n cursor: pointer;\n box-shadow: 0 4px 20px rgba(0,246,255,0.3);\n z-index: 100;\n }\n\n .print-btn:hover { background: #33f8ff; }\n\n /* Totals row */\n .totals-row td {\n font-weight: 700;\n border-top: 2px solid var(--border);\n padding-top: 0.6rem;\n }\n\n /* Responsive */\n @media screen and (max-width: 700px) {\n body { padding: 1rem; }\n .cost-grid { grid-template-columns: 1fr; }\n .stat-row { flex-direction: column; gap: 0.75rem; }\n .score-section { flex-direction: column; gap: 1.5rem; }\n .risk-matrix { grid-template-columns: auto 1fr; }\n .benchmark-label { width: 80px; font-size: 0.65rem; }\n }\n</style>\n</head>\n<body>\n\n<button class="print-btn no-print" onclick="window.print()">Save as PDF</button>\n\n\x3c!-- Header --\x3e\n<div class="report-header">\n <div class="logo">\n <svg viewBox="0 0 32 32" fill="none">\n <polygon points="16,2 30,28 2,28" stroke="#00f6ff" stroke-width="2" fill="none"/>\n <polygon points="16,8 25,25 7,25" stroke="#00f6ff" stroke-width="1" fill="rgba(0,246,255,0.1)"/>\n <circle cx="16" cy="18" r="3" fill="#00f6ff"/>\n </svg>\n <span class="logo-text">THUBAN</span>\n </div>\n <div class="meta">\n Report ID: ${e.reportId}<br>\n Generated: ${t} at ${s}<br>\n Engine: Thuban Code Health v0.3.0<br>\n Classification: Investor Due Diligence\n </div>\n</div>\n\n\x3c!-- Title --\x3e\n<div class="report-title">\n <div class="subtitle">Investor Due Diligence Report</div>\n <h1>Code Health Assessment</h1>\n <div class="project-name">${this._esc(this.companyName)}</div>\n</div>\n\n\x3c!-- Investment Verdict Banner --\x3e\n<div class="verdict-banner" style="background:${v.bg}; border:2px solid ${v.border}; color:${v.color}">\n ${v.label}\n</div>\n\n\x3c!-- Executive Summary --\x3e\n<div class="exec-summary">\n <div class="summary-label">Executive Summary</div>\n ${b}\n</div>\n\n\x3c!-- Score --\x3e\n<div class="score-section">\n <div class="score-ring">\n <svg width="140" height="140" viewBox="0 0 140 140">\n <circle cx="70" cy="70" r="60" fill="none" stroke="${e.gradeColor}22" stroke-width="10"/>\n <circle cx="70" cy="70" r="60" fill="none" stroke="${e.gradeColor}" stroke-width="10"\n stroke-dasharray="${Math.round(3.77*e.score)} 377"\n stroke-linecap="round"/>\n </svg>\n <div class="score-value">\n <div class="grade" style="color:${e.gradeColor}">${e.grade}</div>\n <div class="number">${e.score}/100</div>\n </div>\n </div>\n <div class="score-summary">\n <h2>Trust Score: ${"A"===e.grade?"Excellent":"B"===e.grade?"Good":"C"===e.grade?"Needs Attention":"D"===e.grade?"At Risk":"Critical"}</h2>\n <div class="stat-row">\n <div class="stat stat-info">\n <div class="stat-value">${e.fileCount.toLocaleString()}</div>\n <div class="stat-label">Files Scanned</div>\n </div>\n <div class="stat stat-info">\n <div class="stat-value">${e.lineCount.toLocaleString()}</div>\n <div class="stat-label">Lines of Code</div>\n </div>\n <div class="stat ${r>0?"stat-critical":"stat-good"}">\n <div class="stat-value">${r}</div>\n <div class="stat-label">Hallucinated APIs</div>\n </div>\n <div class="stat ${a>0?"stat-warning":"stat-good"}">\n <div class="stat-value">${a}</div>\n <div class="stat-label">Ghost Functions</div>\n </div>\n <div class="stat ${l>0?"stat-warning":"stat-good"}">\n <div class="stat-value">${c}%</div>\n <div class="stat-label">AI-Generated</div>\n </div>\n </div>\n </div>\n</div>\n\n${r>0?`\n<div class="callout callout-danger">\n <strong>MATERIAL RISK:</strong> ${r} API call${r>1?"s":""} in this codebase reference${1===r?"s":""} methods that do not exist.\n These will throw runtime errors in production. This represents a direct threat to service reliability and customer trust.\n</div>\n`:""}\n\n\x3c!-- Risk Matrix --\x3e\n${"free"===e.tier?'\n<div class="section paywall-section">\n <div class="section-header">\n <span class="icon">⚠</span>\n <h3>Risk Matrix</h3>\n </div>\n <div class="paywall-overlay">\n <div class="paywall-message">\n <div style="font-size:2rem;margin-bottom:0.75rem;">🔒</div>\n <h4 style="margin-bottom:0.5rem;color:var(--text);">Unlock Full Risk Analysis</h4>\n <p style="color:var(--dim);font-size:0.85rem;margin-bottom:1rem;">Risk Matrix, AI Dependency Assessment, and detailed findings are available with a Snapshot ($99) or Full Audit ($399) report key.</p>\n <div style="font-family:var(--mono);font-size:0.8rem;color:var(--cyan);background:rgba(0,246,255,0.05);padding:0.75rem 1rem;border-radius:8px;border:1px solid rgba(0,246,255,0.15);">\n Purchase at <a href="https://thuban.dev/#investors" style="color:var(--cyan)">thuban.dev</a> → run with <code>--key=YOUR_KEY</code>\n </div>\n </div>\n </div>\n</div>\n':`\n<div class="section">\n <div class="section-header">\n <span class="icon">⚠</span>\n <h3>Risk Matrix</h3>\n </div>\n <div class="risk-matrix">\n <div class="rm-corner"></div>\n <div class="rm-col-header" style="border-left:1px solid var(--border)">Likely</div>\n <div class="rm-col-header">Unlikely</div>\n\n <div class="rm-row-header">Critical</div>\n <div class="rm-cell rm-cell-critical-likely" style="border-left:1px solid rgba(42,42,90,0.3)">\n <div class="rm-count" style="color:var(--red)">${R.phantomCount+R.injectionCount}</div>\n <div class="rm-items">\n ${R.phantomCount>0?`${R.phantomCount} hallucinated API${R.phantomCount>1?"s":""}<br>`:""}\n ${R.injectionCount>0?`${R.injectionCount} injection risk${R.injectionCount>1?"s":""}`:""}\n ${0===R.phantomCount&&0===R.injectionCount?"None detected":""}\n </div>\n </div>\n <div class="rm-cell rm-cell-critical-unlikely">\n <div class="rm-count" style="color:var(--orange)">0</div>\n <div class="rm-items">Complex taint paths<br>(not yet assessed)</div>\n </div>\n\n <div class="rm-row-header" style="border-top:1px solid var(--border)">Moderate</div>\n <div class="rm-cell rm-cell-moderate-likely" style="border-left:1px solid rgba(42,42,90,0.3)">\n <div class="rm-count" style="color:var(--yellow)">${R.ghostCount+R.cloneCount}</div>\n <div class="rm-items">\n ${R.ghostCount>0?`${R.ghostCount} ghost/dead code<br>`:""}\n ${R.cloneCount>0?`${R.cloneCount} code clone${R.cloneCount>1?"s":""}`:""}\n ${0===R.ghostCount&&0===R.cloneCount?"None detected":""}\n </div>\n </div>\n <div class="rm-cell rm-cell-moderate-unlikely">\n <div class="rm-count" style="color:var(--cyan)">${R.deprecatedCount+R.styleIssueCount}</div>\n <div class="rm-items">\n ${R.deprecatedCount>0?`${R.deprecatedCount} deprecated API${R.deprecatedCount>1?"s":""}<br>`:""}\n ${R.styleIssueCount>0?`${R.styleIssueCount} style issue${R.styleIssueCount>1?"s":""}`:""}\n ${0===R.deprecatedCount&&0===R.styleIssueCount?"None detected":""}\n </div>\n </div>\n </div>\n</div>\n`}\n\n\x3c!-- AI Dependency Assessment --\x3e\n${"free"===e.tier?"":`\n<div class="section">\n <div class="section-header">\n <span class="icon">🤖</span>\n <h3>AI Dependency Assessment</h3>\n </div>\n <div class="ai-dep-bar">\n <div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:0.5rem;">\n <span style="font-size:0.85rem; color:var(--dim)">AI-Generated Code Percentage</span>\n <span style="font-family:var(--mono); font-size:1.2rem; font-weight:700; color:${f.color}">${c}%</span>\n </div>\n <div class="ai-dep-meter">\n <div class="ai-dep-fill" style="width:${Math.min(c,100)}%; background:${f.color}; opacity:0.7"></div>\n </div>\n <div class="ai-dep-zones">\n <span><10% Minimal</span>\n <span>10-30% Moderate</span>\n <span>30-60% Heavy</span>\n <span>>60% Dominant</span>\n </div>\n <div class="ai-dep-assessment">\n <strong style="color:${f.color}">${f.level}:</strong> ${f.description}\n </div>\n ${l>0?`<div style="margin-top:0.75rem; font-size:0.8rem; color:var(--dim)">\n ${l} function${l>1?"s":""} flagged as high-confidence AI-generated, ${d} flagged as possible.\n These should be reviewed for correctness, edge-case handling, and adequate test coverage.\n </div>`:""}\n </div>\n</div>\n`}\n\n<div class="page-break"></div>\n\n\x3c!-- Remediation Estimate --\x3e\n${"full"!==e.tier?'\n<div class="section paywall-section">\n <div class="section-header">\n <span class="icon">💰</span>\n <h3>Remediation Cost Estimate</h3>\n </div>\n <div class="paywall-overlay">\n <div class="paywall-message">\n <div style="font-size:2rem;margin-bottom:0.75rem;">🔒</div>\n <h4 style="margin-bottom:0.5rem;color:var(--text);">Full Audit Required</h4>\n <p style="color:var(--dim);font-size:0.85rem;margin-bottom:1rem;">Remediation cost estimates, team competency analysis, and comparable benchmarks are available with a Full Audit report key ($399).</p>\n <div style="font-family:var(--mono);font-size:0.8rem;color:var(--cyan);background:rgba(0,246,255,0.05);padding:0.75rem 1rem;border-radius:8px;border:1px solid rgba(0,246,255,0.15);">\n Purchase at <a href="https://thuban.dev/#investors" style="color:var(--cyan)">thuban.dev</a> → run with <code>--key=YOUR_KEY</code>\n </div>\n </div>\n </div>\n</div>\n':`\n<div class="section">\n <div class="section-header">\n <span class="icon">💰</span>\n <h3>Remediation Cost Estimate</h3>\n </div>\n ${y.length>0?`\n <table>\n <thead>\n <tr>\n <th>Category</th>\n <th style="text-align:center">Issues</th>\n <th style="text-align:center">Est. Hours</th>\n <th style="text-align:right">Cost @ £80/hr</th>\n <th style="text-align:right">Cost @ £120/hr</th>\n <th>Priority</th>\n </tr>\n </thead>\n <tbody>\n ${S}\n <tr class="totals-row">\n <td style="font-family:var(--sans)"><strong>TOTAL</strong></td>\n <td style="text-align:center">${$}</td>\n <td style="text-align:center">${x}</td>\n <td style="text-align:right; color:var(--yellow)">£${C.toLocaleString()}</td>\n <td style="text-align:right; color:var(--red)">£${w.toLocaleString()}</td>\n <td></td>\n </tr>\n </tbody>\n </table>\n <div style="font-size:0.8rem; color:var(--dim); margin-top:0.5rem;">\n Estimates based on industry-average remediation times. Actual costs may vary based on team familiarity and codebase complexity.\n Rates shown reflect UK market mid-range (£80/hr) and senior (£120/hr) developer costs.\n </div>\n `:'\n <div style="font-size:0.85rem; color:var(--green); padding:1rem; background:rgba(0,255,136,0.05); border-radius:8px; border:1px solid rgba(0,255,136,0.15);">\n No significant remediation items detected. This codebase meets investment-grade quality standards.\n </div>\n '}\n</div>\n`}\n\n\x3c!-- Team Competency Signals --\x3e\n${"full"!==e.tier?"":`\n<div class="section">\n <div class="section-header">\n <span class="icon">👥</span>\n <h3>Team Competency Signals</h3>\n </div>\n <p style="font-size:0.8rem; color:var(--dim); margin-bottom:1rem;">\n Derived from automated code analysis patterns. These signals indicate engineering team capability\n based on observable code quality metrics, not individual performance.\n </p>\n <table class="competency-table">\n <thead>\n <tr><th>Signal</th><th style="text-align:center">Assessment</th></tr>\n </thead>\n <tbody>${z}</tbody>\n </table>\n</div>\n`}\n\n\x3c!-- Comparable Benchmarks --\x3e\n${"full"!==e.tier?"":`\n<div class="section">\n <div class="section-header">\n <span class="icon">📈</span>\n <h3>Comparable Benchmarks</h3>\n </div>\n <p style="font-size:0.8rem; color:var(--dim); margin-bottom:1.25rem;">\n How this codebase compares to typical trust scores at each funding stage.\n The <span style="color:var(--cyan)">cyan marker</span> indicates this codebase's score of <strong>${e.score}/100</strong>.\n </p>\n <div style="background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:1.5rem 2rem;">\n ${P}\n </div>\n</div>\n`}\n\n${r+o>0&&"free"!==e.tier?`\n<div class="page-break"></div>\n\x3c!-- Hallucination Detail --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">🔮</span>\n <h3>AI Hallucination Detail</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n These API calls reference methods, modules, or endpoints that do not exist in the libraries being used.\n They were likely generated by an AI coding tool and will fail at runtime.\n </p>\n <table>\n <thead>\n <tr><th>Severity</th><th>Location</th><th>API Call</th><th>Impact</th></tr>\n </thead>\n <tbody>${I}</tbody>\n </table>\n</div>\n`:""}\n\n${a>0?`\n\x3c!-- Ghost Code Detail --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">👻</span>\n <h3>Ghost Code — Dead Functions</h3>\n </div>\n <p style="font-size:0.85rem; color:var(--dim); margin-bottom:1rem;">\n ${a} function${a>1?"s":""} exist in the codebase but are never called.\n This represents ${i.toLocaleString()} wasted lines of code (${e.ghostResults.wastedPercent||"?"}% of the codebase).\n </p>\n <table>\n <thead>\n <tr><th>Location</th><th>Function</th><th>Size</th><th>References</th></tr>\n </thead>\n <tbody>${A}</tbody>\n </table>\n</div>\n`:""}\n\n\x3c!-- Technical Debt Cost --\x3e\n<div class="section">\n <div class="section-header">\n <span class="icon">📈</span>\n <h3>Technical Debt Cost Analysis</h3>\n </div>\n <div class="cost-box">\n <div class="cost-grid">\n <div class="cost-item cost-manual">\n <div class="cost-value">£${u.toLocaleString()}</div>\n <div class="cost-label">Manual fix cost (${m} dev-hours)</div>\n </div>\n <div class="cost-item cost-thuban">\n <div class="cost-value">${g}h</div>\n <div class="cost-label">Fix time with Thuban auto-fix</div>\n </div>\n <div class="cost-item cost-saving">\n <div class="cost-value">£${h.toLocaleString()}</div>\n <div class="cost-label">Potential saving (${p}%)</div>\n </div>\n </div>\n </div>\n</div>\n\n\x3c!-- Confidentiality Notice --\x3e\n<div class="confidentiality">\n <strong>CONFIDENTIAL</strong><br>\n This report was generated by Thuban Code Health Engine for investment due diligence purposes.\n Distribution should be limited to authorised investment committee members.\n Thuban does not provide investment advice. This report reflects automated code analysis only.\n</div>\n\n\x3c!-- Footer --\x3e\n<div class="report-footer">\n <div>\n <strong>Thuban</strong> — Code Health Engine<br>\n <a href="https://thuban.dev">thuban.dev</a> | A Silverwings product\n </div>\n <div style="text-align:right;">\n Report: ${e.reportId}<br>\n This report was generated automatically.<br>\n For full details, run: <code style="font-family:var(--mono); color:var(--cyan);">npx thuban scan .</code>\n </div>\n</div>\n\n</body>\n</html>`}_esc(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}save(e,n){const t=n||this.rootPath,s=path.resolve(t),r=path.resolve(this.rootPath);if(!s.startsWith(r+path.sep)&&s!==r)throw new Error(`Output directory must be within the project root: ${r}`);const o=`thuban-investor-report-${this.generatedAt.toISOString().split("T")[0]}.html`,a=path.join(s,o);return fs.writeFileSync(a,e,"utf-8"),a}}module.exports=InvestorReport;
|
|
@@ -1 +1 @@
|
|
|
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="MCowBQYDK2VwAyEAyytegj0i6LHfXajJXodgEW6kAS9HxkWok+H3gGEy1lA=";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],r=t[2],a=_trialHmac8(`THUBAN-TRIAL-${s}`);if(r.toLowerCase()!==a.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",price:"$0/forever",maxFiles:100,maxProjects:3,scansPerMonth:200,showFullDetails:!0,showDashboard:!0,showAutoFix:!1,showCIAction:!1,showVSCode:!0,showMCP:!1,teaserIssues:1/0,maxHallucinationDetail:1/0},pro:{name:"Pro",price:"
|
|
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="MCowBQYDK2VwAyEAyytegj0i6LHfXajJXodgEW6kAS9HxkWok+H3gGEy1lA=";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],r=t[2],a=_trialHmac8(`THUBAN-TRIAL-${s}`);if(r.toLowerCase()!==a.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",price:"$0/forever",maxFiles:100,maxProjects:3,scansPerMonth:200,showFullDetails:!0,showDashboard:!0,showAutoFix:!1,showCIAction:!1,showVSCode:!0,showMCP:!1,teaserIssues:1/0,maxHallucinationDetail:1/0},pro:{name:"Pro",price:"£29/month",maxFiles:1/0,maxProjects:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!1,showVSCode:!0,showMCP:!0,teaserIssues:1/0,maxHallucinationDetail:1/0},team:{name:"Team",price:"£99/month",maxFiles:1/0,maxProjects:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!0,showVSCode:!0,showMCP:!0,teaserIssues:1/0,maxHallucinationDetail:1/0},enterprise:{name:"Enterprise",price:"£299/month",maxFiles:1/0,maxProjects:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!0,showVSCode:!0,showMCP:!0,showSSO:!0,showAuditLogs:!0,showSLA:!0,teaserIssues:1/0,maxHallucinationDetail:1/0},trial:{name:"Trial",price:"Free (14 days)",maxFiles:1/0,maxProjects:1/0,scansPerMonth:1/0,showFullDetails:!0,showDashboard:!0,showAutoFix:!0,showCIAction:!0,showVSCode:!0,showMCP:!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(){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||__filename.includes("node_modules")?TIERS[this.getTier()]||TIERS.free:TIERS.pro}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,r=Math.max(0,e.scansPerMonth-s);return{allowed:r>0,remaining:r,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 r=!1;return r="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:r,scansUsed:r?0:s.used||0,scansRemaining:s.remaining,scansLimit:r?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],r=t[2],a=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}-${r}`),n=Buffer.from(a,"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"}),r=crypto.randomBytes(8).toString("hex"),a=Buffer.from(`THUBAN-${e}-${r}`);return`THUBAN-${e}-${r}-${crypto.sign(null,a,s).toString("hex")}`}}module.exports=LicenseManager,module.exports.validateTrialKey=validateTrialKey;
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Thuban Python AST Helper
|
|
4
|
+
|
|
5
|
+
Reads Python source from stdin, parses it with the built-in `ast` module,
|
|
6
|
+
and prints a single-line JSON report to stdout describing:
|
|
7
|
+
|
|
8
|
+
- unused_import Imported names never referenced anywhere in the file
|
|
9
|
+
- unused_variable Local variables assigned in a function but never read
|
|
10
|
+
- high_complexity Functions whose McCabe cyclomatic complexity exceeds
|
|
11
|
+
COMPLEXITY_THRESHOLD
|
|
12
|
+
- dead_code Statements that are unreachable because they follow
|
|
13
|
+
an unconditional return/raise/break/continue in the
|
|
14
|
+
same block
|
|
15
|
+
- bare_except `except:` clauses with no exception type
|
|
16
|
+
- mutable_default_arg Function parameters defaulting to a mutable literal
|
|
17
|
+
(list/dict/set) or list()/dict()/set() call
|
|
18
|
+
|
|
19
|
+
This script is invoked by packages/scanner/python-ast-analyzer.js as a
|
|
20
|
+
subprocess (one call per Python file). It must never raise past main() -
|
|
21
|
+
on any parse failure it prints {"ok": false, "error": "..."} and exits 0,
|
|
22
|
+
so the caller can treat AST analysis as a best-effort supplement.
|
|
23
|
+
|
|
24
|
+
Output contract (stdout, single JSON object):
|
|
25
|
+
{"ok": true, "issues": [{"type": str, "line": int, "name": str|null, "message": str}, ...]}
|
|
26
|
+
{"ok": false, "error": str}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import ast
|
|
30
|
+
import json
|
|
31
|
+
import sys
|
|
32
|
+
|
|
33
|
+
COMPLEXITY_THRESHOLD = 10
|
|
34
|
+
|
|
35
|
+
TERMINATOR_TYPES = (ast.Return, ast.Raise, ast.Continue, ast.Break)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
39
|
+
# Unused imports
|
|
40
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
class _UnusedImportChecker:
|
|
43
|
+
def __init__(self, tree):
|
|
44
|
+
self.tree = tree
|
|
45
|
+
self.imports = [] # [{'name', 'line', 'source'}]
|
|
46
|
+
self.used_names = set()
|
|
47
|
+
self.dunder_all_names = set()
|
|
48
|
+
|
|
49
|
+
def collect(self):
|
|
50
|
+
for node in ast.walk(self.tree):
|
|
51
|
+
if isinstance(node, ast.Import):
|
|
52
|
+
for alias in node.names:
|
|
53
|
+
if alias.name == '*':
|
|
54
|
+
continue
|
|
55
|
+
bound = alias.asname or alias.name.split('.')[0]
|
|
56
|
+
self.imports.append({'name': bound, 'line': node.lineno, 'source': ''})
|
|
57
|
+
elif isinstance(node, ast.ImportFrom):
|
|
58
|
+
if node.module == '__future__':
|
|
59
|
+
continue
|
|
60
|
+
for alias in node.names:
|
|
61
|
+
if alias.name == '*':
|
|
62
|
+
continue
|
|
63
|
+
bound = alias.asname or alias.name
|
|
64
|
+
self.imports.append({'name': bound, 'line': node.lineno, 'source': node.module or ''})
|
|
65
|
+
elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
|
|
66
|
+
self.used_names.add(node.id)
|
|
67
|
+
elif isinstance(node, ast.Assign):
|
|
68
|
+
for target in node.targets:
|
|
69
|
+
if isinstance(target, ast.Name) and target.id == '__all__' and \
|
|
70
|
+
isinstance(node.value, (ast.List, ast.Tuple, ast.Set)):
|
|
71
|
+
for elt in node.value.elts:
|
|
72
|
+
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
|
|
73
|
+
self.dunder_all_names.add(elt.value)
|
|
74
|
+
|
|
75
|
+
def unused(self):
|
|
76
|
+
issues = []
|
|
77
|
+
seen = set()
|
|
78
|
+
for imp in self.imports:
|
|
79
|
+
name = imp['name']
|
|
80
|
+
if name.startswith('_'):
|
|
81
|
+
continue
|
|
82
|
+
if name in self.used_names or name in self.dunder_all_names:
|
|
83
|
+
continue
|
|
84
|
+
key = (name, imp['line'])
|
|
85
|
+
if key in seen:
|
|
86
|
+
continue
|
|
87
|
+
seen.add(key)
|
|
88
|
+
source_suffix = " from '%s'" % imp['source'] if imp['source'] else ''
|
|
89
|
+
issues.append({
|
|
90
|
+
'type': 'unused_import',
|
|
91
|
+
'line': imp['line'],
|
|
92
|
+
'name': name,
|
|
93
|
+
'message': "Unused import: '%s'%s" % (name, source_suffix),
|
|
94
|
+
})
|
|
95
|
+
return issues
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
99
|
+
# Per-function analysis: complexity, unused locals, mutable defaults
|
|
100
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
class _ComplexityVisitor(ast.NodeVisitor):
|
|
103
|
+
"""Counts McCabe decision points within a single function's own body,
|
|
104
|
+
stopping at nested function/lambda boundaries (those are analyzed
|
|
105
|
+
independently when ast.walk() reaches them on its own)."""
|
|
106
|
+
|
|
107
|
+
def __init__(self):
|
|
108
|
+
self.complexity = 1
|
|
109
|
+
|
|
110
|
+
def visit_If(self, node):
|
|
111
|
+
self.complexity += 1
|
|
112
|
+
self.generic_visit(node)
|
|
113
|
+
|
|
114
|
+
def visit_For(self, node):
|
|
115
|
+
self.complexity += 1
|
|
116
|
+
self.generic_visit(node)
|
|
117
|
+
|
|
118
|
+
def visit_AsyncFor(self, node):
|
|
119
|
+
self.complexity += 1
|
|
120
|
+
self.generic_visit(node)
|
|
121
|
+
|
|
122
|
+
def visit_While(self, node):
|
|
123
|
+
self.complexity += 1
|
|
124
|
+
self.generic_visit(node)
|
|
125
|
+
|
|
126
|
+
def visit_IfExp(self, node):
|
|
127
|
+
self.complexity += 1
|
|
128
|
+
self.generic_visit(node)
|
|
129
|
+
|
|
130
|
+
def visit_ExceptHandler(self, node):
|
|
131
|
+
self.complexity += 1
|
|
132
|
+
self.generic_visit(node)
|
|
133
|
+
|
|
134
|
+
def visit_BoolOp(self, node):
|
|
135
|
+
self.complexity += max(len(node.values) - 1, 0)
|
|
136
|
+
self.generic_visit(node)
|
|
137
|
+
|
|
138
|
+
def visit_comprehension(self, node):
|
|
139
|
+
self.complexity += len(node.ifs)
|
|
140
|
+
self.generic_visit(node)
|
|
141
|
+
|
|
142
|
+
# Nested scopes get their own complexity computation - don't descend.
|
|
143
|
+
def visit_FunctionDef(self, node):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
def visit_AsyncFunctionDef(self, node):
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
def visit_Lambda(self, node):
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _compute_complexity(fn):
|
|
154
|
+
visitor = _ComplexityVisitor()
|
|
155
|
+
for stmt in fn.body:
|
|
156
|
+
visitor.visit(stmt)
|
|
157
|
+
return visitor.complexity
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _is_mutable_default(default):
|
|
161
|
+
if isinstance(default, (ast.List, ast.Dict, ast.Set)):
|
|
162
|
+
return True
|
|
163
|
+
if isinstance(default, ast.Call) and isinstance(default.func, ast.Name):
|
|
164
|
+
return default.func.id in ('list', 'dict', 'set')
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _mutable_defaults(fn):
|
|
169
|
+
issues = []
|
|
170
|
+
args = fn.args
|
|
171
|
+
positional = list(getattr(args, 'posonlyargs', [])) + list(args.args)
|
|
172
|
+
if args.defaults:
|
|
173
|
+
for arg, default in zip(reversed(positional), reversed(args.defaults)):
|
|
174
|
+
if _is_mutable_default(default):
|
|
175
|
+
issues.append({
|
|
176
|
+
'type': 'mutable_default_arg',
|
|
177
|
+
'line': default.lineno,
|
|
178
|
+
'name': arg.arg,
|
|
179
|
+
'message': "Mutable default argument '%s' in function '%s' - shared across all calls" % (arg.arg, fn.name),
|
|
180
|
+
})
|
|
181
|
+
for arg, default in zip(args.kwonlyargs, args.kw_defaults or []):
|
|
182
|
+
if default is not None and _is_mutable_default(default):
|
|
183
|
+
issues.append({
|
|
184
|
+
'type': 'mutable_default_arg',
|
|
185
|
+
'line': default.lineno,
|
|
186
|
+
'name': arg.arg,
|
|
187
|
+
'message': "Mutable default argument '%s' in function '%s' - shared across all calls" % (arg.arg, fn.name),
|
|
188
|
+
})
|
|
189
|
+
return issues
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class _LocalAssignCollector(ast.NodeVisitor):
|
|
193
|
+
"""Collects simple (single-Name-target) local assignments made directly
|
|
194
|
+
within a function's own body - not inside nested function/lambda
|
|
195
|
+
scopes, which own their own locals."""
|
|
196
|
+
|
|
197
|
+
def __init__(self):
|
|
198
|
+
self.assigned = {} # name -> first assignment line
|
|
199
|
+
self.declared_global_nonlocal = set()
|
|
200
|
+
|
|
201
|
+
def visit_Assign(self, node):
|
|
202
|
+
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
203
|
+
name = node.targets[0].id
|
|
204
|
+
self.assigned.setdefault(name, node.lineno)
|
|
205
|
+
self.generic_visit(node)
|
|
206
|
+
|
|
207
|
+
def visit_AnnAssign(self, node):
|
|
208
|
+
if isinstance(node.target, ast.Name) and node.value is not None:
|
|
209
|
+
self.assigned.setdefault(node.target.id, node.lineno)
|
|
210
|
+
self.generic_visit(node)
|
|
211
|
+
|
|
212
|
+
def visit_For(self, node):
|
|
213
|
+
if isinstance(node.target, ast.Name):
|
|
214
|
+
self.assigned.setdefault(node.target.id, node.lineno)
|
|
215
|
+
self.generic_visit(node)
|
|
216
|
+
|
|
217
|
+
def visit_With(self, node):
|
|
218
|
+
for item in node.items:
|
|
219
|
+
if item.optional_vars is not None and isinstance(item.optional_vars, ast.Name):
|
|
220
|
+
self.assigned.setdefault(item.optional_vars.id, node.lineno)
|
|
221
|
+
self.generic_visit(node)
|
|
222
|
+
|
|
223
|
+
def visit_Global(self, node):
|
|
224
|
+
self.declared_global_nonlocal.update(node.names)
|
|
225
|
+
|
|
226
|
+
def visit_Nonlocal(self, node):
|
|
227
|
+
self.declared_global_nonlocal.update(node.names)
|
|
228
|
+
|
|
229
|
+
def visit_FunctionDef(self, node):
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
def visit_AsyncFunctionDef(self, node):
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
def visit_Lambda(self, node):
|
|
236
|
+
pass
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class _LoadNameCollector(ast.NodeVisitor):
|
|
240
|
+
"""Collects every Name read (Load context) anywhere within a subtree,
|
|
241
|
+
INCLUDING nested functions/lambdas - a variable captured by a closure
|
|
242
|
+
still counts as used."""
|
|
243
|
+
|
|
244
|
+
def __init__(self):
|
|
245
|
+
self.loaded = set()
|
|
246
|
+
|
|
247
|
+
def visit_Name(self, node):
|
|
248
|
+
if isinstance(node.ctx, ast.Load):
|
|
249
|
+
self.loaded.add(node.id)
|
|
250
|
+
self.generic_visit(node)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _unused_locals(fn):
|
|
254
|
+
assign_collector = _LocalAssignCollector()
|
|
255
|
+
for stmt in fn.body:
|
|
256
|
+
assign_collector.visit(stmt)
|
|
257
|
+
|
|
258
|
+
load_collector = _LoadNameCollector()
|
|
259
|
+
load_collector.visit(fn)
|
|
260
|
+
|
|
261
|
+
issues = []
|
|
262
|
+
for name, line in assign_collector.assigned.items():
|
|
263
|
+
if name.startswith('_'):
|
|
264
|
+
continue
|
|
265
|
+
if name in assign_collector.declared_global_nonlocal:
|
|
266
|
+
continue
|
|
267
|
+
if name in load_collector.loaded:
|
|
268
|
+
continue
|
|
269
|
+
issues.append({
|
|
270
|
+
'type': 'unused_variable',
|
|
271
|
+
'line': line,
|
|
272
|
+
'name': name,
|
|
273
|
+
'message': "Variable '%s' is assigned but never used" % name,
|
|
274
|
+
})
|
|
275
|
+
return issues
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
279
|
+
# Dead code (unreachable statements)
|
|
280
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
def _check_block(stmts, issues):
|
|
283
|
+
for i, stmt in enumerate(stmts):
|
|
284
|
+
if isinstance(stmt, TERMINATOR_TYPES):
|
|
285
|
+
if i + 1 < len(stmts):
|
|
286
|
+
nxt = stmts[i + 1]
|
|
287
|
+
kind = type(stmt).__name__.lower()
|
|
288
|
+
issues.append({
|
|
289
|
+
'type': 'dead_code',
|
|
290
|
+
'line': nxt.lineno,
|
|
291
|
+
'name': None,
|
|
292
|
+
'message': "Unreachable code after '%s' statement" % kind,
|
|
293
|
+
})
|
|
294
|
+
break # only report the first unreachable run per block
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class _DeadCodeVisitor(ast.NodeVisitor):
|
|
298
|
+
def __init__(self):
|
|
299
|
+
self.issues = []
|
|
300
|
+
|
|
301
|
+
def visit_Module(self, node):
|
|
302
|
+
_check_block(node.body, self.issues)
|
|
303
|
+
self.generic_visit(node)
|
|
304
|
+
|
|
305
|
+
def visit_FunctionDef(self, node):
|
|
306
|
+
_check_block(node.body, self.issues)
|
|
307
|
+
self.generic_visit(node)
|
|
308
|
+
|
|
309
|
+
def visit_AsyncFunctionDef(self, node):
|
|
310
|
+
_check_block(node.body, self.issues)
|
|
311
|
+
self.generic_visit(node)
|
|
312
|
+
|
|
313
|
+
def visit_If(self, node):
|
|
314
|
+
_check_block(node.body, self.issues)
|
|
315
|
+
_check_block(node.orelse, self.issues)
|
|
316
|
+
self.generic_visit(node)
|
|
317
|
+
|
|
318
|
+
def visit_For(self, node):
|
|
319
|
+
_check_block(node.body, self.issues)
|
|
320
|
+
_check_block(node.orelse, self.issues)
|
|
321
|
+
self.generic_visit(node)
|
|
322
|
+
|
|
323
|
+
def visit_AsyncFor(self, node):
|
|
324
|
+
_check_block(node.body, self.issues)
|
|
325
|
+
_check_block(node.orelse, self.issues)
|
|
326
|
+
self.generic_visit(node)
|
|
327
|
+
|
|
328
|
+
def visit_While(self, node):
|
|
329
|
+
_check_block(node.body, self.issues)
|
|
330
|
+
_check_block(node.orelse, self.issues)
|
|
331
|
+
self.generic_visit(node)
|
|
332
|
+
|
|
333
|
+
def visit_Try(self, node):
|
|
334
|
+
_check_block(node.body, self.issues)
|
|
335
|
+
for handler in node.handlers:
|
|
336
|
+
_check_block(handler.body, self.issues)
|
|
337
|
+
_check_block(node.orelse, self.issues)
|
|
338
|
+
_check_block(node.finalbody, self.issues)
|
|
339
|
+
self.generic_visit(node)
|
|
340
|
+
|
|
341
|
+
def visit_With(self, node):
|
|
342
|
+
_check_block(node.body, self.issues)
|
|
343
|
+
self.generic_visit(node)
|
|
344
|
+
|
|
345
|
+
def visit_AsyncWith(self, node):
|
|
346
|
+
_check_block(node.body, self.issues)
|
|
347
|
+
self.generic_visit(node)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
351
|
+
# Suspicious patterns: bare except
|
|
352
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
def _find_bare_except(tree):
|
|
355
|
+
issues = []
|
|
356
|
+
for node in ast.walk(tree):
|
|
357
|
+
if isinstance(node, ast.ExceptHandler) and node.type is None:
|
|
358
|
+
issues.append({
|
|
359
|
+
'type': 'bare_except',
|
|
360
|
+
'line': node.lineno,
|
|
361
|
+
'name': None,
|
|
362
|
+
'message': "Bare 'except:' catches all exceptions including SystemExit/KeyboardInterrupt - catch specific exception types instead",
|
|
363
|
+
})
|
|
364
|
+
return issues
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
368
|
+
# Entry point
|
|
369
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
def main():
|
|
372
|
+
source = sys.stdin.read()
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
tree = ast.parse(source)
|
|
376
|
+
except SyntaxError as e:
|
|
377
|
+
print(json.dumps({'ok': False, 'error': 'SyntaxError: %s' % e}))
|
|
378
|
+
return
|
|
379
|
+
except Exception as e: # pragma: no cover - defensive
|
|
380
|
+
print(json.dumps({'ok': False, 'error': str(e)}))
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
issues = []
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
importer = _UnusedImportChecker(tree)
|
|
387
|
+
importer.collect()
|
|
388
|
+
issues.extend(importer.unused())
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
for node in ast.walk(tree):
|
|
394
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
395
|
+
issues.extend(_mutable_defaults(node))
|
|
396
|
+
complexity = _compute_complexity(node)
|
|
397
|
+
if complexity > COMPLEXITY_THRESHOLD:
|
|
398
|
+
issues.append({
|
|
399
|
+
'type': 'high_complexity',
|
|
400
|
+
'line': node.lineno,
|
|
401
|
+
'name': node.name,
|
|
402
|
+
'message': "Function '%s' has cyclomatic complexity %d (threshold %d)" % (
|
|
403
|
+
node.name, complexity, COMPLEXITY_THRESHOLD),
|
|
404
|
+
})
|
|
405
|
+
issues.extend(_unused_locals(node))
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
dead_code_visitor = _DeadCodeVisitor()
|
|
411
|
+
dead_code_visitor.visit(tree)
|
|
412
|
+
issues.extend(dead_code_visitor.issues)
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
try:
|
|
417
|
+
issues.extend(_find_bare_except(tree))
|
|
418
|
+
except Exception:
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
issues.sort(key=lambda i: i.get('line') or 0)
|
|
422
|
+
print(json.dumps({'ok': True, 'issues': issues}))
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
if __name__ == '__main__':
|
|
426
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const https=require("https"),url=require("url");class SlackNotifier{constructor(e={}){this.webhookUrl=e.webhookUrl||process.env.SLACK_WEBHOOK_URL||null,this.alertsWebhookUrl=e.alertsWebhookUrl||process.env.SLACK_WEBHOOK_URL_ALERTS||this.webhookUrl,this.enabled=!!this.webhookUrl,this.appName=e.appName||"Thuban",this.iconEmoji=e.iconEmoji||":shield:"}async _send(e,t){return t?new Promise(n=>{try{const s=new URL(t),o=JSON.stringify(e),r=https.request({hostname:s.hostname,path:s.pathname,method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(o)},timeout:1e4},e=>{let t="";e.on("data",e=>t+=e),e.on("end",()=>n({ok:200===e.statusCode,status:e.statusCode,body:t}))});r.on("error",e=>n({ok:!1,error:e.message})),r.on("timeout",()=>{r.destroy(),n({ok:!1,error:"timeout"})}),r.write(o),r.end()}catch(e){n({ok:!1,error:e.message})}}):{ok:!1,error:"No webhook URL configured"}}async notifyScanComplete({projectName:e,trustScore:t,grade:n,issueCount:s,criticalCount:o,userId:r,scanId:a}){const i="A+"===n||"A"===n?":white_check_mark:":"B"===n?":large_yellow_circle:":"C"===n?":warning:":":red_circle:";return this._send({username:this.appName,icon_emoji:this.iconEmoji,blocks:[{type:"header",text:{type:"plain_text",text:`${i} Scan Complete: ${e||"Unknown"}`,emoji:!0}},{type:"section",fields:[{type:"mrkdwn",text:`*Trust Score:*\n${t}/100`},{type:"mrkdwn",text:`*Grade:*\n${n}`},{type:"mrkdwn",text:`*Issues:*\n${s} total`},{type:"mrkdwn",text:`*Critical:*\n${o||0}`}]},...r?[{type:"context",elements:[{type:"mrkdwn",text:`User: ${r} | Scan: \`${a||"N/A"}\``}]}]:[]]},this.webhookUrl)}async notifyNewUser({email:e,source:t,timestamp:n}){return this._send({username:this.appName,icon_emoji:":tada:",blocks:[{type:"header",text:{type:"plain_text",text:":tada: New Thuban User!",emoji:!0}},{type:"section",text:{type:"mrkdwn",text:`*Email:* ${e||"Anonymous"}\n*Source:* ${t||"CLI"}\n*Time:* ${n||(new Date).toISOString()}`}}]},this.alertsWebhookUrl)}async notifyFeedback({category:e,text:t,systemInfo:n,timestamp:s}){const o="bug"===e?":bug:":"feature"===e?":bulb:":":speech_balloon:";return this._send({username:this.appName,icon_emoji:o,blocks:[{type:"header",text:{type:"plain_text",text:`${o} Feedback: ${e||"General"}`,emoji:!0}},{type:"section",text:{type:"mrkdwn",text:(t||"").substring(0,1500)}},...n?[{type:"context",elements:[{type:"mrkdwn",text:`OS: ${n.os||"?"} | Node: ${n.nodeVersion||"?"} | Thuban: ${n.thubanVersion||"?"}`}]}]:[]]},this.alertsWebhookUrl)}async notifyAlert({title:e,message:t,severity:n,projectName:s}){const o="critical"===n?":rotating_light:":"high"===n?":red_circle:":"medium"===n?":warning:":":information_source:";return this._send({username:this.appName,icon_emoji:":rotating_light:",blocks:[{type:"header",text:{type:"plain_text",text:`${o} ${e}`,emoji:!0}},{type:"section",text:{type:"mrkdwn",text:t}},...s?[{type:"context",elements:[{type:"mrkdwn",text:`Project: ${s} | ${(new Date).toISOString()}`}]}]:[]]},this.alertsWebhookUrl)}async notifyDailySummary({totalUsers:e,activeToday:t,scansToday:n,totalScans:s,estimatedMRR:o,topIssues:r}){return this._send({username:this.appName,icon_emoji:":bar_chart:",blocks:[{type:"header",text:{type:"plain_text",text:":bar_chart: Thuban Daily Summary",emoji:!0}},{type:"section",fields:[{type:"mrkdwn",text:`*Total Users:*\n${e||0}`},{type:"mrkdwn",text:`*Active Today:*\n${t||0}`},{type:"mrkdwn",text:`*Scans Today:*\n${n||0}`},{type:"mrkdwn",text:`*Total Scans:*\n${s||0}`}]},{type:"section",fields:[{type:"mrkdwn",text:`*Est. MRR:*\n$${o||0}`},{type:"mrkdwn",text:`*Top Issue:*\n${r||"N/A"}`}]},{type:"divider"},{type:"context",elements:[{type:"mrkdwn",text:`Generated ${(new Date).toISOString()} | <https://thuban-alpha.vercel.app/dashboard|View Dashboard>`}]}]},this.webhookUrl)}async sendMessage(e,t){return this._send({username:this.appName,icon_emoji:this.iconEmoji,text:e,...t?{channel:t}:{}},this.webhookUrl)}async test(){return this.sendMessage(":white_check_mark: Thuban Slack integration is working! Dashboard: https://thuban-alpha.vercel.app")}}module.exports=SlackNotifier;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const fs=require("fs"),path=require("path"),os=require("os"),https=require("https"),{execSync:execSync}=require("child_process"),readline=require("readline"),KB=require("./support-knowledge.js"),C={reset:"[0m",bold:"[1m",red:"[31m",green:"[32m",yellow:"[33m",blue:"[34m",cyan:"[36m",gray:"[90m"};function ok(e){return` ${C.green}✓${C.reset} ${e}`}function warn(e){return` ${C.yellow}⚠${C.reset} ${e}`}function fail(e){return` ${C.red}✗${C.reset} ${e}`}function safeExec(e){try{return execSync(e,{encoding:"utf8",timeout:1e4,stdio:["pipe","pipe","pipe"]}).trim()}catch{return null}}function parseSemver(e){const o=(e||"").match(/(\d+)\.(\d+)\.(\d+)/);return o?{major:Number(o[1]),minor:Number(o[2]),patch:Number(o[3])}:null}function countFiles(e){try{return fs.readdirSync(e).filter(o=>{try{return fs.statSync(path.join(e,o)).isFile()}catch{return!1}}).length}catch{return-1}}function wordOverlap(e,o){const s=new Set(e.toLowerCase().split(/\s+/).filter(Boolean)),t=new Set(o.toLowerCase().split(/\s+/).filter(Boolean));if(0===s.size||0===t.size)return 0;let n=0;for(const e of s)t.has(e)&&n++;return n/Math.max(s.size,t.size)}class SupportBot{constructor(e={}){this.rootPath=e.rootPath||process.cwd(),this.verbose=e.verbose||!1,this.smart=e.smart||!1,this._thubanVersion=null}async semanticQuery(e){const o=process.env.OPENAI_API_KEY,s=process.env.PINECONE_API_KEY;if(!o||!s){try{const o=await this._httpPost("europe-west2-orion-os-479912.cloudfunctions.net","/thuban-api/support/query",{},{query:e,topK:3});if(o.data&&o.data.results)return o.data.results}catch{}return null}try{const t=await this._httpPost("api.openai.com","/v1/embeddings",{Authorization:`Bearer ${o}`},{model:"text-embedding-3-small",input:e});if(!t.data||!t.data.data)return null;const n=t.data.data[0].embedding,r=await this._httpPost("thuban-kb-es8yjbx.svc.aped-4627-b74a.pinecone.io","/query",{"Api-Key":s},{vector:n,topK:3,includeMetadata:!0});return r.data&&r.data.matches?r.data.matches.map(e=>({score:e.score,type:e.metadata.type,...e.metadata})):null}catch(e){return this.verbose&&console.error(`${C.gray} Semantic search error: ${e.message}${C.reset}`),null}}_httpPost(e,o,s,t){return new Promise((n,r)=>{const a=JSON.stringify(t),l=https.request({hostname:e,path:o,method:"POST",timeout:1e4,headers:{...s,"Content-Type":"application/json","Content-Length":Buffer.byteLength(a)}},e=>{let o="";e.on("data",e=>o+=e),e.on("end",()=>{try{n({status:e.statusCode,data:JSON.parse(o)})}catch{n({status:e.statusCode,data:o})}})});l.on("error",r),l.on("timeout",()=>{l.destroy(),r(new Error("timeout"))}),l.write(a),l.end()})}async diagnose(){const e=[],o=[],s=[],t={},n=safeExec("node --version"),r=parseSemver(n);t.nodeVersion=n||"unknown",r&&r.major>=16?e.push(ok(`Node.js ${n} (requires >= 16)`)):r?s.push(fail(`Node.js ${n} — version 16+ required`)):s.push(fail("Node.js not found"));const a=safeExec("npm --version");t.npmVersion=a||"unknown",a?e.push(ok(`npm v${a}`)):o.push(warn("npm not found — install via Node.js installer"));const l=safeExec("git --version"),i=l?l.replace(/^git version\s*/,""):null;t.gitVersion=i||"unknown",i?e.push(ok(`Git v${i}`)):o.push(warn("Git not found — some features require Git"));const c=this._readThubanVersion();t.thubanVersion=c||"unknown",c?e.push(ok(`Thuban v${c}`)):o.push(warn("Could not determine Thuban version")),this._thubanVersion=c;const u=os.platform(),d=os.arch(),g=os.release();t.os=u,t.arch=d,t.release=g;const p=this._formatOS(u,g,d);e.push(ok(`OS: ${p}`));try{fs.accessSync(this.rootPath,fs.constants.R_OK),e.push(ok("Target path exists and is readable"))}catch{s.push(fail(`Target path not readable: ${this.rootPath}`))}const $=countFiles(this.rootPath);$>=0&&(0===$?o.push(warn("Target directory is empty")):this.verbose&&e.push(ok(`${$} file(s) in target directory`))),fs.existsSync(path.join(this.rootPath,"package.json"))?e.push(ok("package.json found")):s.push(fail("No package.json found in target directory")),this._hasCodeFiles(this.rootPath)||o.push(warn("No .js or .ts files found in target directory"));const h=this._checkDiskSpace();!1===h.ok?o.push(warn(`Low disk space: ${h.free} available`)):h.free&&this.verbose&&e.push(ok(`Disk space: ${h.free} available`)),/\s/.test(this.rootPath)&&o.push(warn("Path contains spaces — may cause issues with some commands"));try{fs.accessSync(this.rootPath,fs.constants.W_OK),this.verbose&&e.push(ok("Write permissions on target directory"))}catch{o.push(warn("No write permissions on target directory"))}const f={passed:e,warnings:o,failed:s,systemInfo:t};return this._printDiagnosticReport(f),f}_printDiagnosticReport(e){console.log(""),console.log(` ${C.bold}THUBAN SYSTEM DIAGNOSTIC${C.reset}`),console.log(` ${C.gray}────────────────────────${C.reset}`),console.log("");for(const o of e.passed)console.log(o);for(const o of e.warnings)console.log(o);for(const o of e.failed)console.log(o);console.log("");const o=[];e.passed.length&&o.push(`${C.green}${e.passed.length} passed${C.reset}`),e.warnings.length&&o.push(`${C.yellow}${e.warnings.length} warning${1!==e.warnings.length?"s":""}${C.reset}`),e.failed.length&&o.push(`${C.red}${e.failed.length} failed${C.reset}`),console.log(` ${o.join(", ")}`),console.log("")}query(e){return this._queryLocal(e)}async queryAsync(e){const o=this._queryLocal(e);if(o.confidence>=.7)return o;if(this.smart){const o=await this.semanticQuery(e);if(o&&o.length>0&&o[0].score>.35){const e=o[0];return{type:e.type||"faq",data:{question:e.question||e.title||e.name||"",answer:e.answer||e.fix||e.description||"",category:e.category||"general",source:"pinecone",score:e.score,alternatives:o.slice(1).map(e=>({type:e.type,title:e.question||e.title||e.name||"",score:e.score}))},confidence:Math.min(e.score+.3,1)}}}return o}_queryLocal(e){if(!e||"string"!=typeof e)return{type:"none",data:null,confidence:0};const o=e.trim().toLowerCase();if(!o)return{type:"none",data:null,confidence:0};if(KB.patterns&&Array.isArray(KB.patterns))for(const e of KB.patterns)if(e.regex&&new RegExp(e.regex,"i").test(o))return{type:e.type||"troubleshooting",data:e,confidence:1};let s=null,t=0;if(KB.faq&&Array.isArray(KB.faq))for(const e of KB.faq){const n=e.keywords||[];let r=0;for(const e of n)if(o===e.toLowerCase()){r=1;break}if(r<1)for(const e of n)(o.includes(e.toLowerCase())||e.toLowerCase().includes(o))&&(r=Math.max(r,.7));r<.7&&e.question&&wordOverlap(o,e.question)>.5&&(r=Math.max(r,.5)),r>t&&(t=r,s=e)}let n=null,r=0;if(KB.errors&&Array.isArray(KB.errors))for(const e of KB.errors){const s=(e.code||"").toLowerCase(),t=(e.message||"").toLowerCase();if(s&&o.includes(s)){n=e,r=1;break}if(t&&o.includes(t)&&(n=e,r=Math.max(r,.7)),e.keywords)for(const s of e.keywords)o.includes(s.toLowerCase())&&(r=Math.max(r,.5),n=e)}let a=null,l=0;if(KB.commands&&Array.isArray(KB.commands))for(const e of KB.commands){const s=(e.name||"").toLowerCase();if(s&&o.includes(s)){a=e,l=o===s?1:.7;break}if(e.aliases)for(const s of e.aliases)o.includes(s.toLowerCase())&&(a=e,l=.7)}const i=[{type:"faq",data:s,confidence:t},{type:"error",data:n,confidence:r},{type:"command",data:a,confidence:l}].filter(e=>null!==e.data);return i.sort((e,o)=>o.confidence-e.confidence),i.length>0&&i[0].confidence>0?i[0]:{type:"none",data:{suggestions:(KB.faq||[]).slice(0,3).map(e=>({question:e.question,keywords:e.keywords}))},confidence:0}}async startInteractive(){const e=this._readThubanVersion()||"0.0.0",o=readline.createInterface({input:process.stdin,output:process.stdout});console.log(""),console.log(` ${C.cyan}╔══════════════════════════════════════╗${C.reset}`),console.log(` ${C.cyan}║${C.reset} ${C.bold}THUBAN Support Assistant${C.reset} ${C.cyan}║${C.reset}`),console.log(` ${C.cyan}║${C.reset} v${e} · Type 'help' or 'exit' ${C.cyan}║${C.reset}`),console.log(` ${C.cyan}╚══════════════════════════════════════╝${C.reset}`),console.log(""),console.log(` ${C.bold}Quick actions:${C.reset}`),console.log(` ${C.cyan}[1]${C.reset} Run system diagnostic`),console.log(` ${C.cyan}[2]${C.reset} Installation help`),console.log(` ${C.cyan}[3]${C.reset} Scan not working`),console.log(` ${C.cyan}[4]${C.reset} Understanding results`),console.log(` ${C.cyan}[5]${C.reset} Monitor/watch mode guide`),console.log(` ${C.cyan}[6]${C.reset} Commands reference`),console.log(` ${C.cyan}[7]${C.reset} Report a bug (thuban feedback)`),console.log("");const s={1:"diagnose",2:"installation",3:"scan-not-working",4:"understanding-results",5:"monitor-mode",6:"commands",7:"report-bug"},t=()=>new Promise(e=>{o.question(` ${C.bold}You:${C.reset} `,o=>{e(o?o.trim():"")})}),n=()=>new Promise(e=>{o.question(`\n ${C.gray}Did this help? (y/n/more):${C.reset} `,o=>{e(o?o.trim().toLowerCase():"")})});let r=!0;for(;r;){const e=await t();if(!e)continue;const o=e.toLowerCase();if("exit"===o||"quit"===o){console.log(`\n ${C.cyan}Goodbye! Run 'thuban support' anytime.${C.reset}\n`),r=!1;break}if("help"===o){this._printHelp();continue}if("diagnose"===o||"1"===o){await this.diagnose();continue}if(s[o]&&"1"!==o){const e=s[o];if("report-bug"===e){console.log(`\n ${C.cyan}To report a bug, run:${C.reset}`),console.log(" thuban feedback 'describe your issue'\n");continue}this._handleSlugQuery(e);const t=await n();this._handleFollowUp(t,e);continue}const a=this.query(e);if(this._printQueryResult(a),a.confidence<.5)console.log(`\n ${C.yellow}I couldn't find an exact match. Try:${C.reset}`),console.log(` • ${C.cyan}thuban support --diagnose${C.reset} (run diagnostics)`),console.log(` • ${C.cyan}thuban feedback 'describe your issue'${C.reset} (report to the team)`);else{const e=await n();this._handleFollowUp(e,null)}}o.close()}runTroubleshootingFlow(e){if(!KB.troubleshooting||!Array.isArray(KB.troubleshooting))return void console.log(`\n ${C.red}No troubleshooting flows available.${C.reset}\n`);const o=KB.troubleshooting.find(o=>o.slug===e);if(!o){console.log(`\n ${C.red}Unknown troubleshooting flow: ${e}${C.reset}`),console.log(" Available flows:");for(const e of KB.troubleshooting)console.log(` • ${C.cyan}${e.slug}${C.reset} — ${e.title||e.slug}`);return void console.log("")}console.log(""),console.log(` ${C.bold}${o.title||o.slug}${C.reset}`),console.log(` ${C.gray}${"─".repeat(40)}${C.reset}`),console.log("");const s=o.steps||[],t=[];for(let e=0;e<s.length;e++){const o=s[e],n=e+1;if(console.log(` ${C.bold}Step ${n}:${C.reset} ${o.description||o.text||""}`),o.command&&this._isAllowedDiagnosticCommand(o.command)){console.log(` ${C.gray}Command: ${o.command}${C.reset}`);const e=safeExec(o.command);null!==e?(console.log(` ${C.green}Result:${C.reset} ${e}`),t.push({step:n,command:o.command,output:e,status:"ok"})):(console.log(` ${C.red}Command failed or timed out${C.reset}`),t.push({step:n,command:o.command,output:null,status:"failed"}))}o.note&&console.log(` ${C.gray}Note: ${o.note}${C.reset}`),o.fix&&console.log(` ${C.cyan}Fix: ${o.fix}${C.reset}`),console.log("")}console.log(` ${C.bold}Summary${C.reset}`),console.log(` ${C.gray}${"─".repeat(40)}${C.reset}`);const n=t.filter(e=>"ok"===e.status).length,r=t.filter(e=>"failed"===e.status).length;0===r&&t.length>0?console.log(` ${C.green}All ${n} diagnostic check(s) passed.${C.reset}`):r>0&&console.log(` ${C.red}${r} check(s) failed.${C.reset} Review the steps above for fixes.`),o.resolution&&console.log(`\n ${C.cyan}Suggested resolution:${C.reset} ${o.resolution}`),console.log("")}_readThubanVersion(){const e=[path.join(__dirname,"package.json"),path.join(__dirname,"..","package.json"),path.join(__dirname,"..","..","package.json")];for(const o of e)try{const e=fs.readFileSync(o,"utf8"),s=JSON.parse(e);if(s.version)return s.version}catch{}return null}_formatOS(e,o,s){return`${{win32:"Windows",darwin:"macOS",linux:"Linux",freebsd:"FreeBSD"}[e]||e} ${o} (${s})`}_hasCodeFiles(e){try{return fs.readdirSync(e).some(e=>/\.(js|ts|jsx|tsx|mjs|cjs)$/.test(e))}catch{return!1}}_checkDiskSpace(){const e="win32"===os.platform();try{if(e){const e=safeExec(`wmic logicaldisk where "DeviceID='${(path.parse(this.rootPath).root||"C:\\").charAt(0)}:'" get FreeSpace /format:value`);if(e){const o=e.match(/FreeSpace=(\d+)/);if(o){const e=parseInt(o[1],10);return{ok:e>536870912,free:`${(e/1073741824).toFixed(1)} GB`}}}}else{const e=safeExec(`df -h "${this.rootPath}" 2>/dev/null`);if(e){const o=e.split("\n");if(o.length>=2){return{ok:!0,free:o[1].split(/\s+/)[3]||"unknown"}}}}}catch{}return{ok:null,free:null}}_isAllowedDiagnosticCommand(e){if(!e||"string"!=typeof e)return!1;const o=e.trim().toLowerCase();return["node --version","node -v","npm --version","npm -v","npm ls","npm list","git --version","git status","git log","df ","df -h","wmic logicaldisk","cat package.json","type package.json","ls ","dir ","uname","whoami","echo "].some(e=>o.startsWith(e.toLowerCase()))}_printHelp(){console.log(""),console.log(` ${C.bold}Available commands:${C.reset}`),console.log(` ${C.cyan}help${C.reset} — Show this help message`),console.log(` ${C.cyan}diagnose${C.reset} — Run full system diagnostic`),console.log(` ${C.cyan}1-7${C.reset} — Quick action shortcuts`),console.log(` ${C.cyan}exit${C.reset} — Exit the support assistant`),console.log(""),console.log(` ${C.bold}Or type any question:${C.reset}`),console.log(' "How do I scan a project?"'),console.log(' "What does error E001 mean?"'),console.log(' "scan command options"'),console.log("")}_handleSlugQuery(e){if(KB.troubleshooting&&Array.isArray(KB.troubleshooting)){const o=KB.troubleshooting.find(o=>o.slug===e);if(o){if(console.log(""),console.log(` ${C.bold}${o.title||o.slug}${C.reset}`),console.log(` ${C.gray}${"─".repeat(40)}${C.reset}`),o.summary&&console.log(`\n ${o.summary}`),o.steps&&o.steps.length>0){console.log("");for(let e=0;e<o.steps.length;e++){const s=o.steps[e];console.log(` ${C.cyan}${e+1}.${C.reset} ${s.description||s.text||""}`),s.fix&&console.log(` ${C.gray}→ ${s.fix}${C.reset}`)}}return void console.log("")}}const o=this.query(e);this._printQueryResult(o)}_printQueryResult(e){if(console.log(""),"none"===e.type){if(e.data&&e.data.suggestions&&e.data.suggestions.length>0){console.log(` ${C.yellow}No exact match found. Did you mean:${C.reset}`);for(const o of e.data.suggestions)console.log(` • ${o.question||o.keywords?.join(", ")||"(untitled)"}`)}else console.log(` ${C.yellow}No results found.${C.reset}`);return void console.log("")}const o=e.data;switch(e.type){case"faq":console.log(` ${C.bold}${o.question||"FAQ"}${C.reset}`),o.answer&&console.log(` ${o.answer}`),o.example&&console.log(`\n ${C.gray}Example:${C.reset} ${o.example}`);break;case"error":console.log(` ${C.bold}${C.red}Error: ${o.code||"Unknown"}${C.reset}`),o.message&&console.log(` ${o.message}`),o.cause&&console.log(`\n ${C.yellow}Cause:${C.reset} ${o.cause}`),o.fix&&console.log(` ${C.green}Fix:${C.reset} ${o.fix}`);break;case"command":if(console.log(` ${C.bold}${C.cyan}${o.name||"Command"}${C.reset}`),o.description&&console.log(` ${o.description}`),o.usage&&console.log(`\n ${C.gray}Usage:${C.reset} ${o.usage}`),o.options&&Array.isArray(o.options)){console.log(`\n ${C.bold}Options:${C.reset}`);for(const e of o.options)console.log(` ${C.cyan}${e.flag||e.name}${C.reset} ${e.description||""}`)}break;case"troubleshooting":if(console.log(` ${C.bold}${o.title||o.slug||"Troubleshooting"}${C.reset}`),o.response&&console.log(` ${o.response}`),o.steps&&Array.isArray(o.steps))for(const e of o.steps)console.log(` • ${e}`);break;default:console.log(` ${C.gray}(${e.type})${C.reset} ${JSON.stringify(o)}`)}e.confidence<1&&e.confidence>=.5&&console.log(`\n ${C.gray}(confidence: ${Math.round(100*e.confidence)}% — results may be approximate)${C.reset}`),console.log("")}_handleFollowUp(e,o){if(e)switch(e){case"y":case"yes":console.log(`\n ${C.green}Great! Anything else?${C.reset}\n`);break;case"n":case"no":console.log(`\n ${C.yellow}Sorry about that. Try describing your issue differently,`),console.log(` or run '${C.cyan}thuban feedback${C.reset}' to report it.${C.reset}\n`);break;case"more":this._showRelatedEntries(o)}}_showRelatedEntries(e){if(!KB.faq||!Array.isArray(KB.faq))return void console.log(`\n ${C.gray}No additional entries available.${C.reset}\n`);const o=KB.faq.filter(o=>o.slug!==e).slice(0,5);if(0!==o.length){console.log(`\n ${C.bold}Related topics:${C.reset}`);for(const e of o)console.log(` • ${e.question||e.keywords?.join(", ")||e.slug||"(untitled)"}`);console.log("")}else console.log(`\n ${C.gray}No additional entries available.${C.reset}\n`)}}module.exports=SupportBot;
|