@vibgrate/cli 2026.702.2 → 2026.703.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/DOCS.md +521 -146
- package/LICENSE +202 -0
- package/NOTICE +40 -0
- package/README.md +232 -250
- package/dist/baseline-ZBXJONQ6.js +6 -0
- package/dist/baseline-ZBXJONQ6.js.map +1 -0
- package/dist/chunk-2PJRXVUW.js +11389 -0
- package/dist/chunk-2PJRXVUW.js.map +1 -0
- package/dist/chunk-4M2MUQRO.js +800 -0
- package/dist/chunk-4M2MUQRO.js.map +1 -0
- package/dist/chunk-FPUNJNXD.js +31 -0
- package/dist/chunk-FPUNJNXD.js.map +1 -0
- package/dist/chunk-J4DFLN2Q.js +6267 -0
- package/dist/chunk-J4DFLN2Q.js.map +1 -0
- package/dist/chunk-RXP66R2E.js +39 -0
- package/dist/chunk-RXP66R2E.js.map +1 -0
- package/dist/chunk-X5YT263H.js +912 -0
- package/dist/chunk-X5YT263H.js.map +1 -0
- package/dist/cli.d.ts +44 -1
- package/dist/cli.js +3816 -165
- package/dist/cli.js.map +1 -0
- package/dist/fs-7KZ4F3SB.js +3 -0
- package/dist/fs-7KZ4F3SB.js.map +1 -0
- package/dist/index.d.ts +1010 -1
- package/dist/index.js +42 -1
- package/dist/index.js.map +1 -0
- package/dist/parse-worker.d.ts +23 -0
- package/dist/parse-worker.js +32 -0
- package/dist/parse-worker.js.map +1 -0
- package/dist/types-C6ZyQpa9.d.ts +168 -0
- package/grammars/tree-sitter-bash.wasm +0 -0
- package/grammars/tree-sitter-c.wasm +0 -0
- package/grammars/tree-sitter-c_sharp.wasm +0 -0
- package/grammars/tree-sitter-cpp.wasm +0 -0
- package/grammars/tree-sitter-dart.wasm +0 -0
- package/grammars/tree-sitter-elixir.wasm +0 -0
- package/grammars/tree-sitter-go.wasm +0 -0
- package/grammars/tree-sitter-java.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-kotlin.wasm +0 -0
- package/grammars/tree-sitter-lua.wasm +0 -0
- package/grammars/tree-sitter-php.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-ruby.wasm +0 -0
- package/grammars/tree-sitter-rust.wasm +0 -0
- package/grammars/tree-sitter-scala.wasm +0 -0
- package/grammars/tree-sitter-swift.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/grammars/tree-sitter-zig.wasm +0 -0
- package/package.json +68 -33
- package/scripts/postinstall.mjs +84 -0
- package/skills/README.md +58 -0
- package/skills/vg/SKILL.md +42 -0
- package/HCS-RUNTIME-SETUP.md +0 -73
- package/LICENSE.md +0 -45
- package/dist/baseline-TJDKYSFL.js +0 -1
- package/dist/chunk-EK7ODJWE.js +0 -1
- package/dist/chunk-GGDB72YE.js +0 -323
- package/dist/chunk-MKDRULJ6.js +0 -1
- package/dist/chunk-NGFYX3CW.js +0 -2
- package/dist/chunk-OQSVJXWJ.js +0 -143
- package/dist/chunk-XTHPCEME.js +0 -2
- package/dist/dist-5D3BKCOR.js +0 -1
- package/dist/fs-PXXYZATK-ZP2RBR47.js +0 -1
- package/dist/hcs-wasm/package.json +0 -3
- package/dist/hcs-wasm/vibgrate_hcs_wasm.js +0 -450
- package/dist/hcs-wasm/vibgrate_hcs_wasm_bg.wasm +0 -0
- package/dist/hcs-worker.js +0 -374406
- package/dist/semver-TPPMM2NV.js +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,166 +1,3817 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
`,"utf8");try{ee.chmodSync(n,384);}catch{}}function Gt(){try{return ee.rmSync(de()),!0}catch{return false}}function fe(t){return t||(process.env.VIBGRATE_DSN?process.env.VIBGRATE_DSN:Io()?.dsn??void 0)}function Oe(t,e){console.log(`VIBGRATE_INGEST_ID=${t}`),e?.unchanged&&console.log("VIBGRATE_INGEST_UNCHANGED=1");}function Wt(t,e){let n=`${t.scheme}://${e}/v1/ingest/scan`;return fetch(n,{method:"POST",headers:{"Content-Type":"application/json","Content-Encoding":t.contentEncoding,"X-Vibgrate-Timestamp":t.timestamp,Authorization:`VibgrateDSN ${t.keyId}:${t.secret}`,Connection:"close"},body:t.body})}async function Ro(t){try{let e=await t.clone().json();return e?.code!=="REGION_MISMATCH"||!e.region?void 0:Lt(e.region)}catch{return}}async function Fe(t){let e=t.host,n=await Wt(t,e);if(n.status===409){let o=await Ro(n);o&&o!==e&&(console.log(l.yellow(`\u21BB Workspace is pinned to a different region \u2014 retrying upload to ${o}...`)),e=o,n=await Wt(t,e));}return {response:n,host:e}}var Eo=[{files:["CLAUDE.md",".claude"],name:"claude"},{files:[".cursor",".cursorrules"],name:"cursor"},{files:[".windsurf"],name:"windsurf"},{files:[".github/copilot-instructions.md",".copilot"],name:"copilot"},{files:[".gemini"],name:"gemini"},{files:[".continuerc.json"],name:"continue"},{files:[".codex"],name:"codex"}];async function Xt(t){for(let e of Eo)for(let n of e.files)if(await p(S.join(t,n)))return e.name;return null}function Kt(t){let e=l.hex("#3FB0A4"),n=l.hex("#4FE3C1"),o=t?`vg install ${t}`:"vg install --all",r=t?l.dim(` (${t} config detected)`):"";console.log(""),console.log(e(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557")),console.log(e(" \u2551")+" "+n("\u25C6")+" "+l.bold.white("Get AI-aware answers in your editor")+" "+e("\u2551")),console.log(e(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D")),console.log(""),console.log(" "+l.bold.white(o)+r),console.log(""),console.log(" "+e("\xB7")+" "+l.white("Code map in your assistant")+l.dim(" \u2014 call trees, impact surfaces, import paths")),console.log(" "+e("\xB7")+" "+l.white("Offline drift")+l.dim(" \u2014 DriftScore and upgrade priorities, right inside your editor")),console.log(" "+e("\xB7")+" "+l.white("Version-correct library docs")+l.dim(" \u2014 pinned to your lockfile, no hallucinated APIs")),console.log("");}async function Do(t,e,n){let o=fe(n.dsn);if(!o){console.error(l.red("No DSN provided for push.")),console.error(l.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.')),n.strict&&process.exit(1);return}let r=fb(o);if(!r){console.error(l.red("Invalid DSN format.")),n.strict&&process.exit(1);return}let{body:a,contentEncoding:s}=await eb(t),i=String(Date.now()),c=r.host;if(n.region)try{c=ne(n.region);}catch(u){console.error(l.red(u instanceof Error?u.message:String(u))),n.strict&&process.exit(1);return}let p=JSON.stringify(t).length,g=a.length,m=((1-g/p)*100).toFixed(0);console.log(l.dim(`Uploading to ${c}... (${(g/1024).toFixed(0)} KB, ${m}% smaller)`));try{let{response:u,host:f}=await Fe({scheme:r.scheme,host:c,keyId:r.keyId,secret:r.secret,body:a,contentEncoding:s,timestamp:i});if(c=f,!u.ok){let $=await u.text();throw new Error(`HTTP ${u.status}: ${$}`)}let v=await u.json();if(v.unchanged){if(console.log(l.green("\u2714")+` Repository unchanged since ${v.lastScannedAt??"last scan"} \u2014 skipped upload (no credit used).`),v.previousIngestId){Oe(v.previousIngestId,{unchanged:!0});let $=`https://${ge(c)}/${r.workspaceId}/scan/${v.previousIngestId}`;console.log(l.dim(` Previous report: ${$}`));}else n.strict&&(console.error(l.red("Repository unchanged but no previous ingest id returned.")),process.exit(1));return}if(console.log(l.green("\u2714")+` Scan queued for processing (${v.ingestId??"ok"})`),v.ingestId){Oe(v.ingestId);let $=`https://${ge(c)}/${r.workspaceId}/scan/${v.ingestId}`,k=process.platform==="win32"?"\x1B[0G\x1B[2K":"";console.log(""),console.log(k+l.dim(" Processing continues in the background. Results available shortly.")),console.log(""),console.log(k+l.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(k+l.bold(" \u{1F4CA} View Scan Report")),console.log(k+" "+l.underline.cyan($)),console.log(k+l.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(""),console.log("");}}catch(u){let f=u instanceof Error?u.message:String(u);console.error(l.red(`Upload failed: ${f}`)),n.strict&&process.exit(1);}}function To(t,e){return [...e,...b$3(t)]}function zt(t,e){if(t===void 0)return;let n=Number(t);if(!Number.isFinite(n)||n<0)throw new Error(`${e} must be a non-negative number.`);return n}var qt=new Command("scan").description("Scan a project for upgrade drift").argument("[path]","Path to scan",".").option("--out <file>","Output file path").option("--format <format>","Output format (text|json|sarif|md)","text").option("--fail-on <level>","Fail on warn or error").option("--baseline <file>","Compare against baseline").option("--changed-only","Only scan changed files").option("-e, --exclude <glob>",'Exclude paths matching a glob pattern. Repeatable, and a single value may list several patterns separated by commas or semicolons (e.g. --exclude "legacy/**,vendor/**"). Merged with excludes from the config file.',To,[]).option("--concurrency <n>","Max concurrent npm calls","8").option("--push","Auto-push results to Vibgrate API after scan").option("--dsn <dsn>","DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>","Override data residency region for push (us, eu)").option("--strict","Fail on push errors").option("--ui-purpose","Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts","Do not write .vibgrate JSON artifacts to disk").option("--max-privacy","Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline","Run without network calls; do not upload results").option("--package-manifest <file>","Use local package-version manifest JSON/ZIP (for offline mode)").option("--project-scan-timeout <seconds>","Per-project scan timeout in seconds (default: 180)").option("--drift-budget <score>","Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>","Fail if drift worsens by more than % since baseline").option("--repository-name <name>","Override the repository name recorded for this scan (defaults to the directory or package.json name)").action(async(t,e)=>{let n=S.resolve(t);await p(n)||(console.error(l.red(`Path does not exist: ${n}`)),process.exit(1));let o=!!fe(e.dsn),r=!e.offline&&(e.push||o),a=!await p(S.join(n,".vibgrate","scan_result.json")),s;if(r&&o){let m=fe(e.dsn),u=fb(m);if(u){let f=e.region?ne(e.region):u.host,v=await Xa(n),$=await ib(n,v),k=e.repositoryName?.trim()||await hb(n);try{let w=await gb(u,f,{repositoryName:k,vcsSha:$.vcsSha});if(s=w.region,w.vm&&!w.vm.allowed&&(console.error(l.red(w.error??"VM meter usage exhausted")),console.error(l.dim(`VM minutes: ${w.vm.used}/${w.vm.limit} (${w.plan.label} plan) \u2014 enable overages or upgrade your plan.`)),process.exit(1)),w.repositories&&!w.repositories.allowed){console.error(l.red(w.error??`Repository limit reached for the ${w.plan.label} plan \u2014 cannot scan a new repository.`));let U=w.repositories.max<0?"unlimited":String(w.repositories.max);console.error(l.dim(`Repositories: ${w.repositories.total}/${U} (${w.plan.label} plan) \u2014 archive a repository or upgrade your plan.`)),w.upgradeUrl&&console.error(l.dim(` Upgrade: ${w.upgradeUrl}`)),process.exit(1);}if((w.status==="error"||!w.scans.allowed)&&(console.error(l.red(w.error??"Scan ingestion not allowed for this workspace.")),console.error(l.dim(`Credits: ${w.scans.used}/${w.scans.limit} (${w.plan.label} plan)`)),e.strict&&process.exit(1),process.exit(1)),console.log(l.dim(`Plan: ${w.plan.label} \u2014 scan credits ${w.scans.used}/${w.scans.limit} this month`)),w.repository?.unchanged){if(console.log(l.green("\u2714")+` Repository unchanged at ${w.repository.lastVcsSha?.slice(0,7)??"same revision"} \u2014 skipping scan.`),w.repository.lastIngestId){Oe(w.repository.lastIngestId,{unchanged:!0});let U=`https://${ge(f)}/${u.workspaceId}/scan/${w.repository.lastIngestId}`;console.log(l.dim(` Latest report: ${U}`));}else e.strict&&(console.error(l.red("Repository unchanged but no previous ingest id available.")),process.exit(1));return}}catch(w){let U=w instanceof Error?w.message:String(w);console.error(l.yellow(`Preflight check failed: ${U}`)),e.strict&&process.exit(1);}}}let i={vibgrateVersion:E,out:e.out,format:e.format||"text",failOn:e.failOn,baseline:e.baseline,changedOnly:e.changedOnly,exclude:e.exclude,concurrency:parseInt(e.concurrency,10)||8,push:e.push,dsn:e.dsn,region:e.region??s,strict:e.strict,uiPurpose:e.uiPurpose,noLocalArtifacts:e.noLocalArtifacts,maxPrivacy:e.maxPrivacy,offline:e.offline,packageManifest:e.packageManifest,driftBudget:zt(e.driftBudget,"--drift-budget"),driftWorseningPercent:zt(e.driftWorsening,"--drift-worsening"),projectScanTimeout:e.projectScanTimeout&&parseInt(e.projectScanTimeout,10)||void 0,repositoryName:e.repositoryName?.trim()||void 0},c=await f(),p$1=await kb(n,i,c);if(e.failOn){let m=p$1.findings.some(f=>f.level==="error"),u=p$1.findings.some(f=>f.level==="warning");e.failOn==="error"&&m&&(console.error(l.red(`
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`)}var nn=new Command("report").description("Generate a drift report from a scan artifact").option("--in <file>","Input artifact file",".vibgrate/scan_result.json").option("--format <format>","Output format (md|text|json)","text").action(async t=>{let e=S.resolve(t.in);await c(e)||(console.error(l.red(`Artifact not found: ${e}`)),console.error(l.dim('Run "vibgrate scan" first to generate a scan artifact.')),process.exit(1));let n=await b$1(e);switch(t.format){case "md":console.log(Zt(n));break;case "json":console.log(JSON.stringify(n,null,2));break;default:console.log(Qt(n));break}});function on(t){let e=process.platform,n,o;e==="darwin"?(n="open",o=[t]):e==="win32"?(n="cmd",o=["/c","start","",t]):(n="xdg-open",o=[t]);try{let r=spawn(n,o,{stdio:"ignore",detached:!0});return r.on("error",()=>{}),r.unref(),!0}catch{return false}}var Uo=t=>new Promise(e=>setTimeout(e,t)),rn=new Command("login").description("Authenticate the CLI with your Vibgrate workspace via the browser").option("--ingest <url>","Ingest API URL (overrides --region)").option("--region <region>",`Data residency region (${ae().join(", ")})`,"us").option("--no-browser","Do not attempt to open a browser automatically").action(async t=>{let e;try{e=ne(t.region,t.ingest);}catch(s){console.error(l.red(s instanceof Error?s.message:String(s))),process.exit(1);}let n=`https://${e}/v1/auth/device`,o;try{let s=await fetch(`${n}/start`,{method:"POST",headers:{"Content-Type":"application/json",Connection:"close"},body:"{}"});s.ok||(console.error(l.red(`Failed to start login (HTTP ${s.status}).`)),process.exit(1)),o=await s.json();}catch(s){console.error(l.red(`Could not reach ${e}: ${s instanceof Error?s.message:String(s)}`)),process.exit(1);}console.log(""),console.log("To finish signing in, open this URL and approve the request:"),console.log(""),console.log(" "+l.cyan(o.verificationUri)),console.log(""),console.log(" Your code: "+l.bold(o.userCode)),console.log(""),t.browser&&on(o.verificationUriComplete)&&console.log(l.dim("Opening your browser\u2026 (if it does not open, use the URL above)")),console.log(l.dim("Waiting for approval\u2026"));let r=Math.max(2,o.interval||5)*1e3,a=Date.now()+(o.expiresIn||900)*1e3;for(;Date.now()<a;){await Uo(r);let s;try{s=await(await fetch(`${n}/token`,{method:"POST",headers:{"Content-Type":"application/json",Connection:"close"},body:JSON.stringify({deviceCode:o.deviceCode})})).json();}catch{continue}if(s.status!=="authorization_pending"){if(s.status==="complete"&&s.dsn){Ht({dsn:s.dsn,workspaceId:s.workspaceId,keyId:s.keyId,ingestHost:s.ingestHost??e,savedAt:new Date().toISOString()}),console.log(""),console.log(l.green("\u2714")+" Logged in."),s.workspaceId&&console.log(" Workspace: "+l.bold(s.workspaceId)),console.log(l.dim(` Credentials saved to ${de()}`)),console.log(l.dim(' You can now run "vibgrate scan --push".'));return}s.status==="access_denied"&&(console.error(l.red("\u2716 Login was denied in the browser.")),process.exit(1)),(s.status==="expired"||s.status==="invalid")&&(console.error(l.red('\u2716 Login request expired. Run "vibgrate login" again.')),process.exit(1)),s.status==="error"&&(console.error(l.red(`\u2716 ${s.error??"Login failed."}`)),process.exit(1));}}console.error(l.red('\u2716 Timed out waiting for approval. Run "vibgrate login" again.')),process.exit(1);});var an=new Command("logout").description("Clear stored Vibgrate login credentials").action(()=>{let t=Gt();console.log(t?l.green("\u2714")+" Logged out. Stored credentials removed.":l.dim(`No stored credentials found at ${de()}.`));});var Ho=[{category:"pricing",pattern:/price|pricing|billing|subscri|trial|credit|plan|tier|upgrade|premium|pro|enterprise/i},{category:"auth",pattern:/sign[- ]?in|sign[- ]?up|log[- ]?in|log[- ]?out|auth|sso|oauth|password|register|invite|onboard/i},{category:"dashboard",pattern:/dashboard|overview|home|main|summary|stats/i},{category:"settings",pattern:/setting|config|preference|option|profile|account/i},{category:"users",pattern:/user|member|team|role|permission|access|admin|owner/i},{category:"integrations",pattern:/integrat|connect|webhook|api[- ]?key|sync|import|export/i},{category:"reports",pattern:/report|analy|metric|chart|graph|insight|track/i},{category:"workflows",pattern:/workflow|automat|schedule|trigger|action|job|task|pipeline/i},{category:"projects",pattern:/project|workspace|organization|folder|repo/i},{category:"navigation",pattern:/menu|nav|sidebar|header|footer|breadcrumb/i}];function cn(t,e=3){let n=t.topEvidence,o=n.filter(g=>g.kind==="dependency").map(g=>g.value).slice(0,10),r=Xo(n.filter(g=>g.kind==="route").map(g=>g.value)).slice(0,15),a=n.filter(g=>g.kind!=="dependency"&&g.kind!=="route"&&g.kind!=="feature_flag"),s=new Map,i={};for(let g of a){let m=Go(g.value);s.has(m)||s.set(m,new Set);let u=Wo(g.value);u.length>=3&&s.get(m).add(u);}let c=[];for(let[g,m]of s){let u=Jo([...m]);i[g]=u.length;for(let f of u.slice(0,e))c.push({kind:"text",value:f,category:g});}let p=n.filter(g=>g.kind==="feature_flag");return p.length>0&&(i.feature_flags=p.length,c.push({kind:"feature_flag",value:"feature flags detected",category:"feature_flags"})),{samples:c,categoryCounts:i,originalCount:n.length,dependencies:o,routes:r,detectedFrameworks:t.detectedFrameworks}}function Go(t){for(let{category:e,pattern:n}of Ho)if(n.test(t))return e;return "general"}function Wo(t){return t.toLowerCase().replace(/[^a-z0-9\s-]/g," ").replace(/\s+/g," ").trim().slice(0,60)}function Jo(t){let e=t.sort((o,r)=>r.length-o.length),n=[];for(let o of e)n.some(a=>{let s=o.slice(0,6);return a.startsWith(s)||a.includes(o)||o.includes(a)})||n.push(o);return n}function Xo(t){let e=new Set,n=[];for(let o of t){let r=o.replace(/:[a-z_]+/gi,":param").replace(/\[\[*\.*\.*[a-z_]+\]*\]/gi,":param").replace(/\/+$/,"").toLowerCase();e.has(r)||(e.add(r),n.push(o));}return n}var zo=promisify(ln.gzip),J=50;function Ie(t){let e=t.match(/^(.+?)\s*\(/);return e?e[1].trim():t.trim()}function qo(t){return {databaseTechnologies:t.databaseTechnologies.slice(0,10),connectionStrings:[],connectionPoolSettings:t.connectionPoolSettings.slice(0,J),replicationSettings:t.replicationSettings.slice(0,20),readReplicaSettings:t.readReplicaSettings.slice(0,20),failoverSettings:t.failoverSettings.slice(0,20),collationAndEncoding:t.collationAndEncoding.slice(0,20),queryTimeoutDefaults:t.queryTimeoutDefaults.slice(0,20),manualIndexes:t.manualIndexes.map(Ie).slice(0,J),tables:t.tables.map(Ie).slice(0,J),views:t.views.map(Ie).slice(0,J),storedProcedures:t.storedProcedures.map(Ie).slice(0,J),triggers:t.triggers.map(Ie).slice(0,J),rowLevelSecurityPolicies:t.rowLevelSecurityPolicies.slice(0,20),otherServices:t.otherServices.slice(0,20)}}function Yo(t){let e=new Set;return {integrations:t.integrations.filter(o=>{let r=o.provider.split(":")[0];return e.has(r)?false:(e.add(r),true)}).slice(0,J).map(o=>({provider:o.provider,endpoint:"",version:o.version,parameters:[],configOptions:[],authHints:[],files:[]})),openApiSpecifications:t.openApiSpecifications.slice(0,10),webhookUrls:t.webhookUrls.slice(0,20),callbackEndpoints:t.callbackEndpoints.slice(0,20),apiVersionPins:t.apiVersionPins.slice(0,20),tokenExpirationPolicies:t.tokenExpirationPolicies.slice(0,20),rateLimitOverrides:t.rateLimitOverrides.slice(0,20),customHeaders:t.customHeaders.slice(0,20),corsPolicies:t.corsPolicies.slice(0,20),oauthScopes:t.oauthScopes.slice(0,20),apiTokens:[]}}function Qo(t){return {faviconFiles:t.faviconFiles.slice(0,1),productLogos:[]}}function Zo(t){let e={...t};if(e.extended){let n={...e.extended};if(n.dataStores&&(n.dataStores=qo(n.dataStores)),n.apiSurface&&(n.apiSurface=Yo(n.apiSurface)),n.assetBranding&&(n.assetBranding=Qo(n.assetBranding)),n.uiPurpose){let o=cn(n.uiPurpose);n.uiPurpose={enabled:n.uiPurpose.enabled,detectedFrameworks:o.detectedFrameworks,evidenceCount:o.originalCount,capped:n.uiPurpose.capped,topEvidence:[],unknownSignals:[],compacted:o};}if(n.runtimeConfiguration&&(n.runtimeConfiguration={...n.runtimeConfiguration,environmentVariables:n.runtimeConfiguration.environmentVariables.slice(0,100),hiddenConfigFiles:n.runtimeConfiguration.hiddenConfigFiles.slice(0,J),startupArguments:n.runtimeConfiguration.startupArguments.slice(0,100)}),n.operationalResilience){let o=n.operationalResilience;n.operationalResilience={implicitTimeouts:o.implicitTimeouts.slice(0,30),defaultPaginationSize:o.defaultPaginationSize.slice(0,30),implicitRetryLogic:o.implicitRetryLogic.slice(0,30),defaultLocale:o.defaultLocale.slice(0,20),defaultCurrency:o.defaultCurrency.slice(0,20),implicitTimezone:o.implicitTimezone.slice(0,20),defaultCharacterEncoding:o.defaultCharacterEncoding.slice(0,20),sessionStores:o.sessionStores.slice(0,20),distributedLocks:o.distributedLocks.slice(0,20),jobSchedulers:o.jobSchedulers.slice(0,30),idempotencyKeys:o.idempotencyKeys.slice(0,20),rateLimitingCounters:o.rateLimitingCounters.slice(0,20),circuitBreakerState:o.circuitBreakerState.slice(0,20),abTestToggles:o.abTestToggles.slice(0,20),regionalEnablementRules:o.regionalEnablementRules.slice(0,20),betaAccessGroups:o.betaAccessGroups.slice(0,20),licensingEnforcementLogic:o.licensingEnforcementLogic.slice(0,20),killSwitches:o.killSwitches.slice(0,20),connectorRetryLogic:o.connectorRetryLogic.slice(0,20),apiPollingIntervals:o.apiPollingIntervals.slice(0,20),fieldMappings:o.fieldMappings.slice(0,20),schemaRegistryRules:o.schemaRegistryRules.slice(0,20),deadLetterQueueBehavior:o.deadLetterQueueBehavior.slice(0,20),dataMaskingRules:o.dataMaskingRules.slice(0,20),transformationLogic:o.transformationLogic.slice(0,20),timezoneHandling:o.timezoneHandling.slice(0,20),encryptionSettings:o.encryptionSettings.slice(0,30),hardcodedSecretSignals:o.hardcodedSecretSignals.slice(0,20)};}n.dependencyGraph&&(n.dependencyGraph={...n.dependencyGraph,phantomDependencies:n.dependencyGraph.phantomDependencies.slice(0,J),phantomDependencyDetails:n.dependencyGraph.phantomDependencyDetails?.slice(0,J),duplicatedPackages:n.dependencyGraph.duplicatedPackages.slice(0,J)}),e.extended=n;}return e}async function er(t){let e=JSON.stringify(t);return zo(e,{level:9})}async function dn(t){let e=Zo(t);return {body:await er(e),contentEncoding:"gzip"}}function Le(t){let n=t.replace(/[\x00-\x1F\x7F\uFEFF\u200B-\u200D\u2060]/g,"").trim().match(/^vibgrate\+(https?):?\/\/([^:]+):([^@]+)@([^/]+)\/(.+)$/);return n?{scheme:n[1],keyId:n[2],secret:n[3],host:n[4],workspaceId:n[5]}:null}function gn(t,e){return Ne.createHmac("sha256",e).update(t).digest("base64")}var fn=new Command("push").description("Push scan results to Vibgrate API").option("--dsn <dsn>","DSN token (or use VIBGRATE_DSN env)").option("--region <region>",`Override data residency region (${ae().join(", ")})`).option("--file <file>","Scan artifact file",".vibgrate/scan_result.json").option("--strict","Fail on upload errors").action(async t=>{let e=fe(t.dsn);if(!e){console.error(l.red("No DSN provided.")),console.error(l.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.')),t.strict&&process.exit(1);return}let n=Le(e);if(!n){console.error(l.red("Invalid DSN format.")),console.error(l.dim("Expected: vibgrate+https://<key_id>:<secret>@<host>/<workspace_id>")),t.strict&&process.exit(1);return}let o=S.resolve(t.file);if(!await c(o)){console.error(l.red(`Scan artifact not found: ${o}`)),console.error(l.dim('Run "vibgrate scan" first.')),t.strict&&process.exit(1);return}let r=await b$1(o),{body:a,contentEncoding:s}=await dn(r),i=String(Date.now()),c$1=n.host;if(t.region)try{c$1=ne(t.region);}catch(u){console.error(l.red(u instanceof Error?u.message:String(u))),t.strict&&process.exit(1);return}let p=JSON.stringify(r).length,g=a.length,m=((1-g/p)*100).toFixed(0);console.log(l.dim(`Uploading to ${c$1}... (${(g/1024).toFixed(0)} KB, ${m}% smaller)`));try{let{response:u,host:f}=await Fe({scheme:n.scheme,host:c$1,keyId:n.keyId,secret:n.secret,body:a,contentEncoding:s,timestamp:i});if(c$1=f,!u.ok){let $=await u.text();throw new Error(`HTTP ${u.status}: ${$}`)}let v=await u.json();if(console.log(l.green("\u2714")+` Scan queued for processing (${v.ingestId??"ok"})`),console.log(),console.log(l.dim("Processing continues in the background. Results available shortly.")),console.log(),v.ingestId){let k=`https://${ge(c$1)}/${n.workspaceId}/scan/${v.ingestId}`;console.log(l.dim("View report: ")+l.underline(k));}}catch(u){let f=u instanceof Error?u.message:String(u);console.error(l.red(`Upload failed: ${f}`)),t.strict&&process.exit(1);}});var je=b$2(a(),1);var hn="https://registry.npmjs.org/@vibgrate%2fcli/latest",bn=S.join(Ut.homedir(),".vibgrate"),yn=S.join(bn,"update-check.json"),nr=720*60*1e3;async function wn(){try{let t=await or();if(t&&Date.now()-t.checkedAt<nr)return {current:E,latest:t.latest,updateAvailable:je.default.gt(t.latest,E)};let e=new AbortController,n=setTimeout(()=>e.abort(),5e3);n.unref?.();let o;try{o=await fetch(hn,{headers:{Accept:"application/json"},signal:e.signal});}finally{clearTimeout(n);}if(!o.ok)return null;let a=(await o.json()).version;return !a||!je.default.valid(a)?null:(await $n({latest:a,checkedAt:Date.now()}),{current:E,latest:a,updateAvailable:je.default.gt(a,E)})}catch{return null}}async function vn(){try{let t=new AbortController,e=setTimeout(()=>t.abort(),1e4);e.unref?.();let n;try{n=await fetch(hn,{headers:{Accept:"application/json"},signal:t.signal});}finally{clearTimeout(e);}if(!n.ok)return null;let r=(await n.json()).version;return !r||!je.default.valid(r)?null:(await $n({latest:r,checkedAt:Date.now()}),r)}catch{return null}}async function or(){try{let t=await K.readFile(yn,"utf-8"),e=JSON.parse(t);return e.latest&&typeof e.checkedAt=="number"?e:null}catch{return null}}async function $n(t){try{await K.mkdir(bn,{recursive:!0}),await K.writeFile(yn,JSON.stringify(t),"utf-8");}catch{}}function ir(){let t=process.argv[1]||"";return (t.includes("/lib/node_modules/")||t.includes("\\node_modules\\"))&&!t.includes(process.cwd())?t.includes("pnpm")?"pnpm":t.includes("yarn")?"yarn":t.includes("bun")?"bun":"npm":null}function ar(t,e,n){let o=`${e}@${n}`;switch(t){case "pnpm":return `pnpm add -g ${o}`;case "yarn":return `yarn global add ${o}`;case "bun":return `bun add -g ${o}`;default:return `npm install -g ${o}`}}async function cr(t){return await c(S.join(t,"pnpm-lock.yaml"))?"pnpm":await c(S.join(t,"bun.lockb"))?"bun":await c(S.join(t,"yarn.lock"))?"yarn":"npm"}function lr(t,e,n,o){let r=`${e}@${n}`;switch(t){case "pnpm":return o?`pnpm add -D ${r}`:`pnpm add ${r}`;case "yarn":return o?`yarn add --dev ${r}`:`yarn add ${r}`;case "bun":return o?`bun add -d ${r}`:`bun add ${r}`;default:return o?`npm install --save-dev ${r}`:`npm install ${r}`}}async function dr(t){try{let e=S.join(t,"package.json"),n=await(await import('fs/promises')).readFile(e,"utf-8");return !!JSON.parse(n).devDependencies?.["@vibgrate/cli"]}catch{return true}}var Sn=new Command("update").description("Update vibgrate to the latest version").option("--check","Only check for updates, do not install").option("--pm <manager>","Package manager to use (npm, pnpm, yarn, bun)").option("--global","Update global installation").action(async t=>{console.log(l.dim(`Current version: ${E}`)),console.log(l.dim("Checking npm registry..."));let e=await vn();if(e||(console.error(l.red("Could not reach the npm registry. Check your network connection.")),process.exit(1)),!(await import('./semver-TPPMM2NV.js')).gt(e,E)){console.log(l.green("\u2714")+` You are on the latest version (${E}).`);return}if(console.log(l.yellow(`Update available: ${E} \u2192 ${e}`)),t.check){console.log(l.dim('Run "vibgrate update" to install.'));return}let o=process.cwd(),r=ir(),a=t.global||r!==null,s=t.pm||(r??await cr(o)),i;if(a)i=ar(s,"@vibgrate/cli",e),console.log(l.dim(`Updating global installation with ${s}: ${i}`));else {let c=await dr(o);i=lr(s,"@vibgrate/cli",e,c),console.log(l.dim(`Using ${s}: ${i}`));}try{execSync(i,{cwd:o,stdio:"inherit"}),console.log(l.green("\u2714")+` Updated to @vibgrate/cli@${e}`);}catch{console.error(l.red(`Update failed. Run manually: ${i}`)),process.exit(1);}});var gr="https://openvex.dev/ns/v0.2.0",kn=["not_affected","affected","fixed","under_investigation"],xn=["component_not_present","vulnerable_code_not_present","vulnerable_code_not_in_execute_path","vulnerable_code_cannot_be_controlled_by_adversary","inline_mitigations_already_exist"],X=class extends Error{};function fr(t){let e=t.statements.map((n,o)=>{if(!n.vulnerability||typeof n.vulnerability!="string")throw new X(`statement[${o}]: "vulnerability" is required`);if(!kn.includes(n.status))throw new X(`statement[${o}] (${n.vulnerability}): invalid status "${n.status}". Expected one of: ${kn.join(", ")}`);let r=n.products&&n.products.length>0?n.products:t.defaultProduct?[t.defaultProduct]:[];if(r.length===0)throw new X(`statement[${o}] (${n.vulnerability}): no product. Set "products" on the statement or pass --product.`);if(n.status==="not_affected"&&!n.justification&&!n.impact_statement)throw new X(`statement[${o}] (${n.vulnerability}): "not_affected" requires a justification or impact_statement.`);if(n.justification&&!xn.includes(n.justification))throw new X(`statement[${o}] (${n.vulnerability}): invalid justification "${n.justification}". Expected one of: ${xn.join(", ")}`);if(n.status==="affected"&&!n.action_statement)throw new X(`statement[${o}] (${n.vulnerability}): "affected" requires an action_statement.`);let a={vulnerability:{name:n.vulnerability},products:r.map(s=>({"@id":s})),status:n.status};return n.justification&&(a.justification=n.justification),n.impact_statement&&(a.impact_statement=n.impact_statement),n.action_statement&&(a.action_statement=n.action_statement),a});return {"@context":gr,"@id":t.id??`https://vibgrate.com/vex/${randomUUID()}`,author:t.author,timestamp:t.timestamp??new Date().toISOString(),version:1,statements:e}}async function mr(t){let e=await b$1(t),n=Array.isArray(e)?e:e&&typeof e=="object"&&Array.isArray(e.statements)?e.statements:null;if(!n)throw new X(`${t}: expected a JSON array of statements or an object with a "statements" array.`);return n}function hr(t,e){let n;try{n=JSON.parse(t);}catch{throw new X(`--statement must be a JSON object: ${t.slice(0,60)}\u2026`)}return [...e,n]}var Pn=new Command("vex").description("Generate an OpenVEX document (exploitability statements) for attestation").option("--from <file>",'Read statements from a JSON file (array, or an object with a "statements" array)').option("--statement <json>",`Add a statement as inline JSON, e.g. '{"vulnerability":"CVE-2024-1","status":"not_affected","justification":"vulnerable_code_not_present"}'. Repeatable.`,hr,[]).option("--product <ref>","Default product reference applied to statements without their own (e.g. an image digest)").option("--author <name>","Document author","Vibgrate").option("--timestamp <iso>","Override the document timestamp (ISO 8601) for reproducible output").option("--id <uri>","Override the document @id (otherwise a uuid URN is generated)").option("--out <file>","Write the document to a file (default: stdout)").action(async t=>{try{let e$1=[];if(t.from){let r=S.resolve(t.from);await c(r)||(process.stderr.write(l.red(`Statements file not found: ${r}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`
|
|
51
|
-
`
|
|
52
|
-
`
|
|
53
|
-
`
|
|
54
|
-
`))
|
|
55
|
-
`)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
`)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
`));}});var Bs="https://vibgrate.com/help";function Us(){console.log(""),console.log(l.dim(`See ${Bs} for more guidance`));}var xt={scan:()=>{console.log(""),console.log(l.bold.underline("vibgrate scan")+l.dim(" \u2014 Scan a project for upgrade drift")),console.log(""),console.log(l.bold("Usage:")),console.log(" vibgrate scan [path] [options]"),console.log(""),console.log(l.bold("Arguments:")),console.log(` ${l.cyan("[path]")} Path to scan (default: current directory)`),console.log(""),console.log(l.bold("Output options:")),console.log(` ${l.cyan("--format <format>")} Output format: ${l.white("text")} | json | sarif | md (default: text)`),console.log(` ${l.cyan("--out <file>")} Write output to a file instead of stdout`),console.log(""),console.log(l.bold("Baseline & gating:")),console.log(` ${l.cyan("--baseline <file>")} Compare results against a saved baseline`),console.log(` ${l.cyan("--drift-budget <score>")} Fail if drift score exceeds this value (0\u2013100)`),console.log(` ${l.cyan("--drift-worsening <percent>")} Fail if drift worsens by more than % since baseline`),console.log(` ${l.cyan("--fail-on <level>")} Fail exit code on warn or error findings`),console.log(""),console.log(l.bold("Performance:")),console.log(` ${l.cyan("--concurrency <n>")} Max concurrent registry calls (default: 8)`),console.log(` ${l.cyan("--changed-only")} Only scan files changed since last git commit`),console.log(""),console.log(l.bold("Privacy & offline:")),console.log(` ${l.cyan("--offline")} Run without any network calls; skip result upload`),console.log(` ${l.cyan("--package-manifest <file>")} Use a local package-version manifest (JSON or ZIP) for offline mode`),console.log(` ${l.cyan("--no-local-artifacts")} Do not write .vibgrate JSON artifacts to disk`),console.log(` ${l.cyan("--max-privacy")} Strongest privacy mode: minimal scanners + no local artifacts`),console.log(""),console.log(l.bold("Tooling:")),console.log(` ${l.cyan("--ui-purpose")} Enable UI purpose evidence extraction (slower)`),console.log(""),console.log(l.bold("Uploading results:")),console.log(` ${l.cyan("--push")} Auto-push results to Vibgrate API after scan`),console.log(` ${l.cyan("--dsn <dsn>")} DSN token for push (or set VIBGRATE_DSN env var)`),console.log(` ${l.cyan("--region <region>")} Data residency region: us | eu (default: us)`),console.log(` ${l.cyan("--strict")} Fail if the upload to Vibgrate API fails`),console.log(""),console.log(l.bold("Examples:")),console.log(` ${l.dim("# Scan the current directory and display a text report")}`),console.log(" vibgrate scan"),console.log(""),console.log(` ${l.dim("# Scan, fail if drift score > 40, and write SARIF for GitHub Actions")}`),console.log(" vibgrate scan --drift-budget 40 --format sarif --out drift.sarif"),console.log(""),console.log(` ${l.dim("# Scan and automatically upload results via a DSN")}`),console.log(" vibgrate scan --push --dsn $VIBGRATE_DSN"),console.log(""),console.log(` ${l.dim("# Offline scan using a pre-downloaded package manifest")}`),console.log(" vibgrate scan --offline --package-manifest ./manifest.zip");},init:()=>{console.log(""),console.log(l.bold.underline("vibgrate init")+l.dim(" \u2014 Initialise vibgrate in a project directory")),console.log(""),console.log(l.bold("Usage:")),console.log(" vibgrate init [path] [options]"),console.log(""),console.log(l.bold("Arguments:")),console.log(` ${l.cyan("[path]")} Directory to initialise (default: current directory)`),console.log(""),console.log(l.bold("Options:")),console.log(` ${l.cyan("--baseline")} Create an initial drift baseline after init`),console.log(` ${l.cyan("--yes")} Skip all confirmation prompts`),console.log(""),console.log(l.bold("What it does:")),console.log(" \u2022 Creates a .vibgrate/ directory"),console.log(" \u2022 Writes a vibgrate.config.ts starter config"),console.log(" \u2022 Optionally runs an initial baseline scan (--baseline)"),console.log(""),console.log(l.bold("Examples:")),console.log(" vibgrate init"),console.log(" vibgrate init ./my-project --baseline");},baseline:()=>{console.log(""),console.log(l.bold.underline("vibgrate baseline")+l.dim(" \u2014 Save a drift baseline snapshot")),console.log(""),console.log(l.bold("Usage:")),console.log(" vibgrate baseline [path]"),console.log(""),console.log(l.bold("Arguments:")),console.log(` ${l.cyan("[path]")} Path to baseline (default: current directory)`),console.log(""),console.log(l.bold("What it does:")),console.log(" Runs a full scan and saves the result as .vibgrate/baseline.json."),console.log(" Future scans can compare against this file using --baseline."),console.log(""),console.log(l.bold("Examples:")),console.log(" vibgrate baseline"),console.log(" vibgrate scan --baseline .vibgrate/baseline.json --drift-worsening 10");},report:()=>{console.log(""),console.log(l.bold.underline("vibgrate report")+l.dim(" \u2014 Generate a report from a saved scan artifact")),console.log(""),console.log(l.bold("Usage:")),console.log(" vibgrate report [options]"),console.log(""),console.log(l.bold("Options:")),console.log(` ${l.cyan("--in <file>")} Input artifact file (default: .vibgrate/scan_result.json)`),console.log(` ${l.cyan("--format <format>")} Output format: ${l.white("text")} | md | json (default: text)`),console.log(""),console.log(l.bold("Examples:")),console.log(" vibgrate report"),console.log(" vibgrate report --format md > DRIFT-REPORT.md"),console.log(" vibgrate report --in ./ci/scan_result.json --format json");},sbom:()=>{console.log(""),console.log(l.bold.underline("vibgrate sbom")+l.dim(" \u2014 Supply-chain evidence: SBOM export/delta and OpenVEX")),console.log(""),console.log(l.bold("Subcommands:")),console.log(` ${l.cyan("vibgrate sbom export")} Emit a CycloneDX or SPDX SBOM from a scan artifact`),console.log(` ${l.cyan("vibgrate sbom delta")} Compare dependencies between two scan artifacts`),console.log(` ${l.cyan("vibgrate sbom vex")} Emit an OpenVEX document (exploitability statements)`),console.log(""),console.log(l.bold("sbom export options:")),console.log(` ${l.cyan("--in <file>")} Input artifact (default: .vibgrate/scan_result.json)`),console.log(` ${l.cyan("--format <format>")} SBOM format: ${l.white("cyclonedx")} | spdx (default: cyclonedx)`),console.log(` ${l.cyan("--out <file>")} Write SBOM to file instead of stdout`),console.log(""),console.log(l.bold("sbom vex options:")),console.log(` ${l.cyan("--from <file>")} Read OpenVEX statements from a JSON file`),console.log(` ${l.cyan("--statement <json>")} Add a statement as inline JSON (repeatable)`),console.log(` ${l.cyan("--product <ref>")} Default product for statements (e.g. an image digest)`),console.log(` ${l.cyan("--out <file>")} Write the document to file instead of stdout`),console.log(""),console.log(l.bold("Examples:")),console.log(" vibgrate sbom export --format cyclonedx --out sbom.json"),console.log(" vibgrate sbom export --format spdx --out sbom.spdx.json"),console.log(' vibgrate sbom vex --product "$IMAGE_REF" --out openvex.json');},push:()=>{console.log(""),console.log(l.bold.underline("vibgrate push")+l.dim(" \u2014 Upload a scan artifact to the Vibgrate API")),console.log(""),console.log(l.bold("Usage:")),console.log(" vibgrate push [options]"),console.log(""),console.log(l.bold("Options:")),console.log(` ${l.cyan("--dsn <dsn>")} DSN token (or set VIBGRATE_DSN env var)`),console.log(` ${l.cyan("--file <file>")} Artifact to upload (default: .vibgrate/scan_result.json)`),console.log(` ${l.cyan("--region <region>")} Override data residency region: us | eu`),console.log(` ${l.cyan("--strict")} Fail with non-zero exit code on upload error`),console.log(""),console.log(l.bold("Examples:")),console.log(" vibgrate push --dsn $VIBGRATE_DSN"),console.log(" vibgrate push --file ./ci/scan_result.json --strict");},dsn:()=>{console.log(""),console.log(l.bold.underline("vibgrate dsn")+l.dim(" \u2014 Manage DSN tokens for API authentication")),console.log(""),console.log(l.bold("Subcommands:")),console.log(` ${l.cyan("vibgrate dsn create")} Generate a new DSN token`),console.log(""),console.log(l.bold("dsn create options:")),console.log(` ${l.cyan("--workspace <id>")} Workspace ID or "new" to auto-generate ${l.red("(required)")}`),console.log(` ${l.cyan("--region <region>")} Data residency region: us | eu (default: us)`),console.log(` ${l.cyan("--ingest <url>")} Override ingest API URL`),console.log(` ${l.cyan("--write <path>")} Write the DSN to a file (add to .gitignore!)`),console.log(""),console.log(l.bold("Examples:")),console.log(" vibgrate dsn create --workspace new"),console.log(" vibgrate dsn create --workspace abc123"),console.log(" vibgrate dsn create --workspace new --region eu --write .vibgrate/.dsn");},update:()=>{console.log(""),console.log(l.bold.underline("vibgrate update")+l.dim(" \u2014 Update the vibgrate CLI to the latest version")),console.log(""),console.log(l.bold("Usage:")),console.log(" vibgrate update [options]"),console.log(""),console.log(l.bold("Options:")),console.log(` ${l.cyan("--check")} Check for a newer version without installing`),console.log(` ${l.cyan("--pm <manager>")} Force a package manager: npm | pnpm | yarn | bun`),console.log(""),console.log(l.bold("Examples:")),console.log(" vibgrate update"),console.log(" vibgrate update --check"),console.log(" vibgrate update --pm pnpm");},hcs:()=>{console.log(""),console.log(l.bold.underline("vibgrate hcs")+l.dim(" \u2014 Holistic Code Specification: extract and render code facts")),console.log(""),console.log(l.bold("Subcommands:")),console.log(` ${l.cyan("vibgrate hcs extract")} Analyze source code and emit HCS facts as NDJSON`),console.log(` ${l.cyan("vibgrate hcs digest")} Render HCS NDJSON facts into Markdown, JSON, or HTML`),console.log(""),console.log(l.bold("hcs extract options:")),console.log(` ${l.cyan("[path]")} Path to source directory (default: .)`),console.log(` ${l.cyan("-o, --out <file>")} Write NDJSON to file (default: stdout)`),console.log(` ${l.cyan("--language <langs>")} Comma-separated languages to analyze (default: auto-detect)`),console.log(` ${l.cyan("--include-tests")} Include test files in analysis`),console.log(` ${l.cyan("--push")} Stream facts to Vibgrate Cloud API`),console.log(` ${l.cyan("--dsn <dsn>")} DSN token for push (or use VIBGRATE_DSN env)`),console.log(` ${l.cyan("--concurrency <n>")} Number of parallel workers`),console.log(` ${l.cyan("--timeout-mins <mins>")} Total analysis timeout in minutes (default: 60)`),console.log(` ${l.cyan("--verbose")} Print worker stderr and summary statistics`),console.log(""),console.log(l.bold("hcs digest options:")),console.log(` ${l.cyan("--in <file>")} HCS NDJSON file to read (default: stdin)`),console.log(` ${l.cyan("-o, --out <file>")} Write output to file (default: stdout)`),console.log(` ${l.cyan("--format <format>")} Output format: ${l.white("md")} | json | html (default: md)`),console.log(` ${l.cyan("--level <level>")} Detail level: ${l.white("concise")} | full (default: concise)`),console.log(` ${l.cyan("--title <title>")} Override document title`),console.log(""),console.log(l.bold("Examples:")),console.log(` ${l.dim("# Extract HCS facts from current directory")}`),console.log(" vibgrate hcs extract . --out facts.ndjson"),console.log(""),console.log(` ${l.dim("# Render facts as Markdown")}`),console.log(" vibgrate hcs digest --in facts.ndjson --format md > DIGEST.md"),console.log(""),console.log(` ${l.dim("# Full pipeline: extract and pipe directly to digest")}`),console.log(" vibgrate hcs extract . | vibgrate hcs digest --format html --out digest.html");}};function mo(){console.log(""),console.log(l.bold("vibgrate")+l.dim(" \u2014 Continuous Drift Intelligence")),console.log(""),console.log(l.bold("Usage:")),console.log(" vibgrate <command> [options]"),console.log(" vibgrate help [command] Show detailed help for a command"),console.log(""),console.log(l.bold("Getting started:")),console.log(` ${l.cyan("init")} Initialise vibgrate in a project (creates config & .vibgrate/ dir)`),console.log(` ${l.cyan("login")} Sign the CLI into your workspace via the browser`),console.log(` ${l.cyan("logout")} Clear stored login credentials`),console.log(""),console.log(l.bold("Core scanning:")),console.log(` ${l.cyan("scan")} Scan a project for upgrade drift and generate a report`),console.log(` ${l.cyan("baseline")} Save a baseline snapshot to compare future scans against`),console.log(""),console.log(l.bold("Reporting & export:")),console.log(` ${l.cyan("report")} Re-generate a report from a previously saved scan artifact`),console.log(` ${l.cyan("sbom")} Supply-chain evidence: SBOM (CycloneDX/SPDX) export/delta + OpenVEX`),console.log(""),console.log(l.bold("HCS (Holistic Code Specification):")),console.log(` ${l.cyan("hcs")} Extract and render Holistic Code Specification facts`),console.log(` ${l.cyan("hcs extract")} Analyze source code and emit validated HCS facts (NDJSON)`),console.log(` ${l.cyan("hcs digest")} Render HCS NDJSON facts into Markdown, JSON, or HTML`),console.log(""),console.log(l.bold("CI/CD integration:")),console.log(` ${l.cyan("push")} Upload a scan artifact to the Vibgrate API`),console.log(` ${l.cyan("dsn")} Create and manage DSN tokens for API authentication`),console.log(""),console.log(l.bold("Maintenance:")),console.log(` ${l.cyan("update")} Update the vibgrate CLI to the latest version`),console.log(""),console.log(l.dim("Run")+` ${l.cyan("vibgrate help <command>")} `+l.dim("for detailed options, e.g.")+` ${l.cyan("vibgrate help scan")}`);}var ho=new Command("help").description("Show help for vibgrate commands").argument("[command]","Command to show detailed help for").helpOption(false).action(t=>{let e=t?.toLowerCase().trim();e&&xt[e]?xt[e]():(e&&(console.log(""),console.log(l.red(`Unknown command: ${e}`)),console.log(l.dim(`Available commands: ${Object.keys(xt).join(", ")}`))),mo()),Us();});var N=new Command;N.name("vibgrate").description("Continuous Drift Intelligence").version(E).addHelpText("after",`
|
|
166
|
-
|
|
2
|
+
import { resolveDsn, resolveIngestHost, dashHostForIngestHost, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, dsnCommand, pushCommand, ExitCode, CliError, verifyDeterminism, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, countPending, resolveOne, GraphIndex, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, usageError, loadGraph, detectServeLaunch, installAssistant, uninstallAssistant, buildGraph, serializeGraph, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, exportGraph, catalogPath, libDir, stableStringify, writeArtifacts, writeSnapshot, UsageError, cacheDir, probeFreshness, discover, vibgrateDir, modelCacheInfo, clearModelCache, GraphSource, createServer, assistantById, loadCatalog, driftFor, addLibrary, parseDsn as parseDsn$1, publishPrivateLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface, assessDocQuality, symbolsFromApi, fetchHostedDocs, selectForBudget, formatForExt, renderHtml, isModelReady } from './chunk-J4DFLN2Q.js';
|
|
3
|
+
import { resolvedGrammarFiles, grammarSetVersion } from './chunk-X5YT263H.js';
|
|
4
|
+
import { loadAdvancedScanHook, baselineCommand } from './chunk-FPUNJNXD.js';
|
|
5
|
+
import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, prepareCompressedUpload, titleBox, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, brandProgressBar } from './chunk-2PJRXVUW.js';
|
|
6
|
+
import './chunk-RXP66R2E.js';
|
|
7
|
+
import { pathExists as pathExists$1, parseExcludePatterns } from './chunk-4M2MUQRO.js';
|
|
8
|
+
import * as fs4 from 'fs';
|
|
9
|
+
import fs4__default, { realpathSync } from 'fs';
|
|
10
|
+
import * as path9 from 'path';
|
|
11
|
+
import path9__default from 'path';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
import { Command, CommanderError, Option } from 'commander';
|
|
14
|
+
import { execSync, spawn, execFileSync } from 'child_process';
|
|
15
|
+
import chalk5 from 'chalk';
|
|
16
|
+
import * as fs10 from 'fs/promises';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import semver from 'semver';
|
|
19
|
+
import { randomUUID } from 'crypto';
|
|
20
|
+
|
|
21
|
+
function info(message = "") {
|
|
22
|
+
process.stderr.write(`${message}
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
function out(message) {
|
|
26
|
+
process.stdout.write(`${message}
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
function json(value) {
|
|
30
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
33
|
+
var c = chalk5;
|
|
34
|
+
function disableColor() {
|
|
35
|
+
chalk5.level = 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/util/logo.ts
|
|
39
|
+
var teal = (s) => c.hex("#3FB0A4")(s);
|
|
40
|
+
var mint = (s) => c.hex("#4FE3C1")(s);
|
|
41
|
+
var ROBOT = [
|
|
42
|
+
" " + teal("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u256E") + mint("\u279C"),
|
|
43
|
+
" " + c.dim("\u2524") + teal("\u2502") + " " + mint("\u25FC") + " " + mint("\u25FC") + " " + teal("\u2502") + c.dim("\u251C"),
|
|
44
|
+
" " + c.dim("\u2524") + teal("\u2502") + " " + c.dim("\u2581\u2581") + " " + teal("\u2502") + c.dim("\u251C"),
|
|
45
|
+
" " + teal("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u256F")
|
|
46
|
+
];
|
|
47
|
+
function logoLines(root) {
|
|
48
|
+
return [
|
|
49
|
+
"",
|
|
50
|
+
` ${ROBOT[0]}`,
|
|
51
|
+
` ${ROBOT[1]}`,
|
|
52
|
+
` ${ROBOT[2]}`,
|
|
53
|
+
` ${ROBOT[3]}`,
|
|
54
|
+
` ${c.bold.white("vibgrate")} ${teal("graph")} ${c.dim(`\xB7 deterministic code graph \xB7 v${VERSION}`)}`,
|
|
55
|
+
...root ? [` ${c.dim(root)}`] : [],
|
|
56
|
+
""
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
function printLogo(root) {
|
|
60
|
+
if (!process.stderr.isTTY) return;
|
|
61
|
+
process.stderr.write(`${logoLines(root).join("\n")}
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/util/progress.ts
|
|
66
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
67
|
+
var WIDTH = 30;
|
|
68
|
+
function fmtDuration(ms) {
|
|
69
|
+
const s = Math.max(0, Math.round(ms / 1e3));
|
|
70
|
+
if (s < 60) return `${s}s`;
|
|
71
|
+
return `${Math.floor(s / 60)}m${String(s % 60).padStart(2, "0")}s`;
|
|
72
|
+
}
|
|
73
|
+
function formatProgressLine(label, done, total, elapsedMs, frame) {
|
|
74
|
+
const ratio = total > 0 ? Math.min(done / total, 1) : 0;
|
|
75
|
+
const bar = brandProgressBar(ratio, WIDTH);
|
|
76
|
+
const pct = Math.round(ratio * 100);
|
|
77
|
+
const spin = c.cyan(FRAMES[(frame % FRAMES.length + FRAMES.length) % FRAMES.length]);
|
|
78
|
+
const eta = ratio > 0.02 && ratio < 1 ? ` \xB7 eta ${fmtDuration(elapsedMs / ratio * (1 - ratio))}` : "";
|
|
79
|
+
return ` ${spin} ${label} ${bar} ${c.bold(`${pct}%`)} ${c.dim(`${done}/${total} \xB7 ${fmtDuration(elapsedMs)}${eta}`)}`;
|
|
80
|
+
}
|
|
81
|
+
var ProgressBar = class {
|
|
82
|
+
constructor(label) {
|
|
83
|
+
this.label = label;
|
|
84
|
+
}
|
|
85
|
+
label;
|
|
86
|
+
start = Date.now();
|
|
87
|
+
frame = 0;
|
|
88
|
+
tty = Boolean(process.stderr.isTTY);
|
|
89
|
+
update(done, total) {
|
|
90
|
+
if (!this.tty) return;
|
|
91
|
+
const line = formatProgressLine(this.label, done, total, Date.now() - this.start, this.frame++);
|
|
92
|
+
process.stderr.write(`\r\x1B[2K${line}`);
|
|
93
|
+
}
|
|
94
|
+
/** Clear the bar; optionally print a final summary line. */
|
|
95
|
+
done(summary) {
|
|
96
|
+
if (!this.tty) return;
|
|
97
|
+
process.stderr.write("\r\x1B[2K");
|
|
98
|
+
if (summary) process.stderr.write(`${summary}
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/cli-options.ts
|
|
104
|
+
function applyGlobalOptions(cmd) {
|
|
105
|
+
return cmd.option("-C, --cwd <dir>", "run as if started in <dir>", ".").option("--deep", "do more: precise resolution, all edges, semantic links").option("--json", "machine-readable JSON on stdout").option("--generated-at <iso>", "pin the artifact timestamp for byte-deterministic output").option("--graph <file>", "override the map path").option("--no-cache", "full rebuild (ignore the incremental cache)").option("--local", "never touch the network (lexical-only semantic)").option("--quiet", "suppress progress output").option("--no-color", "disable colored output");
|
|
106
|
+
}
|
|
107
|
+
function readGlobal(cmd) {
|
|
108
|
+
const o = cmd.opts();
|
|
109
|
+
return {
|
|
110
|
+
cwd: o.cwd,
|
|
111
|
+
deep: o.deep,
|
|
112
|
+
json: o.json,
|
|
113
|
+
generatedAt: o.generatedAt,
|
|
114
|
+
graph: o.graph,
|
|
115
|
+
noCache: o.cache === false,
|
|
116
|
+
local: o.local,
|
|
117
|
+
quiet: o.quiet
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/commands/build.ts
|
|
122
|
+
function registerBuild(program) {
|
|
123
|
+
const cmd = program.command("build").description("build / update the code map (incremental, deterministic)").argument("[paths...]", "folders or files to map (default: current folder)").option("--only <langs>", "restrict to languages, e.g. ts,py,go").option("--exclude <glob>", "extra ignore glob (repeatable)", collect, []).option("--no-html", "do not write graph.html").option("--no-report", "do not write GRAPH_REPORT.md").option("--no-ground", "do not attach grounding (Phase 2)").option("--jobs <n>", "worker count (1 = single-threaded)").option("--scip <file>", "ingest a SCIP index for precise resolution (default: auto-detect index.scip)").option("--no-scip", "ignore any SCIP index").option("--no-tsc", "skip the in-process TypeScript resolver (heuristic floor only)").option("--no-warm", "do not warm the semantic index in the background after building").option("--grammars <dir>", "directory of grammar .wasm files (offline / air-gapped)").option("-o, --export <file>", "also write the map to a file (format inferred)").action(async function(paths, opts) {
|
|
124
|
+
await runBuild(paths, opts, readGlobal(this));
|
|
125
|
+
});
|
|
126
|
+
applyGlobalOptions(cmd);
|
|
127
|
+
}
|
|
128
|
+
async function runBuild(paths, opts, global) {
|
|
129
|
+
const root = path9.resolve(global.cwd ?? ".");
|
|
130
|
+
const interactive = !global.json && !global.quiet;
|
|
131
|
+
if (interactive) printLogo(path9.basename(root) || root);
|
|
132
|
+
const bar = interactive ? new ProgressBar(c.dim("parsing")) : void 0;
|
|
133
|
+
const jobs = opts.jobs ? Number(opts.jobs) : void 0;
|
|
134
|
+
if (jobs !== void 0 && (!Number.isInteger(jobs) || jobs < 1)) {
|
|
135
|
+
throw usageError(`--jobs must be a positive integer (got "${opts.jobs}")`);
|
|
136
|
+
}
|
|
137
|
+
const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
138
|
+
let result;
|
|
139
|
+
try {
|
|
140
|
+
result = await buildGraph({
|
|
141
|
+
root,
|
|
142
|
+
paths: paths.length ? paths : void 0,
|
|
143
|
+
only,
|
|
144
|
+
exclude: opts.exclude,
|
|
145
|
+
jobs,
|
|
146
|
+
noCache: global.noCache,
|
|
147
|
+
deep: global.deep,
|
|
148
|
+
noGround: opts.ground === false,
|
|
149
|
+
scip: typeof opts.scip === "string" ? opts.scip : void 0,
|
|
150
|
+
noScip: opts.scip === false,
|
|
151
|
+
noTsc: opts.tsc === false,
|
|
152
|
+
generatedAt: global.generatedAt,
|
|
153
|
+
onParseProgress: bar ? (done, total) => bar.update(done, total) : void 0,
|
|
154
|
+
grammarsDir: opts.grammars
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
bar?.done();
|
|
158
|
+
if (err instanceof UsageError) throw usageError(err.message);
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
bar?.done();
|
|
162
|
+
const written = writeArtifacts(result.graph, {
|
|
163
|
+
root,
|
|
164
|
+
html: opts.html,
|
|
165
|
+
report: opts.report,
|
|
166
|
+
graphPath: global.graph
|
|
167
|
+
});
|
|
168
|
+
if (!global.graph) {
|
|
169
|
+
writeSnapshot(root, result.graph.provenance.corpusHash, result.fileStats, {
|
|
170
|
+
only,
|
|
171
|
+
exclude: opts.exclude,
|
|
172
|
+
paths: paths.length ? paths : void 0,
|
|
173
|
+
deep: global.deep,
|
|
174
|
+
noGround: opts.ground === false,
|
|
175
|
+
scip: typeof opts.scip === "string" ? opts.scip : void 0,
|
|
176
|
+
noScip: opts.scip === false,
|
|
177
|
+
noTsc: opts.tsc === false,
|
|
178
|
+
grammarsDir: opts.grammars
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (opts.export) writeExport(result.graph, opts.export);
|
|
182
|
+
if (global.json) {
|
|
183
|
+
json({
|
|
184
|
+
ok: true,
|
|
185
|
+
counts: result.graph.meta.counts,
|
|
186
|
+
languages: result.graph.meta.languages,
|
|
187
|
+
reparsed: result.reparsed,
|
|
188
|
+
reused: result.reused,
|
|
189
|
+
totalFiles: result.totalFiles,
|
|
190
|
+
resolve: result.resolveStats,
|
|
191
|
+
tsc: result.tsc,
|
|
192
|
+
scip: result.scip,
|
|
193
|
+
artifacts: written,
|
|
194
|
+
corpusHash: result.graph.provenance.corpusHash,
|
|
195
|
+
timingMs: result.timing.totalMs
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const { counts } = result.graph.meta;
|
|
200
|
+
const incremental = result.reused > 0 ? `incremental: ${result.reparsed} of ${result.totalFiles} files re-parsed` : `${result.totalFiles} files parsed`;
|
|
201
|
+
const seconds = (result.timing.totalMs / 1e3).toFixed(2);
|
|
202
|
+
info(`${c.cyan("vg")} \xB7 mapped ${rel(root)} in ${seconds}s (${incremental})`);
|
|
203
|
+
info(
|
|
204
|
+
` nodes ${c.bold(String(counts.nodes))} edges ${c.bold(String(counts.edges))} areas ${counts.areas} langs ${result.graph.meta.languages.join(",") || "\u2014"}`
|
|
205
|
+
);
|
|
206
|
+
const callPct = result.resolveStats.callsResolved + result.resolveStats.callsUnresolved > 0 ? Math.round(
|
|
207
|
+
100 * result.resolveStats.callsResolved / (result.resolveStats.callsResolved + result.resolveStats.callsUnresolved)
|
|
208
|
+
) : 100;
|
|
209
|
+
const precise = result.tsc || result.scip;
|
|
210
|
+
const callLabel = precise ? `heuristic floor ${callPct}%` : `calls resolved ${callPct}%`;
|
|
211
|
+
info(c.dim(` ${callLabel} \xB7 resolver ${result.graph.provenance.resolver.join(",")}`));
|
|
212
|
+
if (result.tsc) {
|
|
213
|
+
const jsx = result.tsc.jsx > 0 ? `, ${result.tsc.jsx} JSX` : "";
|
|
214
|
+
info(c.dim(` tsc: ${result.tsc.resolved} precise edges across ${result.tsc.files} TS/JS files (${result.tsc.calls} calls${jsx})`));
|
|
215
|
+
}
|
|
216
|
+
if (result.scip) {
|
|
217
|
+
info(c.dim(` scip: ${result.scip.resolved} precise edges from ${result.scip.tool ?? "index"} (${result.scip.documents} docs)`));
|
|
218
|
+
}
|
|
219
|
+
const artifactList = [written.graphPath, written.htmlPath, written.reportPath].filter(Boolean).map((p) => path9.relative(root, p)).join(" ");
|
|
220
|
+
info(` \u2192 ${artifactList}`);
|
|
221
|
+
if (result.warnings.length) {
|
|
222
|
+
info(c.yellow(` ${result.warnings.length} parse warning(s) \u2014 run with --json for detail`));
|
|
223
|
+
}
|
|
224
|
+
maybeWarmEmbeddings(root, result.graph, global, opts.warm !== false);
|
|
225
|
+
}
|
|
226
|
+
function maybeWarmEmbeddings(root, graph, global, warm) {
|
|
227
|
+
if (!warm || global.json || global.quiet || global.local) return;
|
|
228
|
+
if (!process.stdout.isTTY && !process.stderr.isTTY) return;
|
|
229
|
+
const modelId = resolveEmbedModel();
|
|
230
|
+
if (!isModelReady(modelId)) return;
|
|
231
|
+
if (countPending(graph, root, modelId) === 0) return;
|
|
232
|
+
const cli = process.argv[1];
|
|
233
|
+
if (!cli) return;
|
|
234
|
+
try {
|
|
235
|
+
const child = spawn(process.execPath, [cli, "embed", "-C", root, "--bg"], {
|
|
236
|
+
detached: true,
|
|
237
|
+
stdio: "ignore"
|
|
238
|
+
});
|
|
239
|
+
child.unref();
|
|
240
|
+
info(c.dim(" warming the semantic index in the background \u2014 `vg ask` will be instant"));
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function writeExport(graph, target) {
|
|
245
|
+
if (target === "-") {
|
|
246
|
+
out(serializeGraph(graph).trimEnd());
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const ext = path9.extname(target).toLowerCase();
|
|
250
|
+
let content;
|
|
251
|
+
switch (ext) {
|
|
252
|
+
case ".json":
|
|
253
|
+
content = serializeGraph(graph);
|
|
254
|
+
break;
|
|
255
|
+
case ".md":
|
|
256
|
+
content = renderReport(graph);
|
|
257
|
+
break;
|
|
258
|
+
case ".html":
|
|
259
|
+
content = renderHtml(graph);
|
|
260
|
+
break;
|
|
261
|
+
default:
|
|
262
|
+
throw new CliError(
|
|
263
|
+
`cannot export to "${ext || target}" yet \u2014 supported in Phase 0: .json, .md, .html, "-" (stdout). More formats (graphml, dot, cypher) arrive with \`vg export\` in Phase 1.`,
|
|
264
|
+
ExitCode.USAGE_ERROR
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
fs4.mkdirSync(path9.dirname(path9.resolve(target)), { recursive: true });
|
|
268
|
+
fs4.writeFileSync(target, content);
|
|
269
|
+
}
|
|
270
|
+
function collect(value, prev) {
|
|
271
|
+
return [...prev, value];
|
|
272
|
+
}
|
|
273
|
+
function rel(root) {
|
|
274
|
+
const r = path9.relative(process.cwd(), root);
|
|
275
|
+
return r === "" ? "." : r;
|
|
276
|
+
}
|
|
277
|
+
function registerStatus(program) {
|
|
278
|
+
const cmd = program.command("status").description("cache/freshness, counts, staleness, resolver rungs used").action(async function() {
|
|
279
|
+
await runStatus(readGlobal(this));
|
|
280
|
+
});
|
|
281
|
+
applyGlobalOptions(cmd);
|
|
282
|
+
}
|
|
283
|
+
async function runStatus(global) {
|
|
284
|
+
const root = path9.resolve(global.cwd ?? ".");
|
|
285
|
+
const graphPath = global.graph ?? defaultGraphPath(root);
|
|
286
|
+
const graph = loadGraph(root, graphPath);
|
|
287
|
+
const hasCache = fs4.existsSync(path9.join(cacheDir(root), "parse-cache.json"));
|
|
288
|
+
let stale = null;
|
|
289
|
+
let staleExact = false;
|
|
290
|
+
if (graph) {
|
|
291
|
+
const probe = probeFreshness(root);
|
|
292
|
+
if (probe) {
|
|
293
|
+
stale = driftCount(probe.drift);
|
|
294
|
+
staleExact = true;
|
|
295
|
+
} else {
|
|
296
|
+
const files = discover({ root });
|
|
297
|
+
let changed = 0;
|
|
298
|
+
const graphFiles = new Set(graph.nodes.filter((n) => n.kind === "file").map((n) => n.file));
|
|
299
|
+
const currentFiles = new Set(files.map((f) => f.rel));
|
|
300
|
+
for (const f of currentFiles) if (!graphFiles.has(f)) changed++;
|
|
301
|
+
for (const f of graphFiles) if (!currentFiles.has(f)) changed++;
|
|
302
|
+
stale = changed;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (global.json) {
|
|
306
|
+
json({
|
|
307
|
+
root: path9.relative(process.cwd(), root) || ".",
|
|
308
|
+
graphPath: path9.relative(root, graphPath),
|
|
309
|
+
built: graph !== null,
|
|
310
|
+
generatedAt: graph?.generatedAt ?? null,
|
|
311
|
+
counts: graph?.meta.counts ?? null,
|
|
312
|
+
languages: graph?.meta.languages ?? [],
|
|
313
|
+
cluster: graph?.meta.cluster ?? null,
|
|
314
|
+
resolver: graph?.provenance.resolver ?? [],
|
|
315
|
+
corpusHash: graph?.provenance.corpusHash ?? null,
|
|
316
|
+
cache: hasCache,
|
|
317
|
+
staleFiles: stale,
|
|
318
|
+
staleExact
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!graph) {
|
|
323
|
+
info(`${c.cyan("vg")} \xB7 no map yet at ${c.dim(path9.relative(root, graphPath))}`);
|
|
324
|
+
info(` run ${c.bold("vg")} to build one`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
info(`${c.cyan("vg")} \xB7 ${path9.relative(root, graphPath)}`);
|
|
328
|
+
info(` generated ${graph.generatedAt}`);
|
|
329
|
+
info(
|
|
330
|
+
` nodes ${c.bold(String(graph.meta.counts.nodes))} edges ${c.bold(
|
|
331
|
+
String(graph.meta.counts.edges)
|
|
332
|
+
)} areas ${graph.meta.counts.areas} langs ${graph.meta.languages.join(",") || "\u2014"}`
|
|
333
|
+
);
|
|
334
|
+
info(` cluster ${graph.meta.cluster} \xB7 resolver ${graph.provenance.resolver.join(",")}`);
|
|
335
|
+
info(` cache ${hasCache ? c.green("warm") : c.dim("cold")} \xB7 corpus ${graph.provenance.corpusHash.slice(0, 12)}\u2026`);
|
|
336
|
+
if (stale && stale > 0) {
|
|
337
|
+
info(c.yellow(` ${stale} file(s) changed since last build \u2014 run ${c.bold("vg")} to refresh`));
|
|
338
|
+
} else {
|
|
339
|
+
info(c.dim(" up to date"));
|
|
340
|
+
}
|
|
341
|
+
if (vibgrateDir(root)) ;
|
|
342
|
+
}
|
|
343
|
+
function registerVerify(program) {
|
|
344
|
+
const cmd = program.command("verify").description("determinism self-check (exit 4 on mismatch)").option("--only <langs>", "restrict to languages, e.g. ts,py").option("--jobs <n>", "worker count").action(async function(opts) {
|
|
345
|
+
const global = readGlobal(this);
|
|
346
|
+
const root = path9.resolve(global.cwd ?? ".");
|
|
347
|
+
const result = await verifyDeterminism({
|
|
348
|
+
root,
|
|
349
|
+
only: opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
|
|
350
|
+
jobs: opts.jobs ? Number(opts.jobs) : void 0
|
|
351
|
+
});
|
|
352
|
+
if (global.json) {
|
|
353
|
+
json({ ok: result.ok, checks: result.checks, digest: result.digest });
|
|
354
|
+
} else {
|
|
355
|
+
info(`${c.cyan("vg verify")} \xB7 ${path9.relative(process.cwd(), root) || "."}`);
|
|
356
|
+
for (const check of result.checks) {
|
|
357
|
+
const mark = check.ok ? c.green("\u2714") : c.red("\u2718");
|
|
358
|
+
const detail = check.detail ? c.dim(` (${check.detail})`) : "";
|
|
359
|
+
info(` ${mark} ${check.name}${detail}`);
|
|
360
|
+
}
|
|
361
|
+
info(result.ok ? c.green(` deterministic \xB7 digest ${result.digest.slice(0, 16)}\u2026`) : c.red(" NON-DETERMINISTIC"));
|
|
362
|
+
}
|
|
363
|
+
if (!result.ok) {
|
|
364
|
+
throw new CliError("determinism self-check failed", ExitCode.NON_DETERMINISTIC);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
applyGlobalOptions(cmd);
|
|
368
|
+
}
|
|
369
|
+
function rootOf(global) {
|
|
370
|
+
return path9.resolve(global.cwd ?? ".");
|
|
371
|
+
}
|
|
372
|
+
function requireGraph(global) {
|
|
373
|
+
const root = rootOf(global);
|
|
374
|
+
const graph = loadGraph(root, global.graph);
|
|
375
|
+
if (!graph) {
|
|
376
|
+
throw new CliError(
|
|
377
|
+
`no map found \u2014 run \`vg\` to build one first` + (global.graph ? ` (looked at ${global.graph})` : ""),
|
|
378
|
+
ExitCode.NOT_FOUND
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return { root, graph };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/commands/ask.ts
|
|
385
|
+
function registerAsk(program) {
|
|
386
|
+
const cmd = program.command("ask").description("ask the map \u2014 returns a budget-bounded context block").argument("<question...>", 'your question (a bare quoted string also works: vg "\u2026")').option("-b, --budget <n>", "approx token budget for the answer", "2000").option("--no-semantic", "lexical only (skip the local-embedding pass)").option("--no-refresh", "answer from the map as built \u2014 skip the auto-rebuild when files change").action(async function(question, opts) {
|
|
387
|
+
const global = readGlobal(this);
|
|
388
|
+
const root = rootOf(global);
|
|
389
|
+
if (opts.refresh !== false && !global.graph) {
|
|
390
|
+
const refreshed = await refreshIfStale(root);
|
|
391
|
+
if (refreshed.status === "refreshed" && !global.json) {
|
|
392
|
+
const n = driftCount(refreshed.drift);
|
|
393
|
+
info(c.dim(` map refreshed \u2014 ${n} file(s) drifted (${(refreshed.ms / 1e3).toFixed(2)}s)`));
|
|
394
|
+
} else if (refreshed.status === "error" && !global.json) {
|
|
395
|
+
info(c.yellow(` map refresh failed (${refreshed.message}) \u2014 answering from the last built map`));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const { graph } = requireGraph(global);
|
|
399
|
+
const budget = Number(opts.budget) || 2e3;
|
|
400
|
+
const q = question.join(" ");
|
|
401
|
+
const wantSemantic = opts.semantic !== false && !global.local;
|
|
402
|
+
let result;
|
|
403
|
+
let mode = "lexical";
|
|
404
|
+
let note = "";
|
|
405
|
+
if (wantSemantic) {
|
|
406
|
+
const firstRun = !embeddingsCached(root, resolveEmbedModel());
|
|
407
|
+
if (!global.json && firstRun) {
|
|
408
|
+
info(c.dim(" preparing semantic search (first use embeds the map; cached, resumable & offline after)\u2026"));
|
|
409
|
+
}
|
|
410
|
+
let reason;
|
|
411
|
+
const embedder = await loadEmbedder({ local: global.local, onUnavailable: (r) => reason = r });
|
|
412
|
+
if (embedder) {
|
|
413
|
+
const bar = !global.json && firstRun ? new ProgressBar(c.dim("embedding")) : void 0;
|
|
414
|
+
const vectors = await getNodeEmbeddings(graph, embedder, root, bar ? (d, t) => bar.update(d, t) : void 0);
|
|
415
|
+
bar?.done();
|
|
416
|
+
result = await queryGraphSemantic(graph, q, { budget, embedder, nodeVectors: vectors });
|
|
417
|
+
mode = `semantic (${embedder.id})`;
|
|
418
|
+
} else {
|
|
419
|
+
result = queryGraph(graph, q, { budget });
|
|
420
|
+
note = reason ? unavailableMessage(reason) : "semantic unavailable; used lexical";
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
result = queryGraph(graph, q, { budget });
|
|
424
|
+
if (global.local) note = "semantic skipped under --local; used lexical";
|
|
425
|
+
}
|
|
426
|
+
if (global.json) {
|
|
427
|
+
json({
|
|
428
|
+
question: result.question,
|
|
429
|
+
mode,
|
|
430
|
+
note: note || void 0,
|
|
431
|
+
tokensEstimate: result.tokensEstimate,
|
|
432
|
+
matches: result.matches.map((m) => ({
|
|
433
|
+
name: m.node.qualifiedName,
|
|
434
|
+
kind: m.node.kind,
|
|
435
|
+
file: m.node.file,
|
|
436
|
+
line: m.node.span.start,
|
|
437
|
+
score: m.score,
|
|
438
|
+
why: m.why
|
|
439
|
+
})),
|
|
440
|
+
context: result.context
|
|
441
|
+
});
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
out(result.context);
|
|
445
|
+
info("");
|
|
446
|
+
info(c.dim(`vg \xB7 ${result.matches.length} match(es) \xB7 ~${result.tokensEstimate} tokens (budget ${budget}) \xB7 ${mode}`));
|
|
447
|
+
if (note) info(c.yellow(` ${note}`));
|
|
448
|
+
});
|
|
449
|
+
applyGlobalOptions(cmd);
|
|
450
|
+
}
|
|
451
|
+
function mb(bytes) {
|
|
452
|
+
if (bytes <= 0) return "0 MB";
|
|
453
|
+
if (bytes < 1024 * 1024) return `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
|
454
|
+
return `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
455
|
+
}
|
|
456
|
+
function registerEmbed(program) {
|
|
457
|
+
const cmd = program.command("embed").description("precompute the semantic index for instant `vg ask` (or --where / --clear)").option("--where", "show where the model is cached, and its size").option("--clear", "remove the downloaded model from the shared cache").addOption(new Option("--bg").hideHelp()).action(async function(opts) {
|
|
458
|
+
const global = readGlobal(this);
|
|
459
|
+
const modelId = resolveEmbedModel();
|
|
460
|
+
if (opts.where) return showWhere(rootOf(global), modelId, global.json);
|
|
461
|
+
if (opts.clear) return doClear(global.json);
|
|
462
|
+
const { graph } = requireGraph(global);
|
|
463
|
+
const root = rootOf(global);
|
|
464
|
+
const bg = opts.bg === true;
|
|
465
|
+
const speak = !bg && !global.json;
|
|
466
|
+
if (global.local) {
|
|
467
|
+
if (speak) info(c.dim("vg embed \xB7 semantic is off (--local) \u2014 lexical search only"));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (countPending(graph, root, modelId) === 0) {
|
|
471
|
+
if (global.json) json({ embedded: 0, pending: 0, upToDate: true, model: modelId });
|
|
472
|
+
else if (speak) info(`${c.cyan("vg embed")} \xB7 semantic index already up to date`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
let reason;
|
|
476
|
+
const embedder = await loadEmbedder({
|
|
477
|
+
local: global.local,
|
|
478
|
+
noDownload: bg,
|
|
479
|
+
// background warm-up never downloads
|
|
480
|
+
showDownloadProgress: !bg,
|
|
481
|
+
onUnavailable: (r) => reason = r
|
|
482
|
+
});
|
|
483
|
+
if (!embedder) {
|
|
484
|
+
if (speak && reason) info(c.dim(`vg embed \xB7 ${unavailableMessage(reason)}`));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const pending = countPending(graph, root, modelId);
|
|
488
|
+
const bar = speak ? new ProgressBar(c.dim("embedding")) : void 0;
|
|
489
|
+
await getNodeEmbeddings(graph, embedder, root, bar ? (d, t) => bar.update(d, t) : void 0);
|
|
490
|
+
bar?.done();
|
|
491
|
+
if (global.json) json({ embedded: pending, model: modelId });
|
|
492
|
+
else if (speak) info(`${c.green("\u2714")} vg embed \xB7 semantic index ready (~${pending} node(s) \xB7 model ${modelId})`);
|
|
493
|
+
});
|
|
494
|
+
applyGlobalOptions(cmd);
|
|
495
|
+
}
|
|
496
|
+
function showWhere(root, modelId, asJson) {
|
|
497
|
+
const m = modelCacheInfo(modelId);
|
|
498
|
+
const repoCached = embeddingsCached(root, modelId);
|
|
499
|
+
if (asJson) {
|
|
500
|
+
json({ model: modelId, cacheDir: m.dir, present: m.present, bytes: m.bytes, repoEmbeddings: repoCached });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
info(`${c.cyan("vg embed")} \xB7 model cache`);
|
|
504
|
+
info(` model ${c.bold(modelId)}`);
|
|
505
|
+
info(` shared at ${m.dir}`);
|
|
506
|
+
info(` ${m.present ? c.green(`present \xB7 ~${mb(m.bytes)}`) : c.dim("not downloaded yet (fetched on first semantic ask)")}`);
|
|
507
|
+
info(` this repo ${repoCached ? c.green("embeddings cached") : c.dim("not embedded yet \u2014 run `vg embed`")}`);
|
|
508
|
+
info(c.dim(" no admin/sudo needed \u2014 the cache is in your home folder, shared across all repos."));
|
|
509
|
+
info(c.dim(" turn off: run with --local \xB7 relocate: XDG_CACHE_HOME=<dir> \xB7 remove: vg embed --clear"));
|
|
510
|
+
}
|
|
511
|
+
function doClear(asJson) {
|
|
512
|
+
const before = modelCacheInfo();
|
|
513
|
+
const freed = clearModelCache();
|
|
514
|
+
if (asJson) {
|
|
515
|
+
json({ removed: before.present, bytesFreed: freed, dir: before.dir });
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (!before.present || freed === 0) {
|
|
519
|
+
info(`${c.cyan("vg embed")} \xB7 no model in the shared cache \u2014 nothing to remove (${before.dir})`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
info(`${c.green("\u2714")} vg embed \xB7 removed the shared model (~${mb(freed)}) from ${before.dir}`);
|
|
523
|
+
info(c.dim(" repos use fast lexical search until the next semantic ask re-downloads it."));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/commands/ambiguity.ts
|
|
527
|
+
function ambiguityError(summary, candidates, pickFlag = "--pick") {
|
|
528
|
+
if (candidates.length === 0) {
|
|
529
|
+
return new CliError(`${summary} \u2014 try \`vg hubs\` or a broader name/glob`, ExitCode.NOT_FOUND);
|
|
530
|
+
}
|
|
531
|
+
const lines = candidates.slice(0, 10).map((n, i) => ` ${i + 1}. ${n.qualifiedName} (${n.kind}, ${n.file}:${n.span.start})`);
|
|
532
|
+
const more = candidates.length > 10 ? `
|
|
533
|
+
\u2026and ${candidates.length - 10} more` : "";
|
|
534
|
+
return new CliError(
|
|
535
|
+
`${summary} \u2014 pick one with ${pickFlag} <n>:
|
|
536
|
+
${lines.join("\n")}${more}`,
|
|
537
|
+
ExitCode.NOT_FOUND
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/commands/show.ts
|
|
542
|
+
function registerShow(program) {
|
|
543
|
+
const cmd = program.command("show").description("explain a node: what it is, what it calls, what calls it").argument("<name>", "qualified name, short name, file:line, glob, or id").option("--pick <n>", "pick the nth candidate when ambiguous").action(function(name, opts) {
|
|
544
|
+
const global = readGlobal(this);
|
|
545
|
+
const { graph } = requireGraph(global);
|
|
546
|
+
const { node, candidates } = resolveOne(graph, name, opts.pick ? Number(opts.pick) : void 0);
|
|
547
|
+
if (!node) {
|
|
548
|
+
if (candidates.length === 0) {
|
|
549
|
+
throw ambiguityError(`no node matches "${name}"`, []);
|
|
550
|
+
}
|
|
551
|
+
throw ambiguityError(`"${name}" is ambiguous`, candidates);
|
|
552
|
+
}
|
|
553
|
+
const index = new GraphIndex(graph);
|
|
554
|
+
const callees = dedupe(index.callees(node.id).map((x) => x.node));
|
|
555
|
+
const callers = dedupe(index.callers(node.id).map((x) => x.node));
|
|
556
|
+
const extendsEdges = index.out(node.id, "extends").concat(index.out(node.id, "implements"));
|
|
557
|
+
const supertypes = extendsEdges.map((e) => index.node(e.dst)?.qualifiedName).filter(Boolean);
|
|
558
|
+
const area = graph.areas.find((a) => a.id === node.area);
|
|
559
|
+
if (global.json) {
|
|
560
|
+
json({
|
|
561
|
+
id: node.id,
|
|
562
|
+
name: node.qualifiedName,
|
|
563
|
+
kind: node.kind,
|
|
564
|
+
file: node.file,
|
|
565
|
+
line: node.span.start,
|
|
566
|
+
signature: node.signature ?? null,
|
|
567
|
+
importance: node.importance,
|
|
568
|
+
centrality: node.centrality,
|
|
569
|
+
isHub: node.isHub,
|
|
570
|
+
area: node.area,
|
|
571
|
+
areaLabel: area?.label ?? null,
|
|
572
|
+
tested: node.tested,
|
|
573
|
+
calls: callees.map((n) => n.qualifiedName),
|
|
574
|
+
calledBy: callers.map((n) => n.qualifiedName),
|
|
575
|
+
extends: supertypes
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
info(`${c.cyan(node.qualifiedName)} ${c.dim(`(${node.kind})`)}`);
|
|
580
|
+
info(` ${c.dim(`${node.file}:${node.span.start}`)}`);
|
|
581
|
+
if (node.signature) info(` ${c.bold(node.signature)}`);
|
|
582
|
+
info(
|
|
583
|
+
` importance ${node.importance.toFixed(3)}${node.isHub ? c.yellow(" \u2605 hub") : ""} \xB7 area #${node.area}${area ? ` ${c.dim(area.label)}` : ""}`
|
|
584
|
+
);
|
|
585
|
+
if (supertypes.length) info(` ${c.dim("extends:")} ${supertypes.join(", ")}`);
|
|
586
|
+
info(` ${c.dim("calls")} (${callees.length}): ${callees.slice(0, 12).map((n) => n.qualifiedName).join(", ") || "\u2014"}`);
|
|
587
|
+
info(` ${c.dim("called by")} (${callers.length}): ${callers.slice(0, 12).map((n) => n.qualifiedName).join(", ") || "\u2014"}`);
|
|
588
|
+
});
|
|
589
|
+
applyGlobalOptions(cmd);
|
|
590
|
+
}
|
|
591
|
+
function dedupe(nodes) {
|
|
592
|
+
const seen = /* @__PURE__ */ new Set();
|
|
593
|
+
const out2 = [];
|
|
594
|
+
for (const n of nodes) {
|
|
595
|
+
if (seen.has(n.id)) continue;
|
|
596
|
+
seen.add(n.id);
|
|
597
|
+
out2.push(n);
|
|
598
|
+
}
|
|
599
|
+
return out2;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// src/commands/path.ts
|
|
603
|
+
function registerPath(program) {
|
|
604
|
+
const cmd = program.command("path").description("how A connects to B (shortest path)").argument("<a>", "source node").argument("<b>", "target node").option("--pick-a <n>", "pick the nth candidate for A when ambiguous").option("--pick-b <n>", "pick the nth candidate for B when ambiguous").action(function(a, b, opts) {
|
|
605
|
+
const global = readGlobal(this);
|
|
606
|
+
const { graph } = requireGraph(global);
|
|
607
|
+
const ra = resolveOne(graph, a, opts.pickA ? Number(opts.pickA) : void 0);
|
|
608
|
+
if (!ra.node) throw ambiguityError(`"${a}" ${ra.candidates.length ? "is ambiguous" : "not found"}`, ra.candidates, "--pick-a");
|
|
609
|
+
const rb = resolveOne(graph, b, opts.pickB ? Number(opts.pickB) : void 0);
|
|
610
|
+
if (!rb.node) throw ambiguityError(`"${b}" ${rb.candidates.length ? "is ambiguous" : "not found"}`, rb.candidates, "--pick-b");
|
|
611
|
+
const result = shortestPath(graph, ra.node.id, rb.node.id);
|
|
612
|
+
if (!result) {
|
|
613
|
+
throw new CliError(
|
|
614
|
+
`no path between ${ra.node.qualifiedName} and ${rb.node.qualifiedName}`,
|
|
615
|
+
ExitCode.NOT_FOUND
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
619
|
+
const names = result.ids.map((id) => byId.get(id)?.qualifiedName ?? id);
|
|
620
|
+
if (global.json) {
|
|
621
|
+
json({ from: ra.node.qualifiedName, to: rb.node.qualifiedName, hops: names.length - 1, direction: result.direction, path: names });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
info(`${c.cyan("vg path")} \xB7 ${names.length - 1} hop(s)${result.direction === "reverse" ? c.dim(" (reverse)") : ""}`);
|
|
625
|
+
info(" " + names.map((n) => c.bold(n)).join(c.dim(" \u2192 ")));
|
|
626
|
+
});
|
|
627
|
+
applyGlobalOptions(cmd);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/commands/tree.ts
|
|
631
|
+
function registerTree(program) {
|
|
632
|
+
const cmd = program.command("tree").description("the call tree rooted at a node (callees; --callers to invert)").argument("<name>", "root node").option("--callers", "show callers instead of callees").option("--depth <n>", "max depth", "3").option("--pick <n>", "pick the nth candidate when ambiguous").action(function(name, opts) {
|
|
633
|
+
const global = readGlobal(this);
|
|
634
|
+
const { graph } = requireGraph(global);
|
|
635
|
+
const { node, candidates } = resolveOne(graph, name, opts.pick ? Number(opts.pick) : void 0);
|
|
636
|
+
if (!node) throw ambiguityError(`"${name}" ${candidates.length ? "is ambiguous" : "not found"}`, candidates);
|
|
637
|
+
const index = new GraphIndex(graph);
|
|
638
|
+
const maxDepth = Math.max(1, Number(opts.depth) || 3);
|
|
639
|
+
const direction = opts.callers ? "callers" : "callees";
|
|
640
|
+
if (global.json) {
|
|
641
|
+
json(toJsonTree(index, node, direction, maxDepth, /* @__PURE__ */ new Set()));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
info(`${c.cyan("vg tree")} \xB7 ${direction} of ${c.bold(node.qualifiedName)} (depth ${maxDepth})`);
|
|
645
|
+
const lines = [];
|
|
646
|
+
walk(index, node, direction, maxDepth, 0, /* @__PURE__ */ new Set(), lines, "");
|
|
647
|
+
for (const l of lines) info(l);
|
|
648
|
+
});
|
|
649
|
+
applyGlobalOptions(cmd);
|
|
650
|
+
}
|
|
651
|
+
function next(index, node, direction) {
|
|
652
|
+
const rel2 = direction === "callees" ? index.callees(node.id) : index.callers(node.id);
|
|
653
|
+
const seen = /* @__PURE__ */ new Set();
|
|
654
|
+
const out2 = [];
|
|
655
|
+
for (const { node: n } of rel2) {
|
|
656
|
+
if (seen.has(n.id)) continue;
|
|
657
|
+
seen.add(n.id);
|
|
658
|
+
out2.push(n);
|
|
659
|
+
}
|
|
660
|
+
return out2.sort((a, b) => b.importance - a.importance || a.qualifiedName.localeCompare(b.qualifiedName));
|
|
661
|
+
}
|
|
662
|
+
function walk(index, node, direction, maxDepth, depth, visited, lines, prefix) {
|
|
663
|
+
if (depth === 0) lines.push(`${c.bold(node.qualifiedName)} ${c.dim(`(${node.kind})`)}`);
|
|
664
|
+
if (depth >= maxDepth) return;
|
|
665
|
+
if (visited.has(node.id)) {
|
|
666
|
+
lines.push(`${prefix}${c.dim("\u21BB (cycle)")}`);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
visited.add(node.id);
|
|
670
|
+
const children = next(index, node, direction);
|
|
671
|
+
children.forEach((child, i) => {
|
|
672
|
+
const last = i === children.length - 1;
|
|
673
|
+
const branch = last ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
674
|
+
const cyc = visited.has(child.id) ? c.dim(" \u21BB") : "";
|
|
675
|
+
lines.push(`${prefix}${c.dim(branch)}${child.qualifiedName}${cyc}`);
|
|
676
|
+
walk(index, child, direction, maxDepth, depth + 1, new Set(visited), lines, prefix + (last ? " " : c.dim("\u2502 ")));
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
function toJsonTree(index, node, direction, maxDepth, visited) {
|
|
680
|
+
if (maxDepth <= 0 || visited.has(node.id)) {
|
|
681
|
+
return { name: node.qualifiedName, kind: node.kind, cycle: visited.has(node.id) };
|
|
682
|
+
}
|
|
683
|
+
const nextVisited = new Set(visited).add(node.id);
|
|
684
|
+
return {
|
|
685
|
+
name: node.qualifiedName,
|
|
686
|
+
kind: node.kind,
|
|
687
|
+
children: next(index, node, direction).map(
|
|
688
|
+
(child) => toJsonTree(index, child, direction, maxDepth - 1, nextVisited)
|
|
689
|
+
)
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/commands/insights.ts
|
|
694
|
+
function registerInsights(program) {
|
|
695
|
+
const map = program.command("map").description("the overview report (areas, hubs, untested hotspots)").action(function() {
|
|
696
|
+
const global = readGlobal(this);
|
|
697
|
+
const { graph } = requireGraph(global);
|
|
698
|
+
if (global.json) json(reportJson(graph));
|
|
699
|
+
else out(renderReport(graph));
|
|
700
|
+
});
|
|
701
|
+
applyGlobalOptions(map);
|
|
702
|
+
const hubs = program.command("hubs").description("the most-depended-on code (centrality outliers)").option("-n, --limit <n>", "how many to show", "20").action(function(opts) {
|
|
703
|
+
const global = readGlobal(this);
|
|
704
|
+
const { graph } = requireGraph(global);
|
|
705
|
+
const limit = Number(opts.limit) || 20;
|
|
706
|
+
const list = graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external").sort((a, b) => b.importance - a.importance || a.qualifiedName.localeCompare(b.qualifiedName)).slice(0, limit);
|
|
707
|
+
if (global.json) {
|
|
708
|
+
json(list.map(nodeSummary));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
info(`${c.cyan("vg hubs")} \xB7 top ${list.length} by importance`);
|
|
712
|
+
for (const n of list) {
|
|
713
|
+
const hub = n.isHub ? c.yellow(" \u2605") : "";
|
|
714
|
+
info(` ${pad(n.importance.toFixed(3), 6)} ${c.bold(n.qualifiedName)}${hub} ${c.dim(`${n.file}:${n.span.start}`)}`);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
applyGlobalOptions(hubs);
|
|
718
|
+
const areas = program.command("areas").description("the natural groupings (communities), each labelled and sized").option("-n, --limit <n>", "how many to show", "30").action(function(opts) {
|
|
719
|
+
const global = readGlobal(this);
|
|
720
|
+
const { graph } = requireGraph(global);
|
|
721
|
+
const limit = Number(opts.limit) || 30;
|
|
722
|
+
const list = [...graph.areas].sort((a, b) => b.size - a.size || a.id - b.id).slice(0, limit);
|
|
723
|
+
if (global.json) {
|
|
724
|
+
json(list);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
info(`${c.cyan("vg areas")} \xB7 ${graph.areas.length} communities (${graph.meta.cluster})`);
|
|
728
|
+
for (const a of list) {
|
|
729
|
+
info(
|
|
730
|
+
` ${c.bold(`#${a.id}`)} ${a.label} ${c.dim(`${a.size} nodes \xB7 cohesion ${a.cohesion.toFixed(2)} \xB7 ${a.externalEdges} external`)}`
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
applyGlobalOptions(areas);
|
|
735
|
+
const oddities = program.command("oddities").description("surprising cross-area links (architectural smells)").option("-n, --limit <n>", "how many to show", "20").action(function(opts) {
|
|
736
|
+
const global = readGlobal(this);
|
|
737
|
+
const { graph } = requireGraph(global);
|
|
738
|
+
const limit = Number(opts.limit) || 20;
|
|
739
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
740
|
+
const list = graph.edges.filter((e) => typeof e.surprise === "number" && e.surprise > 0).sort((a, b) => (b.surprise ?? 0) - (a.surprise ?? 0) || a.id.localeCompare(b.id)).slice(0, limit);
|
|
741
|
+
if (global.json) {
|
|
742
|
+
json(
|
|
743
|
+
list.map((e) => ({
|
|
744
|
+
kind: e.kind,
|
|
745
|
+
surprise: e.surprise,
|
|
746
|
+
from: byId.get(e.src)?.qualifiedName ?? e.src,
|
|
747
|
+
to: byId.get(e.dst)?.qualifiedName ?? e.dst
|
|
748
|
+
}))
|
|
749
|
+
);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (!list.length) {
|
|
753
|
+
info(`${c.cyan("vg oddities")} \xB7 none found (no cross-area links, or only one area)`);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
info(`${c.cyan("vg oddities")} \xB7 top ${list.length} surprising links`);
|
|
757
|
+
for (const e of list) {
|
|
758
|
+
const from = byId.get(e.src)?.qualifiedName ?? e.src;
|
|
759
|
+
const to = byId.get(e.dst)?.qualifiedName ?? e.dst;
|
|
760
|
+
info(` ${pad((e.surprise ?? 0).toFixed(2), 5)} ${c.bold(from)} ${c.dim(`\u2014${e.kind}\u2192`)} ${c.bold(to)}`);
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
applyGlobalOptions(oddities);
|
|
764
|
+
}
|
|
765
|
+
function nodeSummary(n) {
|
|
766
|
+
return {
|
|
767
|
+
id: n.id,
|
|
768
|
+
name: n.qualifiedName,
|
|
769
|
+
kind: n.kind,
|
|
770
|
+
file: n.file,
|
|
771
|
+
line: n.span.start,
|
|
772
|
+
importance: n.importance,
|
|
773
|
+
isHub: n.isHub,
|
|
774
|
+
area: n.area
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function reportJson(graph) {
|
|
778
|
+
return {
|
|
779
|
+
counts: graph.meta.counts,
|
|
780
|
+
languages: graph.meta.languages,
|
|
781
|
+
cluster: graph.meta.cluster,
|
|
782
|
+
areas: graph.areas.length,
|
|
783
|
+
hubs: graph.nodes.filter((n) => n.isHub).length
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function pad(s, n) {
|
|
787
|
+
return s.padStart(n, " ");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/commands/impact.ts
|
|
791
|
+
function registerImpact(program) {
|
|
792
|
+
const cmd = program.command("impact").description("what breaks if you change it \u2014 and the tests to run").argument("<name>", "node to assess").option("--depth <n>", "max depth", "4").option("--tests", "also select the tests to run for the affected set").option("--fail-on-untested", "exit 2 if any affected node is untested").option("--pick <n>", "pick the nth candidate when ambiguous").action(function(name, opts) {
|
|
793
|
+
const global = readGlobal(this);
|
|
794
|
+
const { graph } = requireGraph(global);
|
|
795
|
+
const { node, candidates } = resolveOne(graph, name, opts.pick ? Number(opts.pick) : void 0);
|
|
796
|
+
if (!node) throw ambiguityError(`"${name}" ${candidates.length ? "is ambiguous" : "not found"}`, candidates);
|
|
797
|
+
const depth = Number(opts.depth) || 4;
|
|
798
|
+
const result = impactOf(graph, node.id, { depth });
|
|
799
|
+
const withTests = opts.tests || opts.failOnUntested;
|
|
800
|
+
const ti = withTests ? testsToRun(graph, node.id, depth) : void 0;
|
|
801
|
+
const runner = ti ? detectRunner(rootOf(global), node.lang) : void 0;
|
|
802
|
+
if (global.json) {
|
|
803
|
+
json({
|
|
804
|
+
...result,
|
|
805
|
+
tests: ti ? { toRun: ti.affectedTestFiles, command: runner?.command(ti.affectedTestFiles), untestedAffected: ti.untestedAffected } : void 0
|
|
806
|
+
});
|
|
807
|
+
} else {
|
|
808
|
+
info(`${c.cyan("Impact of")} ${c.bold(result.root.name)}`);
|
|
809
|
+
info(` Affected (${result.affected.length}) \u25B8 ${result.direct} direct, ${result.transitive} transitive (depth \u2264 ${result.depth})`);
|
|
810
|
+
const preview = result.affected.slice(0, 15);
|
|
811
|
+
for (const a of preview) {
|
|
812
|
+
info(` ${c.dim(`d${a.depth}`)} ${a.name} ${c.dim(`${a.file}:${a.line} \xB7 conf ${a.confidence.toFixed(2)}`)}`);
|
|
813
|
+
}
|
|
814
|
+
if (result.affected.length > preview.length) info(c.dim(` \u2026and ${result.affected.length - preview.length} more (use --json)`));
|
|
815
|
+
if (result.minEdgeConfidence < 1) info(c.dim(` lowest edge confidence ${result.minEdgeConfidence.toFixed(2)} (ambiguous/dynamic edges lower it)`));
|
|
816
|
+
if (ti) {
|
|
817
|
+
info(` ${c.cyan(`Tests to run (${ti.affectedTestFiles.length})`)} \u25B8 ${ti.affectedTestFiles.join(" \xB7 ") || c.dim("none found")}`);
|
|
818
|
+
if (runner && ti.affectedTestFiles.length) info(c.dim(` $ ${runner.command(ti.affectedTestFiles)}`));
|
|
819
|
+
if (ti.untestedAffected.length) info(c.yellow(` ${ti.untestedAffected.length} affected node(s) are untested`));
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (opts.failOnUntested && ti && ti.untestedAffected.length > 0) {
|
|
823
|
+
throw new CliError(
|
|
824
|
+
`${ti.untestedAffected.length} affected node(s) are untested: ${ti.untestedAffected.slice(0, 5).map((u) => u.name).join(", ")}`,
|
|
825
|
+
ExitCode.GATE_FAILED
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
applyGlobalOptions(cmd);
|
|
830
|
+
}
|
|
831
|
+
function registerServe(program) {
|
|
832
|
+
const cmd = program.command("serve").description("start Vibgrate AI Context \u2014 local-first MCP serving your code map, drift & version-correct docs to your AI").option("--http", "serve over streamable HTTP instead of stdio").option("--port <n>", "port for --http", "7437").option("--host <h>", "host for --http", "127.0.0.1").option("--savings", "record local, counts-only usage savings (opt-in; off by default)").option("--dedup", "collapse a node's heavy relation lists on repeat reads within a session (opt-in; saves tokens)").option("--no-refresh", "serve the map as built \u2014 skip the auto-rebuild when files change").action(async function(opts) {
|
|
833
|
+
const global = readGlobal(this);
|
|
834
|
+
const root = rootOf(global);
|
|
835
|
+
const graphPath = global.graph ?? defaultGraphPath(root);
|
|
836
|
+
const refresh = opts.refresh !== false && !global.graph;
|
|
837
|
+
const serveOpts = {
|
|
838
|
+
savings: opts.savings === true,
|
|
839
|
+
local: global.local === true,
|
|
840
|
+
dedup: opts.dedup === true,
|
|
841
|
+
refresh
|
|
842
|
+
};
|
|
843
|
+
if (!fs4.existsSync(graphPath)) {
|
|
844
|
+
throw new CliError(
|
|
845
|
+
`no map found at ${graphPath} \u2014 run \`vg\` to build one first`,
|
|
846
|
+
ExitCode.NOT_FOUND
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const freshness = refresh ? "auto-refresh" : "as built";
|
|
850
|
+
if (opts.http) {
|
|
851
|
+
await serveHttp(graphPath, opts.host ?? "127.0.0.1", Number(opts.port) || 7437, serveOpts, freshness);
|
|
852
|
+
} else {
|
|
853
|
+
info(c.dim(`vg \xB7 MCP server on stdio (read-only, ${freshness}). Connect your assistant to this process.`));
|
|
854
|
+
await serveStdio(graphPath, serveOpts);
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
applyGlobalOptions(cmd);
|
|
858
|
+
}
|
|
859
|
+
async function serveHttp(graphPath, host, port, opts, freshness) {
|
|
860
|
+
const { createServer: createHttp } = await import('http');
|
|
861
|
+
const { StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
862
|
+
const source = new GraphSource(graphPath, opts.refresh !== false);
|
|
863
|
+
const httpServer = createHttp(async (req, res) => {
|
|
864
|
+
if (req.url !== "/mcp") {
|
|
865
|
+
res.writeHead(404).end("not found");
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
try {
|
|
869
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
870
|
+
const server = createServer(source, opts);
|
|
871
|
+
res.on("close", () => {
|
|
872
|
+
void transport.close();
|
|
873
|
+
void server.close();
|
|
874
|
+
});
|
|
875
|
+
await server.connect(transport);
|
|
876
|
+
await transport.handleRequest(req, res);
|
|
877
|
+
} catch (err) {
|
|
878
|
+
if (!res.headersSent) res.writeHead(500).end("internal error");
|
|
879
|
+
info(c.red(`vg serve: request error: ${err.message}`));
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
await new Promise((resolve15) => httpServer.listen(port, host, resolve15));
|
|
883
|
+
info(c.dim(`vg \xB7 MCP server on http://${host}:${port}/mcp (read-only, local, ${freshness})`));
|
|
884
|
+
await new Promise(() => {
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/commands/install.ts
|
|
889
|
+
var SMALL_REPO_FILES = 150;
|
|
890
|
+
function registerInstall(program) {
|
|
891
|
+
const install = program.command("install").description("add vg to your AI assistant(s): skill + MCP + advisory nudge").argument("[tools...]", `assistant ids: ${ASSISTANTS.map((a) => a.id).join(", ")}`).option("--all", "install for every supported assistant").option("--list", "show the support matrix and exit").option("--no-hook", "skip the advisory nudge").action(function(tools, opts) {
|
|
892
|
+
const global = readGlobal(this);
|
|
893
|
+
const root = rootOf(global);
|
|
894
|
+
if (opts.list) {
|
|
895
|
+
if (global.json) {
|
|
896
|
+
json(ASSISTANTS.map((a) => ({ id: a.id, label: a.label, mcp: !!a.mcp, skill: !!a.skill, nudge: !!a.nudge })));
|
|
897
|
+
} else {
|
|
898
|
+
info(`${c.cyan("vg install")} \xB7 supported assistants`);
|
|
899
|
+
const pad3 = Math.max(...ASSISTANTS.map((a) => a.id.length)) + 2;
|
|
900
|
+
for (const a of ASSISTANTS) {
|
|
901
|
+
info(` ${c.bold(a.id.padEnd(pad3))} ${a.label} ${c.dim(`mcp:${a.mcp ? "\u2713" : "\u2014"} skill:${a.skill ? "\u2713" : "\u2014"} nudge:${a.nudge ? "\u2713" : "\u2014"}`)}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const targets = opts.all ? ASSISTANTS : tools.map(resolve5);
|
|
907
|
+
if (targets.length === 0) throw usageError("name an assistant (e.g. `vg install claude`) or use --all / --list");
|
|
908
|
+
const graph = loadGraph(root, global.graph);
|
|
909
|
+
const fileCount = graph ? graph.nodes.filter((n) => n.kind === "file").length : 0;
|
|
910
|
+
const smallRepo = graph !== null && fileCount > 0 && fileCount < SMALL_REPO_FILES;
|
|
911
|
+
const launch = detectServeLaunch();
|
|
912
|
+
const results = targets.map((a) => ({ id: a.id, ...installAssistant(a, { root, hook: opts.hook, smallRepo, launch }) }));
|
|
913
|
+
if (global.json) {
|
|
914
|
+
json({ root, smallRepo, launch: { command: launch.command, args: launch.args, note: launch.note ?? null }, results });
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
for (const r of results) {
|
|
918
|
+
info(`${c.green("\u2714")} ${c.bold(r.id)} \u2014 wrote ${r.wrote.join(", ")}${r.skipped.length ? c.dim(` (skipped ${r.skipped.join(", ")})`) : ""}`);
|
|
919
|
+
}
|
|
920
|
+
if (launch.note && results.some((r) => r.note)) info(`${c.yellow("!")} ${launch.note}`);
|
|
921
|
+
if (smallRepo) info(c.dim(` note: small repo (${fileCount} files) \u2014 nudge says searching is fine; vg is still used for impact/tests`));
|
|
922
|
+
info(c.dim(" run `vg serve` is wired via MCP; build the map with `vg` if you have not yet"));
|
|
923
|
+
});
|
|
924
|
+
applyGlobalOptions(install);
|
|
925
|
+
const uninstall = program.command("uninstall").description("remove vg from an AI assistant").argument("<tools...>", "assistant ids").option("--purge", "also delete the skill file").action(function(tools, opts) {
|
|
926
|
+
const global = readGlobal(this);
|
|
927
|
+
const root = rootOf(global);
|
|
928
|
+
const results = tools.map(resolve5).map((a) => ({ id: a.id, removed: uninstallAssistant(a, root, !!opts.purge) }));
|
|
929
|
+
if (global.json) {
|
|
930
|
+
json({ root, results });
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
for (const r of results) {
|
|
934
|
+
info(`${c.green("\u2714")} ${c.bold(r.id)} \u2014 removed ${r.removed.length ? r.removed.join(", ") : c.dim("nothing (not installed)")}`);
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
applyGlobalOptions(uninstall);
|
|
938
|
+
}
|
|
939
|
+
function resolve5(id) {
|
|
940
|
+
const a = assistantById(id);
|
|
941
|
+
if (!a) {
|
|
942
|
+
const near = ASSISTANTS.filter(
|
|
943
|
+
(x) => x.id.startsWith(id) || id.startsWith(x.id) || x.id.includes(id) || x.label.toLowerCase().includes(id.toLowerCase())
|
|
944
|
+
).map((x) => x.id);
|
|
945
|
+
const hint = near.length ? ` Did you mean: ${near.join(", ")}?` : "";
|
|
946
|
+
throw new CliError(
|
|
947
|
+
`unknown assistant "${id}".${hint} Supported: ${ASSISTANTS.map((x) => x.id).join(", ")} (see \`vg install --list\`)`,
|
|
948
|
+
ExitCode.USAGE_ERROR
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
return a;
|
|
952
|
+
}
|
|
953
|
+
var HOOK_BEGIN = "# vg:begin";
|
|
954
|
+
var HOOK_END = "# vg:end";
|
|
955
|
+
function registerShare(program) {
|
|
956
|
+
const cmd = program.command("share").description("make the map committable + auto-updating for your team (git)").option("--undo", "reverse what `vg share` installed").option("--reports", "also commit graph.html / GRAPH_REPORT.md (default: gitignored)").action(function(opts) {
|
|
957
|
+
const global = readGlobal(this);
|
|
958
|
+
const root = rootOf(global);
|
|
959
|
+
const gitDir = path9.join(root, ".git");
|
|
960
|
+
if (!fs4.existsSync(gitDir)) {
|
|
961
|
+
throw new CliError("not a git repository \u2014 run `vg share` inside a git repo", ExitCode.USAGE_ERROR);
|
|
962
|
+
}
|
|
963
|
+
if (opts.undo) {
|
|
964
|
+
undo(root);
|
|
965
|
+
info(`${c.cyan("vg share")} ${c.dim("--undo")} \xB7 removed hooks, merge driver, and attributes`);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
writeVibgrateGitignore(root, !!opts.reports);
|
|
969
|
+
writeGitAttributes(root);
|
|
970
|
+
configureMergeDriver(root);
|
|
971
|
+
installPreCommitHook(root);
|
|
972
|
+
info(`${c.cyan("vg share")} \xB7 the map is now committable + auto-updating`);
|
|
973
|
+
info(` ${c.green("\u2714")} .vibgrate/.gitignore (cache + volatile artifacts ignored, graph.json committed)`);
|
|
974
|
+
info(` ${c.green("\u2714")} pre-commit hook (rebuilds + stages graph.json only when it changed)`);
|
|
975
|
+
info(` ${c.green("\u2714")} deterministic merge driver for .vibgrate/graph.json (rebuild on conflict)`);
|
|
976
|
+
info(c.dim(" commit .vibgrate/graph.json so teammates inherit the map on pull"));
|
|
977
|
+
});
|
|
978
|
+
applyGlobalOptions(cmd);
|
|
979
|
+
}
|
|
980
|
+
function writeVibgrateGitignore(root, reports) {
|
|
981
|
+
const dir = path9.join(root, ".vibgrate");
|
|
982
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
983
|
+
const lines = ["# Managed by `vg share` \u2014 keep graph.json committed, ignore the rest", "cache/", "facts.jsonl"];
|
|
984
|
+
if (!reports) lines.push("graph.html", "GRAPH_REPORT.md");
|
|
985
|
+
fs4.writeFileSync(path9.join(dir, ".gitignore"), `${lines.join("\n")}
|
|
986
|
+
`);
|
|
987
|
+
}
|
|
988
|
+
function writeGitAttributes(root) {
|
|
989
|
+
const file = path9.join(root, ".gitattributes");
|
|
990
|
+
const line = ".vibgrate/graph.json merge=vg";
|
|
991
|
+
let content = fs4.existsSync(file) ? fs4.readFileSync(file, "utf8") : "";
|
|
992
|
+
if (!content.split("\n").some((l) => l.trim() === line)) {
|
|
993
|
+
content = content.length ? `${content.replace(/\s*$/, "")}
|
|
994
|
+
${line}
|
|
995
|
+
` : `${line}
|
|
996
|
+
`;
|
|
997
|
+
fs4.writeFileSync(file, content);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function configureMergeDriver(root) {
|
|
1001
|
+
git(root, ["config", "merge.vg.name", "vg deterministic graph rebuild"]);
|
|
1002
|
+
git(root, ["config", "merge.vg.driver", "vg build >/dev/null 2>&1; cp .vibgrate/graph.json %A"]);
|
|
1003
|
+
}
|
|
1004
|
+
function installPreCommitHook(root) {
|
|
1005
|
+
const hooksDir = hookDir(root);
|
|
1006
|
+
fs4.mkdirSync(hooksDir, { recursive: true });
|
|
1007
|
+
const file = path9.join(hooksDir, "pre-commit");
|
|
1008
|
+
const block = [
|
|
1009
|
+
HOOK_BEGIN,
|
|
1010
|
+
"if command -v vg >/dev/null 2>&1; then",
|
|
1011
|
+
" vg build >/dev/null 2>&1 || true",
|
|
1012
|
+
" git add .vibgrate/graph.json >/dev/null 2>&1 || true",
|
|
1013
|
+
"fi",
|
|
1014
|
+
HOOK_END
|
|
1015
|
+
].join("\n");
|
|
1016
|
+
let content = fs4.existsSync(file) ? fs4.readFileSync(file, "utf8") : "#!/bin/sh\n";
|
|
1017
|
+
const re = new RegExp(`${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}`);
|
|
1018
|
+
if (re.test(content)) content = content.replace(re, block);
|
|
1019
|
+
else content = `${content.replace(/\s*$/, "")}
|
|
1020
|
+
|
|
1021
|
+
${block}
|
|
1022
|
+
`;
|
|
1023
|
+
if (!content.startsWith("#!")) content = `#!/bin/sh
|
|
1024
|
+
${content}`;
|
|
1025
|
+
fs4.writeFileSync(file, content);
|
|
1026
|
+
fs4.chmodSync(file, 493);
|
|
1027
|
+
}
|
|
1028
|
+
function undo(root) {
|
|
1029
|
+
const hookFile = path9.join(hookDir(root), "pre-commit");
|
|
1030
|
+
if (fs4.existsSync(hookFile)) {
|
|
1031
|
+
const re = new RegExp(`\\n*${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}\\n*`);
|
|
1032
|
+
const next2 = fs4.readFileSync(hookFile, "utf8").replace(re, "\n");
|
|
1033
|
+
if (next2.trim() === "#!/bin/sh") fs4.rmSync(hookFile);
|
|
1034
|
+
else fs4.writeFileSync(hookFile, next2);
|
|
1035
|
+
}
|
|
1036
|
+
const attr = path9.join(root, ".gitattributes");
|
|
1037
|
+
if (fs4.existsSync(attr)) {
|
|
1038
|
+
const next2 = fs4.readFileSync(attr, "utf8").split("\n").filter((l) => l.trim() !== ".vibgrate/graph.json merge=vg").join("\n");
|
|
1039
|
+
fs4.writeFileSync(attr, next2);
|
|
1040
|
+
}
|
|
1041
|
+
try {
|
|
1042
|
+
git(root, ["config", "--remove-section", "merge.vg"]);
|
|
1043
|
+
} catch {
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function hookDir(root) {
|
|
1047
|
+
try {
|
|
1048
|
+
const configured = git(root, ["config", "--get", "core.hooksPath"]).trim();
|
|
1049
|
+
if (configured) return path9.resolve(root, configured);
|
|
1050
|
+
} catch {
|
|
1051
|
+
}
|
|
1052
|
+
return path9.join(root, ".git", "hooks");
|
|
1053
|
+
}
|
|
1054
|
+
function git(root, args) {
|
|
1055
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8" });
|
|
1056
|
+
}
|
|
1057
|
+
function escapeRe(s) {
|
|
1058
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1059
|
+
}
|
|
1060
|
+
var PIN = "2020-01-01T00:00:00.000Z";
|
|
1061
|
+
function registerBenchmark(program) {
|
|
1062
|
+
const cmd = program.command("benchmark").description("reproducible build + token-reduction benchmark (honest estimates)").option("--budget <n>", "token budget for vg answers", "2000").action(async function(opts) {
|
|
1063
|
+
const global = readGlobal(this);
|
|
1064
|
+
const root = rootOf(global);
|
|
1065
|
+
const budget = Number(opts.budget) || 2e3;
|
|
1066
|
+
const t0 = now();
|
|
1067
|
+
const cold = await buildGraph({ root, generatedAt: PIN, noCache: true });
|
|
1068
|
+
const coldMs = now() - t0;
|
|
1069
|
+
const t1 = now();
|
|
1070
|
+
const warm = await buildGraph({ root, generatedAt: PIN });
|
|
1071
|
+
const warmMs = now() - t1;
|
|
1072
|
+
const a = serializeGraph(cold.graph);
|
|
1073
|
+
const b = serializeGraph((await buildGraph({ root, generatedAt: PIN, noCache: true })).graph);
|
|
1074
|
+
const deterministic = a === b;
|
|
1075
|
+
const questions = deriveQuestions(cold.graph);
|
|
1076
|
+
const fileSizes = fileSizeIndex(root);
|
|
1077
|
+
const perQuestion = questions.map((q) => {
|
|
1078
|
+
const vgTokens = queryGraph(cold.graph, q, { budget }).tokensEstimate;
|
|
1079
|
+
const baseTokens = grepBaselineTokens(q, fileSizes);
|
|
1080
|
+
return { question: q, vgTokens, baselineTokens: baseTokens, ratio: baseTokens > 0 ? round(baseTokens / Math.max(vgTokens, 1)) : 1 };
|
|
1081
|
+
});
|
|
1082
|
+
const totalVg = perQuestion.reduce((s, x) => s + x.vgTokens, 0);
|
|
1083
|
+
const totalBase = perQuestion.reduce((s, x) => s + x.baselineTokens, 0);
|
|
1084
|
+
const aggregateRatio = totalVg > 0 ? round(totalBase / totalVg) : 0;
|
|
1085
|
+
const result = {
|
|
1086
|
+
repo: { files: cold.totalFiles, nodes: cold.graph.meta.counts.nodes, edges: cold.graph.meta.counts.edges },
|
|
1087
|
+
build: { coldMs: round(coldMs), incrementalMs: round(warmMs), reusedOnWarm: warm.reused },
|
|
1088
|
+
determinism: { byteIdentical: deterministic },
|
|
1089
|
+
tokenReduction: { questions: perQuestion, aggregateRatio, note: "estimates (~4 chars/token); scales with repo size" }
|
|
1090
|
+
};
|
|
1091
|
+
if (global.json) {
|
|
1092
|
+
json(result);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
info(`${c.cyan("vg benchmark")} \xB7 ${result.repo.files} files \xB7 ${result.repo.nodes} nodes`);
|
|
1096
|
+
info(` build cold ${result.build.coldMs.toFixed(0)}ms \xB7 incremental ${result.build.incrementalMs.toFixed(0)}ms (reused ${warm.reused})`);
|
|
1097
|
+
info(` determinism ${deterministic ? c.green("byte-identical \u2713") : c.red("NON-DETERMINISTIC \u2717")}`);
|
|
1098
|
+
if (perQuestion.length) {
|
|
1099
|
+
info(` token reduction vs grep/read baseline (${c.dim("estimates")}):`);
|
|
1100
|
+
for (const q of perQuestion) {
|
|
1101
|
+
info(` ${pad2(`${q.ratio.toFixed(1)}\xD7`, 6)} ${c.dim(`vg ${q.vgTokens} vs ~${q.baselineTokens}`)} "${q.question}"`);
|
|
1102
|
+
}
|
|
1103
|
+
info(` ${c.bold(`aggregate \u2248 ${aggregateRatio.toFixed(1)}\xD7 fewer tokens`)} ${c.dim("(honest estimate, scales with repo size)")}`);
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
applyGlobalOptions(cmd);
|
|
1107
|
+
}
|
|
1108
|
+
function deriveQuestions(graph) {
|
|
1109
|
+
return graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external").sort((a, b) => b.importance - a.importance || a.qualifiedName.localeCompare(b.qualifiedName)).slice(0, 5).map((n) => n.name);
|
|
1110
|
+
}
|
|
1111
|
+
function fileSizeIndex(root) {
|
|
1112
|
+
return discover({ root }).map((f) => ({ rel: f.rel, abs: f.abs, size: safeSize(f.abs) }));
|
|
1113
|
+
}
|
|
1114
|
+
function grepBaselineTokens(term, files) {
|
|
1115
|
+
const lower = term.toLowerCase();
|
|
1116
|
+
let bytes = 0;
|
|
1117
|
+
for (const f of files) {
|
|
1118
|
+
if (f.size > 512 * 1024) continue;
|
|
1119
|
+
let content = "";
|
|
1120
|
+
try {
|
|
1121
|
+
content = fs4.readFileSync(f.abs, "utf8");
|
|
1122
|
+
} catch {
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (content.toLowerCase().includes(lower)) bytes += content.length;
|
|
1126
|
+
}
|
|
1127
|
+
return Math.ceil(bytes / 4);
|
|
1128
|
+
}
|
|
1129
|
+
function safeSize(abs) {
|
|
1130
|
+
try {
|
|
1131
|
+
return fs4.statSync(abs).size;
|
|
1132
|
+
} catch {
|
|
1133
|
+
return 0;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
function now() {
|
|
1137
|
+
return Number(process.hrtime.bigint() / 1000n) / 1e3;
|
|
1138
|
+
}
|
|
1139
|
+
function round(x) {
|
|
1140
|
+
return Math.round(x * 1e3) / 1e3;
|
|
1141
|
+
}
|
|
1142
|
+
function pad2(s, n) {
|
|
1143
|
+
return s.padStart(n, " ");
|
|
1144
|
+
}
|
|
1145
|
+
function registerTests(program) {
|
|
1146
|
+
const cmd = program.command("tests").description("which tests cover a node (call/coverage linkage)").argument("<name>", "node to inspect").option("--missing", "show untested nodes nearby instead").option("--run", "print the command to run exactly these tests").option("--exec", "run that command").option("--pick <n>", "pick the nth candidate when ambiguous").action(function(name, opts) {
|
|
1147
|
+
const global = readGlobal(this);
|
|
1148
|
+
const { root, graph } = requireGraph(global);
|
|
1149
|
+
const { node, candidates } = resolveOne(graph, name, opts.pick ? Number(opts.pick) : void 0);
|
|
1150
|
+
if (!node) throw ambiguityError(`"${name}" ${candidates.length ? "is ambiguous" : "not found"}`, candidates);
|
|
1151
|
+
if (opts.missing) {
|
|
1152
|
+
const nearby = graph.nodes.filter((n) => n.file === node.file && (n.kind === "function" || n.kind === "method") && n.tested === false).sort((a, b) => b.importance - a.importance);
|
|
1153
|
+
if (global.json) {
|
|
1154
|
+
json({ file: node.file, untested: nearby.map((n) => ({ name: n.qualifiedName, line: n.span.start })) });
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
info(`${c.cyan("vg tests --missing")} \xB7 untested in ${node.file} (${nearby.length})`);
|
|
1158
|
+
for (const n of nearby) info(` ${c.yellow("\u25CB")} ${n.qualifiedName} ${c.dim(`:${n.span.start}`)}`);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const covers = coveringTests(graph, node);
|
|
1162
|
+
const testFiles = covers.filter((c2) => c2.basis === "call").map((c2) => c2.file);
|
|
1163
|
+
if (opts.run || opts.exec) {
|
|
1164
|
+
const runner = detectRunner(rootOf(global), node.lang);
|
|
1165
|
+
const command = runner.command(testFiles);
|
|
1166
|
+
if (opts.exec) {
|
|
1167
|
+
info(c.dim(`$ ${command}`));
|
|
1168
|
+
execSync(command, { cwd: root, stdio: "inherit" });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
if (global.json) json({ runner: runner.name, command, testFiles });
|
|
1172
|
+
else info(command);
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (global.json) {
|
|
1176
|
+
json({ node: node.qualifiedName, tested: node.tested, coverage: node.coverage ?? null, covers });
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
info(`${c.cyan("vg tests")} \xB7 ${c.bold(node.qualifiedName)} ${node.tested ? c.green("tested") : c.yellow("untested")}`);
|
|
1180
|
+
if (typeof node.coverage === "number") info(c.dim(` line coverage ${(node.coverage * 100).toFixed(0)}%`));
|
|
1181
|
+
if (covers.length === 0) {
|
|
1182
|
+
info(c.dim(" no covering tests found"));
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
for (const t of covers) {
|
|
1186
|
+
info(` ${c.green("\u2714")} ${t.file} ${c.dim(`(${t.basis}, conf ${t.confidence.toFixed(2)})`)}`);
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
applyGlobalOptions(cmd);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/commands/facts.ts
|
|
1193
|
+
function registerFacts(program) {
|
|
1194
|
+
const cmd = program.command("facts").description("the deterministic facts for a node (contract / invariant / characterization)").argument("<name>", "node to inspect").option("--pick <n>", "pick the nth candidate when ambiguous").action(function(name, opts) {
|
|
1195
|
+
const global = readGlobal(this);
|
|
1196
|
+
const { graph } = requireGraph(global);
|
|
1197
|
+
const { node, candidates } = resolveOne(graph, name, opts.pick ? Number(opts.pick) : void 0);
|
|
1198
|
+
if (!node) throw ambiguityError(`"${name}" ${candidates.length ? "is ambiguous" : "not found"}`, candidates);
|
|
1199
|
+
if (!graph.facts) {
|
|
1200
|
+
if (global.json) json({ node: node.qualifiedName, facts: [], note: "facts require a --deep build" });
|
|
1201
|
+
else info(`${c.cyan("vg facts")} \xB7 run ${c.bold("vg --deep")} to derive facts (none in this map)`);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const facts = graph.facts.filter((f) => f.subjectIds.includes(node.id));
|
|
1205
|
+
if (global.json) {
|
|
1206
|
+
json({ node: node.qualifiedName, facts });
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
info(`${c.cyan("vg facts")} \xB7 ${c.bold(node.qualifiedName)} (${facts.length})`);
|
|
1210
|
+
if (facts.length === 0) {
|
|
1211
|
+
info(c.dim(" no facts derived for this node"));
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
for (const f of facts) {
|
|
1215
|
+
info(` ${c.bold(f.kind)} ${c.dim(`[${f.derivedBy}\u2192${f.confidence}]`)}: ${summarize(f.predicate)}`);
|
|
1216
|
+
}
|
|
1217
|
+
});
|
|
1218
|
+
applyGlobalOptions(cmd);
|
|
1219
|
+
}
|
|
1220
|
+
function summarize(predicate) {
|
|
1221
|
+
if (predicate && typeof predicate === "object") {
|
|
1222
|
+
const o = predicate;
|
|
1223
|
+
if (typeof o.signature === "string") return o.signature;
|
|
1224
|
+
if (typeof o.guard === "string") return `guards: ${o.guard}`;
|
|
1225
|
+
if (Array.isArray(o.pinnedBy)) return `pinned by ${o.pinnedBy.join(", ")}`;
|
|
1226
|
+
}
|
|
1227
|
+
return JSON.stringify(predicate);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/commands/guide.ts
|
|
1231
|
+
function registerGuide(program) {
|
|
1232
|
+
const cmd = program.command("guide").description("cited relevant standards/practices for a node (free pack)").argument("<name>", "node to inspect").option("--pick <n>", "pick the nth candidate when ambiguous").action(function(name, opts) {
|
|
1233
|
+
const global = readGlobal(this);
|
|
1234
|
+
const { graph } = requireGraph(global);
|
|
1235
|
+
const { node, candidates } = resolveOne(graph, name, opts.pick ? Number(opts.pick) : void 0);
|
|
1236
|
+
if (!node) throw ambiguityError(`"${name}" ${candidates.length ? "is ambiguous" : "not found"}`, candidates);
|
|
1237
|
+
const entries = new Map(FREE_PACK.entries.map((e) => [e.id, e]));
|
|
1238
|
+
const grounding = (graph.grounding ?? []).filter((g) => g.src === node.id);
|
|
1239
|
+
if (global.json) {
|
|
1240
|
+
json({
|
|
1241
|
+
node: node.qualifiedName,
|
|
1242
|
+
guidance: grounding.map((g) => ({
|
|
1243
|
+
kind: g.kind,
|
|
1244
|
+
rationale: g.rationale,
|
|
1245
|
+
confidence: g.confidence,
|
|
1246
|
+
topic: entries.get(g.packEntryId)?.topic ?? g.packEntryId,
|
|
1247
|
+
summary: entries.get(g.packEntryId)?.summary ?? "",
|
|
1248
|
+
citation: g.citation
|
|
1249
|
+
}))
|
|
1250
|
+
});
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
info(`${c.cyan("vg guide")} \xB7 ${c.bold(node.qualifiedName)} (${grounding.length})`);
|
|
1254
|
+
if (grounding.length === 0) {
|
|
1255
|
+
info(c.dim(" no matching guidance (no security-relevant signals detected)"));
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
for (const g of grounding) {
|
|
1259
|
+
const entry = entries.get(g.packEntryId);
|
|
1260
|
+
const tag = g.rationale === "recommended" ? c.green(g.kind) : c.yellow(g.kind);
|
|
1261
|
+
info(` ${tag} ${c.dim(`(${g.rationale}, conf ${g.confidence.toFixed(2)})`)}`);
|
|
1262
|
+
if (entry) {
|
|
1263
|
+
info(` ${entry.summary}`);
|
|
1264
|
+
info(c.dim(` ${entry.citation.title} \u2014 ${entry.citation.url}`));
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
applyGlobalOptions(cmd);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/commands/why.ts
|
|
1272
|
+
function registerWhy(program) {
|
|
1273
|
+
const cmd = program.command("why").description("who introduced a dependency (and any open vulnerabilities), from git history").argument("<package>", "package name to explain").action(async function(pkg) {
|
|
1274
|
+
const global = readGlobal(this);
|
|
1275
|
+
const root = rootOf(global);
|
|
1276
|
+
if (!await gitHistoryAvailable(root)) {
|
|
1277
|
+
throw new CliError(
|
|
1278
|
+
"git history is required for `vg why` (not a git repository, or git is unavailable)",
|
|
1279
|
+
ExitCode.USAGE_ERROR
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
const timelines = await buildVersionTimelines(root);
|
|
1283
|
+
const pt = timelines ? findPackageAnyEcosystem(timelines, pkg) ?? null : null;
|
|
1284
|
+
const artifact = readScanArtifact(root);
|
|
1285
|
+
const vulnPkg = artifact?.extended?.vulnerabilities?.packages.find((p) => p.package === pkg) ?? null;
|
|
1286
|
+
if (global.json) {
|
|
1287
|
+
json({ package: pkg, history: pt?.changes ?? [], vulnerabilities: vulnPkg?.advisories ?? [] });
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
info(`${c.cyan("vg why")} ${c.bold(pkg)}`);
|
|
1291
|
+
if (!pt || pt.changes.length === 0) {
|
|
1292
|
+
info(c.dim(" no git history found for this package (npm lockfile). It may not be an npm dependency,"));
|
|
1293
|
+
info(c.dim(" or the lockfile predates its introduction."));
|
|
1294
|
+
} else {
|
|
1295
|
+
for (let i = 0; i < pt.changes.length; i++) {
|
|
1296
|
+
const ch = pt.changes[i];
|
|
1297
|
+
const verb = i === 0 ? c.green("added") : c.yellow("\u2192");
|
|
1298
|
+
const date = ch.commit.date.slice(0, 10);
|
|
1299
|
+
info(
|
|
1300
|
+
` ${verb} ${c.bold(ch.version)} ${c.dim(`${ch.commit.shortSha} ${date} ${ch.commit.authorName}`)}`
|
|
1301
|
+
);
|
|
1302
|
+
info(c.dim(` ${ch.commit.subject}`));
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (vulnPkg && vulnPkg.advisories.length) {
|
|
1306
|
+
info("");
|
|
1307
|
+
info(c.red(` ${vulnPkg.advisories.length} open vulnerabilit${vulnPkg.advisories.length === 1 ? "y" : "ies"} at ${pkg}@${vulnPkg.version}:`));
|
|
1308
|
+
for (const adv of vulnPkg.advisories) {
|
|
1309
|
+
const cve = adv.aliases.find((a) => a.startsWith("CVE-"));
|
|
1310
|
+
const idLabel = cve && cve !== adv.id ? `${adv.id} (${cve})` : adv.id;
|
|
1311
|
+
const cvss = adv.cvss != null ? ` cvss ${adv.cvss}` : "";
|
|
1312
|
+
const fixed = adv.fixedVersions.length ? ` \u2014 fixed in ${adv.fixedVersions.join(", ")}` : " \u2014 no fix available";
|
|
1313
|
+
info(` ${severityTag(adv.severity)} ${idLabel}${c.dim(cvss)}${c.dim(fixed)}`);
|
|
1314
|
+
if (adv.introduced) {
|
|
1315
|
+
const exposure = adv.exposureDays != null ? `, ${adv.exposureDays}d exposed` : "";
|
|
1316
|
+
info(
|
|
1317
|
+
c.dim(
|
|
1318
|
+
` introduced by ${adv.introduced.authorName} in ${adv.introduced.shortSha} on ${adv.introduced.date.slice(0, 10)}${exposure}`
|
|
1319
|
+
)
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
} else if (artifact?.extended?.vulnerabilities) {
|
|
1324
|
+
info("");
|
|
1325
|
+
info(c.green(" no known vulnerabilities for this package in the last scan"));
|
|
1326
|
+
} else {
|
|
1327
|
+
info("");
|
|
1328
|
+
info(c.dim(" run `vg scan --vulns` to check this package for known vulnerabilities"));
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
applyGlobalOptions(cmd);
|
|
1332
|
+
}
|
|
1333
|
+
function severityTag(severity) {
|
|
1334
|
+
switch (severity) {
|
|
1335
|
+
case "critical":
|
|
1336
|
+
return c.red("critical");
|
|
1337
|
+
case "high":
|
|
1338
|
+
return c.red("high");
|
|
1339
|
+
case "moderate":
|
|
1340
|
+
return c.yellow("moderate");
|
|
1341
|
+
case "low":
|
|
1342
|
+
return c.dim("low");
|
|
1343
|
+
default:
|
|
1344
|
+
return c.dim(severity);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/commands/bisect.ts
|
|
1349
|
+
function registerBisect(program) {
|
|
1350
|
+
const cmd = program.command("bisect").description("pinpoint the commit where a dependency crossed a version line, from git history").argument("<package>", "package name to bisect").argument("<constraint>", 'target version or semver range (a bare version means ">=" that version)').option("--assert", "exit non-zero when the current version does not satisfy the constraint (CI gate)").action(async function(pkg, constraintArg) {
|
|
1351
|
+
const global = readGlobal(this);
|
|
1352
|
+
const opts = this.opts();
|
|
1353
|
+
const root = rootOf(global);
|
|
1354
|
+
if (!await gitHistoryAvailable(root)) {
|
|
1355
|
+
throw new CliError(
|
|
1356
|
+
"git history is required for `vg bisect` (not a git repository, or git is unavailable)",
|
|
1357
|
+
ExitCode.USAGE_ERROR
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
const range = normalizeConstraint(constraintArg);
|
|
1361
|
+
if (!range) {
|
|
1362
|
+
throw new CliError(
|
|
1363
|
+
`not a valid version or semver range: ${constraintArg}`,
|
|
1364
|
+
ExitCode.USAGE_ERROR
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
const timelines = await buildVersionTimelines(root);
|
|
1368
|
+
const pt = timelines ? findPackageAnyEcosystem(timelines, pkg) ?? null : null;
|
|
1369
|
+
if (!pt || pt.changes.length === 0) {
|
|
1370
|
+
if (global.json) {
|
|
1371
|
+
json({ package: pkg, constraint: range, found: false, satisfiedNow: false, crossings: [] });
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
info(`${c.cyan("vg bisect")} ${c.bold(pkg)} ${c.dim(range)}`);
|
|
1375
|
+
info(c.dim(" no git history found for this package (lockfile). It may not be a resolved"));
|
|
1376
|
+
info(c.dim(" dependency in a supported ecosystem, or the lockfile predates its introduction."));
|
|
1377
|
+
throw new CliError(`no version history for ${pkg}`, ExitCode.NOT_FOUND);
|
|
1378
|
+
}
|
|
1379
|
+
const crossings = findVersionCrossings(pt, range);
|
|
1380
|
+
const current = pt.changes[pt.changes.length - 1];
|
|
1381
|
+
const satisfiedNow = versionSatisfies(current.version, range);
|
|
1382
|
+
const firstEntered = crossings.find((cr) => cr.kind === "entered") ?? null;
|
|
1383
|
+
if (global.json) {
|
|
1384
|
+
json({
|
|
1385
|
+
package: pt.name,
|
|
1386
|
+
ecosystem: pt.ecosystem,
|
|
1387
|
+
constraint: range,
|
|
1388
|
+
currentVersion: current.version,
|
|
1389
|
+
satisfiedNow,
|
|
1390
|
+
firstEntered,
|
|
1391
|
+
crossings
|
|
1392
|
+
});
|
|
1393
|
+
} else {
|
|
1394
|
+
info(`${c.cyan("vg bisect")} ${c.bold(pt.name)} ${c.dim(range)}`);
|
|
1395
|
+
const verdict = satisfiedNow ? c.green(`current ${current.version} satisfies ${range}`) : c.yellow(`current ${current.version} does not satisfy ${range}`);
|
|
1396
|
+
info(` ${verdict}`);
|
|
1397
|
+
info("");
|
|
1398
|
+
if (!firstEntered) {
|
|
1399
|
+
info(c.dim(` never crossed ${range} in git history`));
|
|
1400
|
+
info(
|
|
1401
|
+
c.dim(
|
|
1402
|
+
` latest in history: ${current.version} ${c.dim(`(${current.commit.shortSha} ${current.commit.date.slice(0, 10)})`)}`
|
|
1403
|
+
)
|
|
1404
|
+
);
|
|
1405
|
+
} else {
|
|
1406
|
+
info(` ${c.bold("first reached")} ${range}:`);
|
|
1407
|
+
printCrossing(firstEntered, true);
|
|
1408
|
+
const rest = crossings.slice(crossings.indexOf(firstEntered) + 1);
|
|
1409
|
+
if (rest.length) {
|
|
1410
|
+
info("");
|
|
1411
|
+
info(c.dim(` ${rest.length} later crossing${rest.length === 1 ? "" : "s"}:`));
|
|
1412
|
+
for (const cr of rest) printCrossing(cr, false);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if (opts.assert && !satisfiedNow) {
|
|
1417
|
+
throw new CliError(
|
|
1418
|
+
`${pt.name}@${current.version} does not satisfy ${range}`,
|
|
1419
|
+
ExitCode.GATE_FAILED
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
});
|
|
1423
|
+
applyGlobalOptions(cmd);
|
|
1424
|
+
}
|
|
1425
|
+
function printCrossing(cr, primary) {
|
|
1426
|
+
const arrow = cr.kind === "entered" ? c.green("\u2192") : c.yellow("\u2193");
|
|
1427
|
+
const date = cr.commit.date.slice(0, 10);
|
|
1428
|
+
const from = cr.previousVersion ? c.dim(` from ${cr.previousVersion}`) : c.dim(" (added)");
|
|
1429
|
+
const ver = primary ? c.bold(cr.version) : cr.version;
|
|
1430
|
+
info(` ${arrow} ${ver}${from} ${c.dim(`${cr.commit.shortSha} ${date} ${cr.commit.authorName}`)}`);
|
|
1431
|
+
info(c.dim(` ${cr.commit.subject}`));
|
|
1432
|
+
}
|
|
1433
|
+
var STANDARDS_FILES = [".vibgrate/standards.json", "vibgrate.standards.json"];
|
|
1434
|
+
function asString(v) {
|
|
1435
|
+
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
1436
|
+
}
|
|
1437
|
+
function normalizeStandards(raw) {
|
|
1438
|
+
const list = Array.isArray(raw) ? raw : Array.isArray(raw?.banned) ? raw.banned : [];
|
|
1439
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1440
|
+
const banned = [];
|
|
1441
|
+
for (const item of list) {
|
|
1442
|
+
if (!item || typeof item !== "object") continue;
|
|
1443
|
+
const r = item;
|
|
1444
|
+
const name = asString(r.name);
|
|
1445
|
+
if (!name) continue;
|
|
1446
|
+
const ecosystem = asString(r.ecosystem)?.toLowerCase();
|
|
1447
|
+
const key = `${ecosystem ?? "*"}\0${name.toLowerCase()}`;
|
|
1448
|
+
if (seen.has(key)) continue;
|
|
1449
|
+
seen.add(key);
|
|
1450
|
+
banned.push({ name, ...ecosystem ? { ecosystem } : {}, ...asString(r.use) ? { use: asString(r.use) } : {}, ...asString(r.reason) ? { reason: asString(r.reason) } : {} });
|
|
1451
|
+
}
|
|
1452
|
+
return { banned };
|
|
1453
|
+
}
|
|
1454
|
+
function loadStandards(root) {
|
|
1455
|
+
for (const rel2 of STANDARDS_FILES) {
|
|
1456
|
+
const p = path9__default.join(root, rel2);
|
|
1457
|
+
let text;
|
|
1458
|
+
try {
|
|
1459
|
+
text = fs4__default.readFileSync(p, "utf8");
|
|
1460
|
+
} catch {
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
try {
|
|
1464
|
+
return { policy: normalizeStandards(JSON.parse(text)), path: p };
|
|
1465
|
+
} catch {
|
|
1466
|
+
return { policy: null, path: p };
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return { policy: null, path: null };
|
|
1470
|
+
}
|
|
1471
|
+
function checkStandards(policy, records) {
|
|
1472
|
+
const out2 = [];
|
|
1473
|
+
for (const r of records) {
|
|
1474
|
+
const rule = policy.banned.find((b) => b.name.toLowerCase() === r.name.toLowerCase() && (!b.ecosystem || b.ecosystem === r.ecosystem));
|
|
1475
|
+
if (rule) out2.push({ ecosystem: r.ecosystem, name: r.name, installed: r.installed ?? null, use: rule.use ?? null, reason: rule.reason ?? null });
|
|
1476
|
+
}
|
|
1477
|
+
return out2.sort((a, b) => a.ecosystem.localeCompare(b.ecosystem) || a.name.localeCompare(b.name));
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// src/commands/drift.ts
|
|
1481
|
+
function registerDrift(program) {
|
|
1482
|
+
const cmd = program.command("drift").description("what is outdated across dependencies (offline; --online for currency)").option("--online", "query the npm registry for latest versions (opt-in network)").option("--fail-on <level>", "exit 2 on violations \u2014 major|minor (version drift) or standards (enterprise policy)").action(async function(opts) {
|
|
1483
|
+
const global = readGlobal(this);
|
|
1484
|
+
const root = rootOf(global);
|
|
1485
|
+
if (opts.online && global.local) {
|
|
1486
|
+
throw new CliError("--online conflicts with --local (no network in local mode)", ExitCode.USAGE_ERROR);
|
|
1487
|
+
}
|
|
1488
|
+
if (opts.failOn && !["major", "minor", "standards"].includes(opts.failOn)) {
|
|
1489
|
+
throw new CliError(`--fail-on must be one of: major, minor, standards (got "${opts.failOn}")`, ExitCode.USAGE_ERROR);
|
|
1490
|
+
}
|
|
1491
|
+
const inv = inventory(root);
|
|
1492
|
+
if (opts.online && !global.local) await enrichOnline(inv.records);
|
|
1493
|
+
let standards = null;
|
|
1494
|
+
if (opts.failOn === "standards") {
|
|
1495
|
+
const loaded = loadStandards(root);
|
|
1496
|
+
if (loaded.path && !loaded.policy) {
|
|
1497
|
+
throw new CliError(`standards policy at ${loaded.path} is not valid JSON`, ExitCode.USAGE_ERROR);
|
|
1498
|
+
}
|
|
1499
|
+
standards = { policyPath: loaded.path, defined: !!loaded.policy, violations: loaded.policy ? checkStandards(loaded.policy, inv.records) : [] };
|
|
1500
|
+
}
|
|
1501
|
+
if (global.json) {
|
|
1502
|
+
json(standards ? { ...inv, standards } : inv);
|
|
1503
|
+
} else {
|
|
1504
|
+
const breakdown = ECOSYSTEMS.filter((e) => inv.counts[e] > 0).map((e) => `${e} ${inv.counts[e]}`).join(" \xB7 ");
|
|
1505
|
+
info(`${c.cyan("vg drift")} \xB7 ${inv.counts.total} dependencies${breakdown ? ` (${breakdown})` : ""}`);
|
|
1506
|
+
if (opts.online) {
|
|
1507
|
+
const drifted = inv.records.filter((r) => r.drift && r.drift !== "current" && r.drift !== "unknown");
|
|
1508
|
+
if (drifted.length === 0) info(c.green(" all checked dependencies are current"));
|
|
1509
|
+
for (const r of drifted.sort((a, b) => rank(b.drift) - rank(a.drift))) {
|
|
1510
|
+
const tag = r.drift === "major" ? c.red("major") : r.drift === "minor" ? c.yellow("minor") : c.dim("patch");
|
|
1511
|
+
info(` ${tag} ${r.name} ${c.dim(`${r.installed} \u2192 ${r.latest}`)}`);
|
|
1512
|
+
}
|
|
1513
|
+
} else {
|
|
1514
|
+
info(c.dim(" offline inventory \u2014 run with --online for currency vs the npm registry"));
|
|
1515
|
+
info(c.dim(" CVE/EOL/governance: the Vibgrate platform (vg push)"));
|
|
1516
|
+
}
|
|
1517
|
+
if (standards) printStandards(standards);
|
|
1518
|
+
}
|
|
1519
|
+
if (standards && standards.violations.length) {
|
|
1520
|
+
throw new CliError(`${standards.violations.length} banned dependency(ies) violate standards`, ExitCode.GATE_FAILED);
|
|
1521
|
+
}
|
|
1522
|
+
if (opts.failOn && opts.failOn !== "standards") {
|
|
1523
|
+
const threshold = opts.failOn === "minor" ? 2 : 3;
|
|
1524
|
+
const bad = inv.records.filter((r) => rank(r.drift) >= threshold);
|
|
1525
|
+
if (bad.length) {
|
|
1526
|
+
throw new CliError(`${bad.length} dependency(ies) drift at/above ${opts.failOn}`, ExitCode.GATE_FAILED);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
applyGlobalOptions(cmd);
|
|
1531
|
+
}
|
|
1532
|
+
function rank(drift) {
|
|
1533
|
+
return drift === "major" ? 3 : drift === "minor" ? 2 : drift === "patch" ? 1 : 0;
|
|
1534
|
+
}
|
|
1535
|
+
function printStandards(s) {
|
|
1536
|
+
if (!s.defined) {
|
|
1537
|
+
info(c.dim(` no standards policy (${STANDARDS_FILES.join(" or ")}) \u2014 nothing to enforce`));
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
if (s.violations.length === 0) {
|
|
1541
|
+
info(c.green(" standards: no banned dependencies in use"));
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
info(c.red(` standards: ${s.violations.length} banned dependency(ies)`));
|
|
1545
|
+
for (const v of s.violations) {
|
|
1546
|
+
const fix = v.use ? c.dim(` \u2192 use ${v.use}`) : "";
|
|
1547
|
+
const why = v.reason ? c.dim(` (${v.reason})`) : "";
|
|
1548
|
+
info(` ${c.red("banned")} ${v.ecosystem}:${v.name}${v.installed ? c.dim(` ${v.installed}`) : ""}${fix}${why}`);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
// src/commands/models.ts
|
|
1553
|
+
function registerModels(program) {
|
|
1554
|
+
const cmd = program.command("models").description("the local model fleet (Ollama / LM Studio / gguf), discovered offline").action(function() {
|
|
1555
|
+
const global = readGlobal(this);
|
|
1556
|
+
const models = discoverModels();
|
|
1557
|
+
if (global.json) {
|
|
1558
|
+
json({ models });
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
info(`${c.cyan("vg models")} \xB7 ${models.length} local model(s) discovered`);
|
|
1562
|
+
if (models.length === 0) {
|
|
1563
|
+
info(c.dim(" none found (looked in ~/.ollama, ~/.lmstudio, ~/models, ~/.cache)"));
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
for (const m of models) info(` ${c.bold(m.runtime.padEnd(10))} ${m.name}`);
|
|
1567
|
+
});
|
|
1568
|
+
applyGlobalOptions(cmd);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// src/commands/savings.ts
|
|
1572
|
+
function registerSavings(program) {
|
|
1573
|
+
const cmd = program.command("savings").description("local, privacy-safe report of tokens/$ saved vs a grep baseline (estimates)").option("--days <n>", "window in days", "30").action(function() {
|
|
1574
|
+
const global = readGlobal(this);
|
|
1575
|
+
const days = Number(this.opts().days) || 30;
|
|
1576
|
+
const report = readSavings(rootOf(global), days, Date.now());
|
|
1577
|
+
if (global.json) {
|
|
1578
|
+
json(report);
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
info(`${c.cyan("vg savings")} \xB7 last ${days} days ${c.dim("(local, nothing left your machine)")}`);
|
|
1582
|
+
if (!report.enabled && report.queries === 0) {
|
|
1583
|
+
info(c.dim(" recording is off. Enable with `vg serve --savings`, then query via MCP."));
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
if (report.queries === 0) {
|
|
1587
|
+
info(c.dim(" no queries recorded yet in this window"));
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
info(` queries ${report.queries} \xB7 context tokens ${fmt(report.vgTokens)} (grep/read baseline \u2248 ${fmt(report.baselineTokens)}) \u2192 ${report.ratio}\xD7 fewer`);
|
|
1591
|
+
info(` est. cost (${report.rateLabel}): $${report.estCostVg} vs $${report.estCostBaseline} \u2192 saved \u2248 $${report.saved}`);
|
|
1592
|
+
info(c.dim(" estimates with stated assumptions (~4 chars/token; ~400 tokens/file baseline); scales with repo size."));
|
|
1593
|
+
});
|
|
1594
|
+
applyGlobalOptions(cmd);
|
|
1595
|
+
}
|
|
1596
|
+
function fmt(n) {
|
|
1597
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
|
|
1598
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
1599
|
+
return String(n);
|
|
1600
|
+
}
|
|
1601
|
+
function registerLib(program) {
|
|
1602
|
+
const cmd = program.command("lib").description("library currency: version-correct, drift-annotated usage docs").argument("[args...]", "a library name, or: add <source> | publish <name> | resolve <name> | refresh").option("--name <name>", "library name for `add`").option("--version <v>", "pin the doc version for `add`/`publish`").option("--online", "(deprecated; network is on by default) allow network for add/refresh URL sources").option("-b, --budget <n>", "trim docs to ~N tokens").option("--readme <path>", "README path for `publish` (default ./README.md)").option("--dts <path>", "TypeScript declaration (.d.ts) path for `publish`").option("--language <lang>", "primary language for `publish`").option("--region <region>", "data-residency region for the hosted catalog (same as scans; default us)").option("--ingest <url>", "hosted catalog/ingest URL override (host extracted; wins over --region)").action(async function(args, opts) {
|
|
1603
|
+
const global = readGlobal(this);
|
|
1604
|
+
const root = rootOf(global);
|
|
1605
|
+
const [verb, ...rest] = args;
|
|
1606
|
+
const network = !global.local;
|
|
1607
|
+
if (!verb) return listCatalog(root, global.json);
|
|
1608
|
+
const online = (Boolean(opts.online) || network) && !global.local;
|
|
1609
|
+
if (verb === "add") return addCmd(root, rest, { ...opts, online }, global.json);
|
|
1610
|
+
if (verb === "publish") return publishCmd(root, rest, opts, global, global.json);
|
|
1611
|
+
if (verb === "resolve") return resolveCmd(root, rest, global.json);
|
|
1612
|
+
if (verb === "refresh") return refreshCmd(root, { online }, global.json);
|
|
1613
|
+
return showCmd(root, args.join(" "), opts.budget ? Number(opts.budget) : void 0, global.json, {
|
|
1614
|
+
online: network,
|
|
1615
|
+
// hosted-on-miss allowed unless --local
|
|
1616
|
+
region: opts.region,
|
|
1617
|
+
ingest: opts.ingest
|
|
1618
|
+
});
|
|
1619
|
+
});
|
|
1620
|
+
applyGlobalOptions(cmd);
|
|
1621
|
+
}
|
|
1622
|
+
function listCatalog(root, asJson) {
|
|
1623
|
+
const catalog = loadCatalog(root);
|
|
1624
|
+
const libs = Object.values(catalog.libraries).sort((a, b) => a.name.localeCompare(b.name));
|
|
1625
|
+
if (asJson) {
|
|
1626
|
+
json(libs.map((l) => ({ id: l.id, name: l.name, version: l.version, source: l.source.type })));
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
info(`${c.cyan("vg lib")} \xB7 ${libs.length} library(ies) in the catalog`);
|
|
1630
|
+
for (const l of libs) {
|
|
1631
|
+
const d = driftFor(root, l);
|
|
1632
|
+
const tag = d.drift === "behind" ? c.yellow(" (docs behind your version)") : d.drift === "ahead" ? c.dim(" (docs ahead)") : "";
|
|
1633
|
+
info(` ${c.bold(l.name)} ${c.dim(`@${l.version}`)} ${c.dim(`[${l.source.type}]`)}${tag}`);
|
|
1634
|
+
}
|
|
1635
|
+
if (!libs.length) info(c.dim(" empty \u2014 add docs with `vg lib add <path|url>`"));
|
|
1636
|
+
}
|
|
1637
|
+
async function addCmd(root, rest, opts, asJson) {
|
|
1638
|
+
const source = rest[0];
|
|
1639
|
+
if (!source) throw usageError("usage: vg lib add <path|url|git> [--name <name>] [--version <v>] [--online]");
|
|
1640
|
+
let entry;
|
|
1641
|
+
try {
|
|
1642
|
+
entry = await addLibrary(source, { root, name: opts.name, version: opts.version, allowNetwork: opts.online });
|
|
1643
|
+
} catch (err) {
|
|
1644
|
+
throw new CliError(err.message, ExitCode.ERROR);
|
|
1645
|
+
}
|
|
1646
|
+
if (asJson) {
|
|
1647
|
+
json(entry);
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
info(`${c.green("\u2714")} added ${c.bold(entry.name)} ${c.dim(`@${entry.version}`)} (${entry.bytes} bytes) \u2192 ${entry.docFile}`);
|
|
1651
|
+
}
|
|
1652
|
+
function readPackageVersion(root) {
|
|
1653
|
+
try {
|
|
1654
|
+
const pkg = JSON.parse(fs4__default.readFileSync(path9__default.join(root, "package.json"), "utf8"));
|
|
1655
|
+
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
1656
|
+
} catch {
|
|
1657
|
+
return void 0;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
function readOptionalFile(p) {
|
|
1661
|
+
try {
|
|
1662
|
+
const t = fs4__default.readFileSync(p, "utf8");
|
|
1663
|
+
return t.trim() ? t : void 0;
|
|
1664
|
+
} catch {
|
|
1665
|
+
return void 0;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
async function publishCmd(root, rest, opts, global, asJson) {
|
|
1669
|
+
if (global.local) throw usageError("vg lib publish uploads to the hosted catalog \u2014 it needs network (remove --local)");
|
|
1670
|
+
const name = (rest.join(" ") || opts.name || "").trim();
|
|
1671
|
+
if (!name) throw usageError("usage: vg lib publish <name> [--version <v>] [--readme <path>] [--dts <path>] [--language <lang>]");
|
|
1672
|
+
const dsn = resolveDsn();
|
|
1673
|
+
const parsed = dsn ? parseDsn$1(dsn) : null;
|
|
1674
|
+
if (!parsed) throw new CliError("publishing a private library requires a DSN \u2014 run `vibgrate login` or set VIBGRATE_DSN", ExitCode.USAGE_ERROR);
|
|
1675
|
+
const version = (opts.version || readPackageVersion(root) || "").trim();
|
|
1676
|
+
if (!version) throw usageError("a --version is required (or run from a package whose package.json has a version)");
|
|
1677
|
+
const readme = readOptionalFile(opts.readme ? path9__default.resolve(opts.readme) : path9__default.join(root, "README.md"));
|
|
1678
|
+
const dts = opts.dts ? readOptionalFile(path9__default.resolve(opts.dts)) : void 0;
|
|
1679
|
+
if (!readme && !dts) throw usageError("nothing to publish \u2014 no README.md found and no --dts given");
|
|
1680
|
+
const result = await publishPrivateLibrary(
|
|
1681
|
+
{ name, version, readme, dts, language: opts.language },
|
|
1682
|
+
{ region: opts.region, ingest: opts.ingest, auth: { keyId: parsed.keyId, secret: parsed.secret } }
|
|
1683
|
+
);
|
|
1684
|
+
if (!result.ok) {
|
|
1685
|
+
const msg = result.message || result.error || `publish failed (HTTP ${result.status})`;
|
|
1686
|
+
const code = result.status === 402 ? ExitCode.GATE_FAILED : result.status === 401 ? ExitCode.USAGE_ERROR : ExitCode.ERROR;
|
|
1687
|
+
throw new CliError(msg, code);
|
|
1688
|
+
}
|
|
1689
|
+
if (asJson) {
|
|
1690
|
+
json(result);
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
info(`${c.green("\u2714")} published ${c.bold(name)} ${c.dim(`@${version}`)} \xB7 ${result.entities} entities \xB7 ${result.indexedTokens} tokens indexed (MCP-token meter)`);
|
|
1694
|
+
info(c.dim(` private to your workspace \xB7 targetId ${result.targetId}`));
|
|
1695
|
+
}
|
|
1696
|
+
function resolveCmd(root, rest, asJson) {
|
|
1697
|
+
const name = rest.join(" ");
|
|
1698
|
+
if (!name) throw usageError("usage: vg lib resolve <name>");
|
|
1699
|
+
const entry = resolveLib(loadCatalog(root), name);
|
|
1700
|
+
if (!entry) throw new CliError(`no library matches "${name}" (id ${libId(name)})`, ExitCode.NOT_FOUND);
|
|
1701
|
+
if (asJson) json({ id: entry.id, name: entry.name, version: entry.version });
|
|
1702
|
+
else info(`${entry.id} ${c.dim(`(${entry.name}@${entry.version})`)}`);
|
|
1703
|
+
}
|
|
1704
|
+
async function refreshCmd(root, opts, asJson) {
|
|
1705
|
+
const catalog = loadCatalog(root);
|
|
1706
|
+
const results = [];
|
|
1707
|
+
for (const entry of Object.values(catalog.libraries)) {
|
|
1708
|
+
try {
|
|
1709
|
+
await addLibrary(entry.source.location, { root, name: entry.name, version: entry.version, allowNetwork: opts.online });
|
|
1710
|
+
results.push({ name: entry.name, ok: true });
|
|
1711
|
+
} catch {
|
|
1712
|
+
results.push({ name: entry.name, ok: false });
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
if (asJson) json({ refreshed: results });
|
|
1716
|
+
else info(`${c.cyan("vg lib refresh")} \xB7 ${results.filter((r) => r.ok).length}/${results.length} re-ingested`);
|
|
1717
|
+
}
|
|
1718
|
+
async function showCmd(root, name, budget, asJson, opts = {}) {
|
|
1719
|
+
const online = opts.online;
|
|
1720
|
+
const catalog = loadCatalog(root);
|
|
1721
|
+
const entry = resolveLib(catalog, name);
|
|
1722
|
+
const ver = resolveVersion(root, name);
|
|
1723
|
+
const displayName = entry?.name ?? name;
|
|
1724
|
+
let localDoc;
|
|
1725
|
+
let docVersion = ver.served ?? "*";
|
|
1726
|
+
let docSource;
|
|
1727
|
+
let drift;
|
|
1728
|
+
if (entry) {
|
|
1729
|
+
localDoc = readDoc(root, entry);
|
|
1730
|
+
docVersion = entry.version;
|
|
1731
|
+
drift = driftFor(root, entry);
|
|
1732
|
+
} else {
|
|
1733
|
+
const local = localPackageDocs(root, name);
|
|
1734
|
+
if (local) {
|
|
1735
|
+
localDoc = local.docs;
|
|
1736
|
+
docVersion = local.version ?? ver.served ?? "*";
|
|
1737
|
+
docSource = local.source;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
const apiSurface = localApiSurface(root, displayName);
|
|
1741
|
+
const quality = localDoc !== void 0 ? assessDocQuality([localDoc, apiSurface].filter(Boolean).join("\n\n"), { name: displayName, symbols: symbolsFromApi(apiSurface) }) : { sufficient: false, score: 0, reasons: ["no local docs"] };
|
|
1742
|
+
let hosted = null;
|
|
1743
|
+
if (online && !quality.sufficient) {
|
|
1744
|
+
const dsn = resolveDsn();
|
|
1745
|
+
const parsed = dsn ? parseDsn$1(dsn) : null;
|
|
1746
|
+
hosted = await fetchHostedDocs(
|
|
1747
|
+
{ name: displayName, query: name, maxTokens: budget },
|
|
1748
|
+
{ region: opts.region, ingest: opts.ingest, auth: parsed ? { keyId: parsed.keyId, secret: parsed.secret } : void 0 }
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
if (localDoc === void 0 && !hosted) {
|
|
1752
|
+
throw new CliError(
|
|
1753
|
+
`no library docs for "${name}" \u2014 add with \`vg lib add <path|url> --name ${name}\` or install the package${online ? "" : " (or retry with --online for the hosted catalog)"}`,
|
|
1754
|
+
ExitCode.NOT_FOUND
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
let doc;
|
|
1758
|
+
if (hosted) {
|
|
1759
|
+
doc = hosted.content;
|
|
1760
|
+
docSource = "hosted";
|
|
1761
|
+
docVersion = hosted.version ?? docVersion;
|
|
1762
|
+
} else {
|
|
1763
|
+
doc = selectForBudget({ readme: localDoc, apiSurface, budget }).text;
|
|
1764
|
+
}
|
|
1765
|
+
if (asJson) {
|
|
1766
|
+
json({
|
|
1767
|
+
id: entry?.id,
|
|
1768
|
+
name: displayName,
|
|
1769
|
+
version: docVersion,
|
|
1770
|
+
source: docSource ?? "catalog",
|
|
1771
|
+
drift: drift ?? null,
|
|
1772
|
+
version_mismatch: ver.mismatch ?? null,
|
|
1773
|
+
quality: { sufficient: quality.sufficient, score: quality.score, reasons: quality.reasons },
|
|
1774
|
+
escalate: quality.sufficient ? null : "hosted",
|
|
1775
|
+
escalated: Boolean(hosted),
|
|
1776
|
+
docs: doc
|
|
1777
|
+
});
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
info(`${c.cyan("vg lib")} \xB7 ${c.bold(displayName)} ${c.dim(`@${docVersion}`)}${docSource ? c.dim(` (from ${docSource})`) : ""}`);
|
|
1781
|
+
if (ver.mismatch) info(c.yellow(` \u26A0 ${ver.mismatch.note}`));
|
|
1782
|
+
if (hosted) info(c.green(" \u2714 local docs were thin \u2014 served richer docs from the hosted catalog"));
|
|
1783
|
+
else if (!quality.sufficient) info(c.yellow(` \u26A0 local docs look thin (${quality.reasons.join(", ")})${online ? " \u2014 hosted catalog had nothing better" : " \u2014 run without --local for the hosted catalog"}`));
|
|
1784
|
+
if (drift?.drift === "behind") info(c.yellow(` \u26A0 docs are for ${drift.cataloged}, but your installed version is ${drift.installed} \u2014 refresh the docs`));
|
|
1785
|
+
else if (drift?.drift === "current") info(c.green(` \u2714 docs match your installed version (${drift.installed})`));
|
|
1786
|
+
info("");
|
|
1787
|
+
out(doc);
|
|
1788
|
+
}
|
|
1789
|
+
function registerExport(program) {
|
|
1790
|
+
const cmd = program.command("export").description("export the map (format inferred: json|ndjson|graphml|dot|cypher|md|html|cdx.json|spdx.json)").argument("[file]", "target file (or - for stdout)", "map.json").action(function(file) {
|
|
1791
|
+
const global = readGlobal(this);
|
|
1792
|
+
const { graph } = requireGraph(global);
|
|
1793
|
+
const root = rootOf(global);
|
|
1794
|
+
const format = detectFormat(file);
|
|
1795
|
+
if (!format) throw usageError(`unknown export format for "${file}"`);
|
|
1796
|
+
const needsDeps = format === "cyclonedx" || format === "spdx";
|
|
1797
|
+
const content = exportGraph(format, {
|
|
1798
|
+
graph,
|
|
1799
|
+
deps: needsDeps ? inventory(root).records : void 0,
|
|
1800
|
+
models: needsDeps && !global.local ? discoverModels() : void 0,
|
|
1801
|
+
generatedAt: graph.generatedAt
|
|
1802
|
+
});
|
|
1803
|
+
if (file === "-") {
|
|
1804
|
+
out(content.trimEnd());
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
fs4.mkdirSync(path9.dirname(path9.resolve(file)), { recursive: true });
|
|
1808
|
+
fs4.writeFileSync(file, content);
|
|
1809
|
+
info(`${c.green("\u2714")} wrote ${file} ${c.dim(`(${format})`)}`);
|
|
1810
|
+
});
|
|
1811
|
+
applyGlobalOptions(cmd);
|
|
1812
|
+
}
|
|
1813
|
+
function detectFormat(file) {
|
|
1814
|
+
const lower = file.toLowerCase();
|
|
1815
|
+
if (lower.endsWith(".cdx.json")) return "cyclonedx";
|
|
1816
|
+
if (lower.endsWith(".spdx.json")) return "spdx";
|
|
1817
|
+
if (file === "-") return "json";
|
|
1818
|
+
return formatForExt(path9.extname(file));
|
|
1819
|
+
}
|
|
1820
|
+
function registerBundle(program) {
|
|
1821
|
+
const cmd = program.command("bundle").description("build an air-gapped bundle (grammars + graph + library catalog)").option("--offline", "offline bundle (default; present for clarity)").option("-o, --out <dir>", "output directory", "vg-bundle").action(function(opts) {
|
|
1822
|
+
const global = readGlobal(this);
|
|
1823
|
+
const root = rootOf(global);
|
|
1824
|
+
const outDir = path9.resolve(root, opts.out ?? "vg-bundle");
|
|
1825
|
+
let grammarFiles;
|
|
1826
|
+
try {
|
|
1827
|
+
grammarFiles = resolvedGrammarFiles();
|
|
1828
|
+
} catch {
|
|
1829
|
+
throw new CliError("no grammars found to bundle (run a build first)", ExitCode.ERROR);
|
|
1830
|
+
}
|
|
1831
|
+
fs4.mkdirSync(path9.join(outDir, "grammars"), { recursive: true });
|
|
1832
|
+
let grammarCount = 0;
|
|
1833
|
+
for (const { fileName, absPath } of grammarFiles) {
|
|
1834
|
+
fs4.copyFileSync(absPath, path9.join(outDir, "grammars", fileName));
|
|
1835
|
+
grammarCount++;
|
|
1836
|
+
}
|
|
1837
|
+
const included = [`grammars/ (${grammarCount} .wasm)`];
|
|
1838
|
+
const graphSrc = global.graph ?? defaultGraphPath(root);
|
|
1839
|
+
if (fs4.existsSync(graphSrc)) {
|
|
1840
|
+
fs4.copyFileSync(graphSrc, path9.join(outDir, "graph.json"));
|
|
1841
|
+
included.push("graph.json");
|
|
1842
|
+
}
|
|
1843
|
+
const cat = catalogPath(root);
|
|
1844
|
+
if (fs4.existsSync(cat)) {
|
|
1845
|
+
fs4.copyFileSync(cat, path9.join(outDir, "vibgrate.lib.json"));
|
|
1846
|
+
included.push("vibgrate.lib.json");
|
|
1847
|
+
if (fs4.existsSync(libDir(root))) {
|
|
1848
|
+
fs4.cpSync(libDir(root), path9.join(outDir, "lib"), { recursive: true });
|
|
1849
|
+
included.push("lib/");
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
const manifest = {
|
|
1853
|
+
tool: "vg",
|
|
1854
|
+
version: VERSION,
|
|
1855
|
+
grammars: grammarSetVersion(),
|
|
1856
|
+
grammarCount,
|
|
1857
|
+
included,
|
|
1858
|
+
usage: "Run `vg build --grammars <this dir>/grammars` to build fully offline."
|
|
1859
|
+
};
|
|
1860
|
+
fs4.writeFileSync(path9.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
|
|
1861
|
+
`);
|
|
1862
|
+
if (global.json) {
|
|
1863
|
+
json({ out: path9.relative(root, outDir), ...manifest });
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
info(`${c.cyan("vg bundle")} \xB7 wrote ${path9.relative(root, outDir)}/`);
|
|
1867
|
+
for (const i of included) info(` ${c.green("\u2714")} ${i}`);
|
|
1868
|
+
info(c.dim(` offline: vg build --grammars "${path9.relative(root, outDir)}/grammars"`));
|
|
1869
|
+
});
|
|
1870
|
+
applyGlobalOptions(cmd);
|
|
1871
|
+
}
|
|
1872
|
+
var initCommand = new Command("init").description("Initialize vibgrate in a project").argument("[path]", "Path to initialize", ".").option("--baseline", "Create initial baseline after init").option("--yes", "Skip confirmation prompts").action(async (targetPath, opts) => {
|
|
1873
|
+
const rootDir = path9.resolve(targetPath);
|
|
1874
|
+
const vibgrateDir2 = path9.join(rootDir, ".vibgrate");
|
|
1875
|
+
await ensureDir(vibgrateDir2);
|
|
1876
|
+
console.log(chalk5.green("\u2714") + ` Created ${chalk5.bold(".vibgrate/")} directory`);
|
|
1877
|
+
const configPath = path9.join(rootDir, "vibgrate.config.ts");
|
|
1878
|
+
if (await pathExists(configPath)) {
|
|
1879
|
+
console.log(chalk5.dim(" vibgrate.config.ts already exists, skipping"));
|
|
1880
|
+
} else {
|
|
1881
|
+
await writeDefaultConfig(rootDir);
|
|
1882
|
+
console.log(chalk5.green("\u2714") + ` Created ${chalk5.bold("vibgrate.config.ts")}`);
|
|
1883
|
+
}
|
|
1884
|
+
if (opts.baseline) {
|
|
1885
|
+
const { runBaseline } = await import('./baseline-ZBXJONQ6.js');
|
|
1886
|
+
await runBaseline(rootDir);
|
|
1887
|
+
}
|
|
1888
|
+
console.log("");
|
|
1889
|
+
console.log(chalk5.bold("Next steps:"));
|
|
1890
|
+
console.log(` ${chalk5.cyan("vibgrate scan")} Scan for upgrade drift`);
|
|
1891
|
+
console.log(` ${chalk5.cyan("vibgrate baseline")} Create a drift baseline`);
|
|
1892
|
+
console.log("");
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
// src/reporting/utils/ingest-id-output.ts
|
|
1896
|
+
function emitIngestIdLine(ingestId, options) {
|
|
1897
|
+
console.log(`VIBGRATE_INGEST_ID=${ingestId}`);
|
|
1898
|
+
if (options?.unchanged) {
|
|
1899
|
+
console.log("VIBGRATE_INGEST_UNCHANGED=1");
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
function emitDriftScoreLine(score) {
|
|
1903
|
+
if (process.env.VIBGRATE_EMIT_MARKERS !== "1") return;
|
|
1904
|
+
console.log(`VIBGRATE_DRIFT_SCORE=${score}`);
|
|
1905
|
+
}
|
|
1906
|
+
var AI_ASSISTANT_DETECTORS = [
|
|
1907
|
+
{ files: ["CLAUDE.md", ".claude"], name: "claude" },
|
|
1908
|
+
{ files: [".cursor", ".cursorrules"], name: "cursor" },
|
|
1909
|
+
{ files: [".windsurf"], name: "windsurf" },
|
|
1910
|
+
{ files: [".github/copilot-instructions.md", ".copilot"], name: "copilot" },
|
|
1911
|
+
{ files: [".gemini"], name: "gemini" },
|
|
1912
|
+
{ files: [".continuerc.json"], name: "continue" },
|
|
1913
|
+
{ files: [".codex"], name: "codex" }
|
|
1914
|
+
];
|
|
1915
|
+
async function detectAiAssistant(rootDir) {
|
|
1916
|
+
for (const detector of AI_ASSISTANT_DETECTORS) {
|
|
1917
|
+
for (const file of detector.files) {
|
|
1918
|
+
if (await pathExists$1(path9.join(rootDir, file))) {
|
|
1919
|
+
return detector.name;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return null;
|
|
1924
|
+
}
|
|
1925
|
+
function printAiContextPrompt(detectedAssistant) {
|
|
1926
|
+
const teal2 = chalk5.hex("#3FB0A4");
|
|
1927
|
+
const mint2 = chalk5.hex("#4FE3C1");
|
|
1928
|
+
const installCmd = detectedAssistant ? `vg install ${detectedAssistant}` : "vg install --all";
|
|
1929
|
+
const detectedNote = detectedAssistant ? chalk5.dim(` (${detectedAssistant} config detected)`) : "";
|
|
1930
|
+
console.log("");
|
|
1931
|
+
console.log(teal2(" \u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E"));
|
|
1932
|
+
console.log(teal2(" \u2502") + " " + mint2("\u25C6") + " " + chalk5.bold.white("Get AI-aware answers in your editor") + " " + teal2("\u2502"));
|
|
1933
|
+
console.log(teal2(" \u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
1934
|
+
console.log("");
|
|
1935
|
+
console.log(" " + chalk5.bold.white(installCmd) + detectedNote);
|
|
1936
|
+
console.log("");
|
|
1937
|
+
console.log(" " + teal2("\xB7") + " " + chalk5.white("Code map in your assistant") + chalk5.dim(" \u2014 call trees, impact surfaces, import paths"));
|
|
1938
|
+
console.log(" " + teal2("\xB7") + " " + chalk5.white("Offline drift") + chalk5.dim(" \u2014 DriftScore and upgrade priorities, right inside your editor"));
|
|
1939
|
+
console.log(" " + teal2("\xB7") + " " + chalk5.white("Version-correct library docs") + chalk5.dim(" \u2014 pinned to your lockfile, no hallucinated APIs"));
|
|
1940
|
+
console.log("");
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// src/reporting/commands/scan.ts
|
|
1944
|
+
async function autoPush(artifact, rootDir, opts) {
|
|
1945
|
+
const dsn = resolveDsn(opts.dsn);
|
|
1946
|
+
if (!dsn) {
|
|
1947
|
+
console.error(chalk5.red("No DSN provided for push."));
|
|
1948
|
+
console.error(chalk5.dim('Run "vibgrate login", set VIBGRATE_DSN, or use the --dsn flag.'));
|
|
1949
|
+
if (opts.strict) process.exit(1);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
const parsed = parseDsn(dsn);
|
|
1953
|
+
if (!parsed) {
|
|
1954
|
+
console.error(chalk5.red("Invalid DSN format."));
|
|
1955
|
+
if (opts.strict) process.exit(1);
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
const { body, contentEncoding } = await prepareCompressedUpload(artifact);
|
|
1959
|
+
const timestamp = String(Date.now());
|
|
1960
|
+
let host = parsed.host;
|
|
1961
|
+
if (opts.region) {
|
|
1962
|
+
try {
|
|
1963
|
+
host = resolveIngestHost(opts.region);
|
|
1964
|
+
} catch (e) {
|
|
1965
|
+
console.error(chalk5.red(e instanceof Error ? e.message : String(e)));
|
|
1966
|
+
if (opts.strict) process.exit(1);
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
const originalSize = JSON.stringify(artifact).length;
|
|
1971
|
+
const compressedSize = body.length;
|
|
1972
|
+
const ratio = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
1973
|
+
console.log(chalk5.dim(`Uploading to ${host}... (${(compressedSize / 1024).toFixed(0)} KB, ${ratio}% smaller)`));
|
|
1974
|
+
try {
|
|
1975
|
+
const { response, host: uploadedHost } = await uploadScanArtifact({
|
|
1976
|
+
scheme: parsed.scheme,
|
|
1977
|
+
host,
|
|
1978
|
+
keyId: parsed.keyId,
|
|
1979
|
+
secret: parsed.secret,
|
|
1980
|
+
body,
|
|
1981
|
+
contentEncoding,
|
|
1982
|
+
timestamp,
|
|
1983
|
+
force: opts.force
|
|
1984
|
+
});
|
|
1985
|
+
host = uploadedHost;
|
|
1986
|
+
if (!response.ok) {
|
|
1987
|
+
const text = await response.text();
|
|
1988
|
+
throw new Error(`HTTP ${response.status}: ${text}`);
|
|
1989
|
+
}
|
|
1990
|
+
const result = await response.json();
|
|
1991
|
+
if (result.unchanged) {
|
|
1992
|
+
console.log(
|
|
1993
|
+
chalk5.green("\u2714") + ` Repository unchanged since ${result.lastScannedAt ?? "last scan"} \u2014 skipped upload (no credit used).`
|
|
1994
|
+
);
|
|
1995
|
+
if (result.previousIngestId) {
|
|
1996
|
+
emitIngestIdLine(result.previousIngestId, { unchanged: true });
|
|
1997
|
+
const dashUrl = `https://${dashHostForIngestHost(host)}/${parsed.workspaceId}/scan/${result.previousIngestId}`;
|
|
1998
|
+
console.log(chalk5.dim(` Previous report: ${dashUrl}`));
|
|
1999
|
+
} else if (opts.strict) {
|
|
2000
|
+
console.error(chalk5.red("Repository unchanged but no previous ingest id returned."));
|
|
2001
|
+
process.exit(1);
|
|
2002
|
+
}
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
console.log(chalk5.green("\u2714") + ` Scan queued for processing (${result.ingestId ?? "ok"})`);
|
|
2006
|
+
if (result.ingestId) {
|
|
2007
|
+
emitIngestIdLine(result.ingestId);
|
|
2008
|
+
const dashUrl = `https://${dashHostForIngestHost(host)}/${parsed.workspaceId}/scan/${result.ingestId}`;
|
|
2009
|
+
const CLEAR_LINE = process.platform === "win32" ? "\x1B[0G\x1B[2K" : "";
|
|
2010
|
+
console.log("");
|
|
2011
|
+
console.log(CLEAR_LINE + chalk5.dim(" Processing continues in the background. Results available shortly."));
|
|
2012
|
+
console.log("");
|
|
2013
|
+
console.log(CLEAR_LINE + chalk5.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2014
|
+
console.log(CLEAR_LINE + chalk5.bold(" \u{1F4CA} View Scan Report"));
|
|
2015
|
+
console.log(CLEAR_LINE + " " + chalk5.underline.cyan(dashUrl));
|
|
2016
|
+
console.log(CLEAR_LINE + chalk5.cyan("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2017
|
+
console.log("");
|
|
2018
|
+
console.log("");
|
|
2019
|
+
}
|
|
2020
|
+
} catch (e) {
|
|
2021
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2022
|
+
console.error(chalk5.red(`Upload failed: ${msg}`));
|
|
2023
|
+
if (opts.strict) process.exit(1);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
function collectExcludes(value, previous) {
|
|
2027
|
+
return [...previous, ...parseExcludePatterns(value)];
|
|
2028
|
+
}
|
|
2029
|
+
function reportStandards(rootDir) {
|
|
2030
|
+
const loaded = loadStandards(rootDir);
|
|
2031
|
+
if (!loaded.policy) return;
|
|
2032
|
+
const violations = checkStandards(loaded.policy, inventory(rootDir).records);
|
|
2033
|
+
if (violations.length === 0) {
|
|
2034
|
+
console.log(chalk5.green("\u2714") + " Standards: no banned dependencies in use");
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
console.log(chalk5.red(`
|
|
2038
|
+
\u26A0 Standards: ${violations.length} banned dependency(ies) in use`));
|
|
2039
|
+
for (const v of violations) {
|
|
2040
|
+
const fix = v.use ? chalk5.dim(` \u2192 use ${v.use}`) : "";
|
|
2041
|
+
const why = v.reason ? chalk5.dim(` (${v.reason})`) : "";
|
|
2042
|
+
console.log(` ${chalk5.red("banned")} ${v.ecosystem}:${v.name}${v.installed ? chalk5.dim(` ${v.installed}`) : ""}${fix}${why}`);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
function parseNonNegativeNumber(value, label) {
|
|
2046
|
+
if (value === void 0) return void 0;
|
|
2047
|
+
const parsed = Number(value);
|
|
2048
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
2049
|
+
throw new Error(`${label} must be a non-negative number.`);
|
|
2050
|
+
}
|
|
2051
|
+
return parsed;
|
|
2052
|
+
}
|
|
2053
|
+
var scanCommand = new Command("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif|md)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option(
|
|
2054
|
+
"-e, --exclude <glob>",
|
|
2055
|
+
'Exclude paths matching a glob pattern. Repeatable, and a single value may list several patterns separated by commas or semicolons (e.g. --exclude "legacy/**,vendor/**"). Merged with excludes from the config file.',
|
|
2056
|
+
collectExcludes,
|
|
2057
|
+
[]
|
|
2058
|
+
).option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--full", "Comprehensive scan: turns on known-vulnerability detection (= --vulns) and, when a standards policy exists, a banned-dependency report \u2014 on top of drift scoring and the code map").option("--vulns", "Also scan installed dependencies for known vulnerabilities (OSV online, or advisories from --package-manifest when offline)").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--project-scan-timeout <seconds>", "Per-project scan timeout in seconds (default: 180)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").option("--repository-name <name>", "Override the repository name recorded for this scan (defaults to the directory or package.json name)").option("--force", "Always create a fresh scan ingest, even if the repository is unchanged since the last scan (skips the unchanged/reuse optimization). Used by scheduled and dashboard-triggered scans.").option("--no-graph", "Skip building the local code map (the AI/docs index) that scan produces after scoring drift").action(async (targetPath, opts) => {
|
|
2059
|
+
const rootDir = path9.resolve(targetPath);
|
|
2060
|
+
if (!await pathExists$1(rootDir)) {
|
|
2061
|
+
console.error(chalk5.red(`Path does not exist: ${rootDir}`));
|
|
2062
|
+
process.exit(1);
|
|
2063
|
+
}
|
|
2064
|
+
const hasDsn = !!resolveDsn(opts.dsn);
|
|
2065
|
+
const willPush = !opts.offline && (opts.push || hasDsn);
|
|
2066
|
+
const isFirstRun = !await pathExists$1(path9.join(rootDir, ".vibgrate", "scan_result.json"));
|
|
2067
|
+
let pinnedRegion;
|
|
2068
|
+
if (willPush && hasDsn) {
|
|
2069
|
+
const dsn = resolveDsn(opts.dsn);
|
|
2070
|
+
const parsed = parseDsn(dsn);
|
|
2071
|
+
if (parsed) {
|
|
2072
|
+
const ingestHost = opts.region ? resolveIngestHost(opts.region) : parsed.host;
|
|
2073
|
+
const vcs = await detectVcs(rootDir);
|
|
2074
|
+
const fingerprint = await computeRepoFingerprint(rootDir, vcs);
|
|
2075
|
+
const repositoryName = opts.repositoryName?.trim() || await resolveRepositoryName(rootDir);
|
|
2076
|
+
try {
|
|
2077
|
+
const preflight = await fetchScanPreflight(parsed, ingestHost, {
|
|
2078
|
+
repositoryName,
|
|
2079
|
+
vcsSha: fingerprint.vcsSha
|
|
2080
|
+
});
|
|
2081
|
+
pinnedRegion = preflight.region;
|
|
2082
|
+
if (preflight.vm && !preflight.vm.allowed) {
|
|
2083
|
+
console.error(chalk5.red(preflight.error ?? "VM meter usage exhausted"));
|
|
2084
|
+
console.error(
|
|
2085
|
+
chalk5.dim(
|
|
2086
|
+
`VM minutes: ${preflight.vm.used}/${preflight.vm.limit} (${preflight.plan.label} plan) \u2014 enable overages or upgrade your plan.`
|
|
2087
|
+
)
|
|
2088
|
+
);
|
|
2089
|
+
process.exit(1);
|
|
2090
|
+
}
|
|
2091
|
+
if (preflight.repositories && !preflight.repositories.allowed) {
|
|
2092
|
+
console.error(
|
|
2093
|
+
chalk5.red(
|
|
2094
|
+
preflight.error ?? `Repository limit reached for the ${preflight.plan.label} plan \u2014 cannot scan a new repository.`
|
|
2095
|
+
)
|
|
2096
|
+
);
|
|
2097
|
+
const max = preflight.repositories.max < 0 ? "unlimited" : String(preflight.repositories.max);
|
|
2098
|
+
console.error(
|
|
2099
|
+
chalk5.dim(
|
|
2100
|
+
`Repositories: ${preflight.repositories.total}/${max} (${preflight.plan.label} plan) \u2014 archive a repository or upgrade your plan.`
|
|
2101
|
+
)
|
|
2102
|
+
);
|
|
2103
|
+
if (preflight.upgradeUrl) {
|
|
2104
|
+
console.error(chalk5.dim(` Upgrade: ${preflight.upgradeUrl}`));
|
|
2105
|
+
}
|
|
2106
|
+
process.exit(1);
|
|
2107
|
+
}
|
|
2108
|
+
if (preflight.status === "error" || !preflight.scans.allowed) {
|
|
2109
|
+
console.error(chalk5.red(preflight.error ?? "Scan ingestion not allowed for this workspace."));
|
|
2110
|
+
console.error(
|
|
2111
|
+
chalk5.dim(
|
|
2112
|
+
`Credits: ${preflight.scans.used}/${preflight.scans.limit} (${preflight.plan.label} plan)`
|
|
2113
|
+
)
|
|
2114
|
+
);
|
|
2115
|
+
if (opts.strict) process.exit(1);
|
|
2116
|
+
process.exit(1);
|
|
2117
|
+
}
|
|
2118
|
+
console.log(
|
|
2119
|
+
chalk5.dim(
|
|
2120
|
+
`Plan: ${preflight.plan.label} \u2014 scan credits ${preflight.scans.used}/${preflight.scans.limit} this month`
|
|
2121
|
+
)
|
|
2122
|
+
);
|
|
2123
|
+
if (preflight.repository?.unchanged && !opts.force) {
|
|
2124
|
+
console.log(
|
|
2125
|
+
chalk5.green("\u2714") + ` Repository unchanged at ${preflight.repository.lastVcsSha?.slice(0, 7) ?? "same revision"} \u2014 skipping scan.`
|
|
2126
|
+
);
|
|
2127
|
+
if (preflight.repository.lastIngestId) {
|
|
2128
|
+
emitIngestIdLine(preflight.repository.lastIngestId, { unchanged: true });
|
|
2129
|
+
const dashUrl = `https://${dashHostForIngestHost(ingestHost)}/${parsed.workspaceId}/scan/${preflight.repository.lastIngestId}`;
|
|
2130
|
+
console.log(chalk5.dim(` Latest report: ${dashUrl}`));
|
|
2131
|
+
} else if (opts.strict) {
|
|
2132
|
+
console.error(chalk5.red("Repository unchanged but no previous ingest id available."));
|
|
2133
|
+
process.exit(1);
|
|
2134
|
+
}
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
} catch (e) {
|
|
2138
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2139
|
+
console.error(chalk5.yellow(`Preflight check failed: ${msg}`));
|
|
2140
|
+
if (opts.strict) process.exit(1);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
const scanOpts = {
|
|
2145
|
+
vibgrateVersion: VERSION,
|
|
2146
|
+
out: opts.out,
|
|
2147
|
+
format: opts.format || "text",
|
|
2148
|
+
failOn: opts.failOn,
|
|
2149
|
+
baseline: opts.baseline,
|
|
2150
|
+
changedOnly: opts.changedOnly,
|
|
2151
|
+
exclude: opts.exclude,
|
|
2152
|
+
concurrency: parseInt(opts.concurrency, 10) || 8,
|
|
2153
|
+
push: opts.push,
|
|
2154
|
+
dsn: opts.dsn,
|
|
2155
|
+
// An explicit --region wins; otherwise route to the workspace's pinned
|
|
2156
|
+
// region as reported by preflight.
|
|
2157
|
+
region: opts.region ?? pinnedRegion,
|
|
2158
|
+
strict: opts.strict,
|
|
2159
|
+
uiPurpose: opts.uiPurpose,
|
|
2160
|
+
noLocalArtifacts: opts.noLocalArtifacts,
|
|
2161
|
+
maxPrivacy: opts.maxPrivacy,
|
|
2162
|
+
offline: opts.offline,
|
|
2163
|
+
// `--full` is the comprehensive umbrella: it turns on vulnerability scanning.
|
|
2164
|
+
vulns: opts.vulns || opts.full,
|
|
2165
|
+
packageManifest: opts.packageManifest,
|
|
2166
|
+
driftBudget: parseNonNegativeNumber(opts.driftBudget, "--drift-budget"),
|
|
2167
|
+
driftWorseningPercent: parseNonNegativeNumber(opts.driftWorsening, "--drift-worsening"),
|
|
2168
|
+
projectScanTimeout: opts.projectScanTimeout ? parseInt(opts.projectScanTimeout, 10) || void 0 : void 0,
|
|
2169
|
+
repositoryName: opts.repositoryName?.trim() || void 0,
|
|
2170
|
+
force: opts.force
|
|
2171
|
+
};
|
|
2172
|
+
const wantGraph = opts.graph !== false && !opts.maxPrivacy;
|
|
2173
|
+
if (wantGraph) {
|
|
2174
|
+
scanOpts.postScan = async (report) => {
|
|
2175
|
+
const result = await buildGraph({
|
|
2176
|
+
root: rootDir,
|
|
2177
|
+
exclude: opts.exclude,
|
|
2178
|
+
onParseProgress: (done, total) => report(done, total, "parsing")
|
|
2179
|
+
});
|
|
2180
|
+
writeArtifacts(result.graph, { root: rootDir });
|
|
2181
|
+
writeSnapshot(rootDir, result.graph.provenance.corpusHash, result.fileStats, {
|
|
2182
|
+
exclude: opts.exclude
|
|
2183
|
+
});
|
|
2184
|
+
const { counts } = result.graph.meta;
|
|
2185
|
+
return `${counts.nodes.toLocaleString()} nodes \xB7 ${counts.edges.toLocaleString()} edges`;
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
const advanced = await loadAdvancedScanHook();
|
|
2189
|
+
const artifact = await runCoreScan(rootDir, scanOpts, advanced);
|
|
2190
|
+
emitDriftScoreLine(artifact.drift.score);
|
|
2191
|
+
if (opts.full) {
|
|
2192
|
+
reportStandards(rootDir);
|
|
2193
|
+
}
|
|
2194
|
+
if (opts.failOn) {
|
|
2195
|
+
const hasErrors = artifact.findings.some((f) => f.level === "error");
|
|
2196
|
+
const hasWarnings = artifact.findings.some((f) => f.level === "warning");
|
|
2197
|
+
if (opts.failOn === "error" && hasErrors) {
|
|
2198
|
+
console.error(chalk5.red(`
|
|
2199
|
+
Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
|
|
2200
|
+
process.exit(2);
|
|
2201
|
+
}
|
|
2202
|
+
if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
|
|
2203
|
+
console.error(chalk5.red(`
|
|
2204
|
+
Failing: findings detected at warn level or above.`));
|
|
2205
|
+
process.exit(2);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
|
|
2209
|
+
console.error(chalk5.red(`
|
|
2210
|
+
Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
|
|
2211
|
+
process.exit(2);
|
|
2212
|
+
}
|
|
2213
|
+
if (scanOpts.driftWorseningPercent !== void 0) {
|
|
2214
|
+
if (artifact.delta === void 0) {
|
|
2215
|
+
console.error(chalk5.red("\nFailing fitness function: --drift-worsening requires --baseline to compare against previous drift."));
|
|
2216
|
+
process.exit(2);
|
|
2217
|
+
}
|
|
2218
|
+
if (artifact.delta > 0) {
|
|
2219
|
+
const baselineScore = artifact.drift.score - artifact.delta;
|
|
2220
|
+
const denominator = Math.max(Math.abs(baselineScore), 1e-4);
|
|
2221
|
+
const worseningPercent = artifact.delta / denominator * 100;
|
|
2222
|
+
if (worseningPercent > scanOpts.driftWorseningPercent) {
|
|
2223
|
+
console.error(chalk5.red(`
|
|
2224
|
+
Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (threshold ${scanOpts.driftWorseningPercent}%).`));
|
|
2225
|
+
process.exit(2);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
if (willPush) {
|
|
2230
|
+
await autoPush(artifact, rootDir, scanOpts);
|
|
2231
|
+
}
|
|
2232
|
+
const showAiPrompt = scanOpts.format === "text" && !hasDsn && !opts.offline && !opts.noLocalArtifacts && !opts.maxPrivacy && isFirstRun;
|
|
2233
|
+
if (showAiPrompt) {
|
|
2234
|
+
const detectedAssistant = await detectAiAssistant(rootDir);
|
|
2235
|
+
printAiContextPrompt(detectedAssistant);
|
|
2236
|
+
}
|
|
2237
|
+
});
|
|
2238
|
+
function formatText(artifact) {
|
|
2239
|
+
const lines = [];
|
|
2240
|
+
const teal2 = chalk5.hex("#3FB0A4");
|
|
2241
|
+
const mint2 = chalk5.hex("#4FE3C1");
|
|
2242
|
+
lines.push("");
|
|
2243
|
+
lines.push(" " + teal2("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u256E") + mint2("\u279C"));
|
|
2244
|
+
lines.push(" " + chalk5.dim("\u2524") + teal2("\u2502") + " " + mint2("\u25FC") + " " + mint2("\u25FC") + " " + teal2("\u2502") + chalk5.dim("\u251C") + " " + chalk5.bold.white("vibgrate"));
|
|
2245
|
+
lines.push(" " + chalk5.dim("\u2524") + teal2("\u2502") + " " + chalk5.dim("\u2581\u2581") + " " + teal2("\u2502") + chalk5.dim("\u251C") + " " + chalk5.dim(`Drift Intelligence Engine v${VERSION}`));
|
|
2246
|
+
lines.push(" " + teal2("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u256F"));
|
|
2247
|
+
lines.push("");
|
|
2248
|
+
lines.push(...titleBox("Vibgrate Drift Report", teal2));
|
|
2249
|
+
lines.push("");
|
|
2250
|
+
for (const project of artifact.projects) {
|
|
2251
|
+
lines.push(chalk5.bold(` \u2500\u2500 ${project.name} `) + chalk5.dim(`(${project.type}) ${project.path}`));
|
|
2252
|
+
if (project.runtime) {
|
|
2253
|
+
const behindStr = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ? chalk5.yellow(` (${project.runtimeMajorsBehind} major${project.runtimeMajorsBehind > 1 ? "s" : ""} behind)`) : chalk5.green(" (current)");
|
|
2254
|
+
lines.push(` Runtime: ${project.runtime}${behindStr}`);
|
|
2255
|
+
}
|
|
2256
|
+
if (project.targetFramework) {
|
|
2257
|
+
lines.push(` Target: ${project.targetFramework}`);
|
|
2258
|
+
}
|
|
2259
|
+
if (project.frameworks.length > 0) {
|
|
2260
|
+
lines.push(" Frameworks:");
|
|
2261
|
+
for (const fw of project.frameworks) {
|
|
2262
|
+
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ? chalk5.green("current") : chalk5.yellow(`${fw.majorsBehind} behind`) : chalk5.dim("unknown");
|
|
2263
|
+
lines.push(` ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
const b = project.dependencyAgeBuckets;
|
|
2267
|
+
const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
|
|
2268
|
+
if (total > 0) {
|
|
2269
|
+
lines.push(" Dependencies:");
|
|
2270
|
+
lines.push(` ${chalk5.green(`${b.current} current`)} ${chalk5.yellow(`${b.oneBehind} 1-behind`)} ${chalk5.red(`${b.twoPlusBehind} 2+ behind`)} ${chalk5.dim(`${b.unknown} unknown`)}`);
|
|
2271
|
+
}
|
|
2272
|
+
lines.push("");
|
|
2273
|
+
}
|
|
2274
|
+
if (artifact.delta !== void 0) {
|
|
2275
|
+
const deltaStr = artifact.delta > 0 ? chalk5.red(`+${artifact.delta}`) : artifact.delta < 0 ? chalk5.green(`${artifact.delta}`) : chalk5.dim("0");
|
|
2276
|
+
lines.push(chalk5.bold(" Drift Delta: ") + deltaStr + " (vs baseline)");
|
|
2277
|
+
lines.push("");
|
|
2278
|
+
}
|
|
2279
|
+
if (artifact.extended) {
|
|
2280
|
+
lines.push(...formatExtended(artifact.extended));
|
|
2281
|
+
}
|
|
2282
|
+
if (artifact.findings.length > 0) {
|
|
2283
|
+
const errors = artifact.findings.filter((f) => f.level === "error");
|
|
2284
|
+
const warnings = artifact.findings.filter((f) => f.level === "warning");
|
|
2285
|
+
const notes = artifact.findings.filter((f) => f.level === "note");
|
|
2286
|
+
const summary = [
|
|
2287
|
+
errors.length > 0 ? chalk5.red(`${errors.length} error${errors.length !== 1 ? "s" : ""}`) : "",
|
|
2288
|
+
warnings.length > 0 ? chalk5.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}`) : "",
|
|
2289
|
+
notes.length > 0 ? chalk5.blue(`${notes.length} note${notes.length !== 1 ? "s" : ""}`) : ""
|
|
2290
|
+
].filter(Boolean).join(chalk5.dim(", "));
|
|
2291
|
+
lines.push(chalk5.bold.underline(` Findings`) + chalk5.dim(` (${summary})`));
|
|
2292
|
+
for (const f of artifact.findings) {
|
|
2293
|
+
const icon = f.level === "error" ? chalk5.red("\u2716") : f.level === "warning" ? chalk5.yellow("\u26A0") : chalk5.blue("\u2139");
|
|
2294
|
+
lines.push(` ${icon} ${f.message}`);
|
|
2295
|
+
lines.push(chalk5.dim(` ${f.ruleId} in ${f.location}`));
|
|
2296
|
+
}
|
|
2297
|
+
lines.push("");
|
|
2298
|
+
}
|
|
2299
|
+
const actions = generatePriorityActions(artifact);
|
|
2300
|
+
if (actions.length > 0) {
|
|
2301
|
+
lines.push(...titleBox("Top Priority Actions"));
|
|
2302
|
+
lines.push("");
|
|
2303
|
+
for (let i = 0; i < actions.length; i++) {
|
|
2304
|
+
const a = actions[i];
|
|
2305
|
+
const num = chalk5.bold.cyan(` ${i + 1}.`);
|
|
2306
|
+
lines.push(`${num} ${chalk5.bold(a.title)}`);
|
|
2307
|
+
lines.push(chalk5.dim(` ${a.explanation}`));
|
|
2308
|
+
if (a.impact) lines.push(` Impact: ${chalk5.green(a.impact)}`);
|
|
2309
|
+
lines.push("");
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
if (artifact.extended?.architecture) {
|
|
2313
|
+
lines.push(...formatArchitectureDiagram(artifact.extended.architecture));
|
|
2314
|
+
}
|
|
2315
|
+
if (artifact.solutions && artifact.solutions.length > 0) {
|
|
2316
|
+
lines.push(...titleBox("Solution Drift Summary"));
|
|
2317
|
+
lines.push("");
|
|
2318
|
+
for (const solution of artifact.solutions) {
|
|
2319
|
+
const solScore = solution.drift?.score;
|
|
2320
|
+
const color = typeof solScore === "number" ? solScore <= 30 ? chalk5.green : solScore <= 60 ? chalk5.yellow : chalk5.red : chalk5.dim;
|
|
2321
|
+
lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk5.dim("n/a")}`);
|
|
2322
|
+
}
|
|
2323
|
+
lines.push("");
|
|
2324
|
+
}
|
|
2325
|
+
const scoreColor = artifact.drift.score <= 30 ? chalk5.green : artifact.drift.score <= 60 ? chalk5.yellow : chalk5.red;
|
|
2326
|
+
lines.push(...titleBox("Drift Score Summary"));
|
|
2327
|
+
lines.push("");
|
|
2328
|
+
lines.push(chalk5.bold(" Drift Score: ") + scoreColor.bold(`${artifact.drift.score}/100`));
|
|
2329
|
+
lines.push(chalk5.bold(" Risk Level: ") + riskBadge(artifact.drift.riskLevel));
|
|
2330
|
+
lines.push(chalk5.bold(" Projects: ") + `${artifact.projects.length}`);
|
|
2331
|
+
if (artifact.vcs) {
|
|
2332
|
+
const vcsParts = [artifact.vcs.type];
|
|
2333
|
+
if (artifact.vcs.branch) vcsParts.push(artifact.vcs.branch);
|
|
2334
|
+
if (artifact.vcs.shortSha) vcsParts.push(chalk5.dim(artifact.vcs.shortSha));
|
|
2335
|
+
lines.push(chalk5.bold(" VCS: ") + vcsParts.join(" "));
|
|
2336
|
+
}
|
|
2337
|
+
lines.push("");
|
|
2338
|
+
const m = new Set(artifact.drift.measured ?? ["runtime", "framework", "dependency", "eol"]);
|
|
2339
|
+
lines.push(" " + chalk5.bold.underline("Score Breakdown"));
|
|
2340
|
+
lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) : chalk5.dim("n/a")}`);
|
|
2341
|
+
lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) : chalk5.dim("n/a")}`);
|
|
2342
|
+
lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) : chalk5.dim("n/a")}`);
|
|
2343
|
+
lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) : chalk5.dim("n/a")}`);
|
|
2344
|
+
lines.push("");
|
|
2345
|
+
const scannedParts = [`Scanned at ${artifact.timestamp}`];
|
|
2346
|
+
if (artifact.durationMs !== void 0) {
|
|
2347
|
+
const secs = (artifact.durationMs / 1e3).toFixed(1);
|
|
2348
|
+
scannedParts.push(`${secs}s`);
|
|
2349
|
+
}
|
|
2350
|
+
if (artifact.filesScanned !== void 0) {
|
|
2351
|
+
scannedParts.push(`${artifact.filesScanned} file${artifact.filesScanned !== 1 ? "s" : ""} scanned`);
|
|
2352
|
+
}
|
|
2353
|
+
if (artifact.treeSummary) {
|
|
2354
|
+
scannedParts.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files`);
|
|
2355
|
+
scannedParts.push(`${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
|
|
2356
|
+
}
|
|
2357
|
+
lines.push(chalk5.dim(` ${scannedParts.join(" \xB7 ")}`));
|
|
2358
|
+
lines.push("");
|
|
2359
|
+
return lines.join("\n");
|
|
2360
|
+
}
|
|
2361
|
+
function riskBadge(level) {
|
|
2362
|
+
switch (level) {
|
|
2363
|
+
case "low":
|
|
2364
|
+
return chalk5.bgGreen.black(" LOW ");
|
|
2365
|
+
case "moderate":
|
|
2366
|
+
return chalk5.bgYellow.black(" MODERATE ");
|
|
2367
|
+
case "high":
|
|
2368
|
+
return chalk5.bgRed.white(" HIGH ");
|
|
2369
|
+
default:
|
|
2370
|
+
return level;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
function scoreBar(score) {
|
|
2374
|
+
return driftBar(score, 20);
|
|
2375
|
+
}
|
|
2376
|
+
var CATEGORY_LABELS = {
|
|
2377
|
+
frontend: "Frontend",
|
|
2378
|
+
metaFrameworks: "Meta-frameworks",
|
|
2379
|
+
bundlers: "Bundlers",
|
|
2380
|
+
css: "CSS / UI",
|
|
2381
|
+
backend: "Backend",
|
|
2382
|
+
orm: "ORM / Database",
|
|
2383
|
+
testing: "Testing",
|
|
2384
|
+
lintFormat: "Lint & Format",
|
|
2385
|
+
apiMessaging: "API & Messaging",
|
|
2386
|
+
observability: "Observability",
|
|
2387
|
+
payment: "Payment",
|
|
2388
|
+
auth: "Auth",
|
|
2389
|
+
email: "Email",
|
|
2390
|
+
cloud: "Cloud",
|
|
2391
|
+
databases: "Databases",
|
|
2392
|
+
messaging: "Messaging",
|
|
2393
|
+
crm: "CRM & Comms",
|
|
2394
|
+
storage: "Storage",
|
|
2395
|
+
search: "Search & AI"
|
|
2396
|
+
};
|
|
2397
|
+
function formatExtended(ext) {
|
|
2398
|
+
const lines = [];
|
|
2399
|
+
if (ext.toolingInventory) {
|
|
2400
|
+
const inv = ext.toolingInventory;
|
|
2401
|
+
const categories = Object.entries(inv).filter(([, items]) => items.length > 0);
|
|
2402
|
+
if (categories.length > 0) {
|
|
2403
|
+
lines.push(chalk5.bold.underline(" Tech Stack"));
|
|
2404
|
+
for (const [cat, items] of categories) {
|
|
2405
|
+
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
2406
|
+
const names = items.map((i) => chalk5.white(i.name)).join(chalk5.dim(", "));
|
|
2407
|
+
lines.push(` ${chalk5.cyan(label)}: ${names}`);
|
|
2408
|
+
}
|
|
2409
|
+
lines.push("");
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
if (ext.serviceDependencies) {
|
|
2413
|
+
const svc = ext.serviceDependencies;
|
|
2414
|
+
const categories = Object.entries(svc).filter(([, items]) => items.length > 0);
|
|
2415
|
+
if (categories.length > 0) {
|
|
2416
|
+
lines.push(chalk5.bold.underline(" Services & Integrations"));
|
|
2417
|
+
for (const [cat, items] of categories) {
|
|
2418
|
+
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
2419
|
+
const names = items.map((i) => {
|
|
2420
|
+
const ver = i.version ? chalk5.dim(` ${i.version}`) : "";
|
|
2421
|
+
return chalk5.white(i.name) + ver;
|
|
2422
|
+
}).join(chalk5.dim(", "));
|
|
2423
|
+
lines.push(` ${chalk5.cyan(label)}: ${names}`);
|
|
2424
|
+
}
|
|
2425
|
+
lines.push("");
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
if (ext.breakingChangeExposure) {
|
|
2429
|
+
const bc = ext.breakingChangeExposure;
|
|
2430
|
+
if (bc.deprecatedPackages.length > 0 || bc.legacyPolyfills.length > 0) {
|
|
2431
|
+
lines.push(chalk5.bold.underline(" Breaking Change Exposure"));
|
|
2432
|
+
const exposureColor = bc.exposureScore >= 40 ? chalk5.red : bc.exposureScore >= 20 ? chalk5.yellow : chalk5.green;
|
|
2433
|
+
lines.push(` Exposure Score: ${exposureColor.bold(`${bc.exposureScore}/100`)}`);
|
|
2434
|
+
if (bc.deprecatedPackages.length > 0) {
|
|
2435
|
+
lines.push(` ${chalk5.red("Deprecated")}: ${bc.deprecatedPackages.map((p) => chalk5.dim(p)).join(", ")}`);
|
|
2436
|
+
}
|
|
2437
|
+
if (bc.legacyPolyfills.length > 0) {
|
|
2438
|
+
lines.push(` ${chalk5.yellow("Polyfills")}: ${bc.legacyPolyfills.map((p) => chalk5.dim(p)).join(", ")}`);
|
|
2439
|
+
}
|
|
2440
|
+
if (bc.peerConflictsDetected) {
|
|
2441
|
+
lines.push(` ${chalk5.red("\u26A0")} Peer dependency conflicts detected`);
|
|
2442
|
+
}
|
|
2443
|
+
lines.push(` Recommendation: ${chalk5.bold(bc.overallRecommendation)}`);
|
|
2444
|
+
const projectsWithPlans = bc.projectIntelligence.filter((p) => p.packages.length > 0).slice(0, 3);
|
|
2445
|
+
if (projectsWithPlans.length > 0) {
|
|
2446
|
+
lines.push(" Major Upgrade Intelligence:");
|
|
2447
|
+
for (const p of projectsWithPlans) {
|
|
2448
|
+
lines.push(` - ${p.project} (${p.recommendation})`);
|
|
2449
|
+
for (const pkg of p.packages.slice(0, 2)) {
|
|
2450
|
+
lines.push(` \xB7 ${pkg.package} ${pkg.currentVersion ?? "?"} \u2192 ${pkg.targetVersion ?? "?"} | touched ~${pkg.usage.touchedPercent}% | ${pkg.automatable}`);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
lines.push("");
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
if (ext.tsModernity && ext.tsModernity.typescriptVersion) {
|
|
2458
|
+
const ts = ext.tsModernity;
|
|
2459
|
+
lines.push(chalk5.bold.underline(" TypeScript"));
|
|
2460
|
+
const parts = [];
|
|
2461
|
+
parts.push(`v${ts.typescriptVersion}`);
|
|
2462
|
+
if (ts.strict === true) parts.push(chalk5.green("strict \u2714"));
|
|
2463
|
+
else if (ts.strict === false) parts.push(chalk5.yellow("strict \u2716"));
|
|
2464
|
+
if (ts.moduleType) parts.push(ts.moduleType.toUpperCase());
|
|
2465
|
+
if (ts.target) parts.push(`target: ${ts.target}`);
|
|
2466
|
+
lines.push(` ${parts.join(chalk5.dim(" \xB7 "))}`);
|
|
2467
|
+
lines.push("");
|
|
2468
|
+
}
|
|
2469
|
+
if (ext.buildDeploy) {
|
|
2470
|
+
const bd = ext.buildDeploy;
|
|
2471
|
+
const hasSomething = bd.ci.length > 0 || bd.docker.dockerfileCount > 0 || bd.packageManagers.length > 0;
|
|
2472
|
+
if (hasSomething) {
|
|
2473
|
+
lines.push(chalk5.bold.underline(" Build & Deploy"));
|
|
2474
|
+
if (bd.ci.length > 0) lines.push(` CI: ${bd.ci.join(", ")}`);
|
|
2475
|
+
if (bd.docker.dockerfileCount > 0) {
|
|
2476
|
+
lines.push(` Docker: ${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""} (${bd.docker.baseImages.join(", ")})`);
|
|
2477
|
+
}
|
|
2478
|
+
if (bd.packageManagers.length > 0) lines.push(` Package Managers: ${bd.packageManagers.join(", ")}`);
|
|
2479
|
+
if (bd.monorepoTools.length > 0) lines.push(` Monorepo: ${bd.monorepoTools.join(", ")}`);
|
|
2480
|
+
if (bd.iac.length > 0) lines.push(` IaC: ${bd.iac.join(", ")}`);
|
|
2481
|
+
lines.push("");
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
if (ext.uiPurpose) {
|
|
2485
|
+
const up = ext.uiPurpose;
|
|
2486
|
+
lines.push(chalk5.bold.underline(" Product Purpose Signals"));
|
|
2487
|
+
lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : chalk5.dim("unknown")}`);
|
|
2488
|
+
lines.push(` Evidence: ${up.topEvidence.length}${up.capped ? chalk5.dim(` of ${up.evidenceCount} (capped)`) : ""}`);
|
|
2489
|
+
const top = up.topEvidence.slice(0, 8);
|
|
2490
|
+
if (top.length > 0) {
|
|
2491
|
+
lines.push(" Top Signals:");
|
|
2492
|
+
for (const item of top) {
|
|
2493
|
+
lines.push(` - [${item.kind}] ${item.value} ${chalk5.dim(`(${item.file})`)}`);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
if (up.unknownSignals.length > 0) {
|
|
2497
|
+
lines.push(" Unknowns:");
|
|
2498
|
+
for (const u of up.unknownSignals.slice(0, 4)) {
|
|
2499
|
+
lines.push(` - ${chalk5.yellow(u)}`);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
lines.push("");
|
|
2503
|
+
}
|
|
2504
|
+
if (ext.securityPosture) {
|
|
2505
|
+
const sec = ext.securityPosture;
|
|
2506
|
+
lines.push(chalk5.bold.underline(" Security Posture"));
|
|
2507
|
+
const checks = [];
|
|
2508
|
+
checks.push(sec.lockfilePresent ? chalk5.green("Lockfile \u2714") : chalk5.red("Lockfile \u2716"));
|
|
2509
|
+
checks.push(sec.gitignoreCoversEnv ? chalk5.green(".env \u2714") : chalk5.red(".env \u2716"));
|
|
2510
|
+
checks.push(sec.gitignoreCoversNodeModules ? chalk5.green("node_modules \u2714") : chalk5.yellow("node_modules \u2716"));
|
|
2511
|
+
if (sec.multipleLockfileTypes) checks.push(chalk5.yellow("Multiple lockfiles \u26A0"));
|
|
2512
|
+
if (sec.envFilesTracked) checks.push(chalk5.red("Env files tracked \u2716"));
|
|
2513
|
+
lines.push(` ${checks.join(chalk5.dim(" \xB7 "))}`);
|
|
2514
|
+
lines.push("");
|
|
2515
|
+
}
|
|
2516
|
+
if (ext.platformMatrix) {
|
|
2517
|
+
const pm = ext.platformMatrix;
|
|
2518
|
+
if (pm.nativeModules.length > 0 || pm.dockerBaseImages.length > 0) {
|
|
2519
|
+
lines.push(chalk5.bold.underline(" Platform"));
|
|
2520
|
+
if (pm.nativeModules.length > 0) {
|
|
2521
|
+
lines.push(` Native modules: ${pm.nativeModules.map((m) => chalk5.dim(m)).join(", ")}`);
|
|
2522
|
+
}
|
|
2523
|
+
if (pm.osAssumptions.length > 0) {
|
|
2524
|
+
lines.push(` OS assumptions: ${pm.osAssumptions.join(", ")}`);
|
|
2525
|
+
}
|
|
2526
|
+
lines.push("");
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
if (ext.codeQuality) {
|
|
2530
|
+
const cq = ext.codeQuality;
|
|
2531
|
+
lines.push(chalk5.bold.underline(" Code Quality"));
|
|
2532
|
+
lines.push(` Files: ${chalk5.white(`${cq.filesAnalyzed}`)} \xB7 Functions: ${chalk5.white(`${cq.functionsAnalyzed}`)} \xB7 Avg complexity: ${chalk5.white(`${cq.avgCyclomaticComplexity}`)} \xB7 Avg length: ${chalk5.white(`${cq.avgFunctionLength}`)} lines`);
|
|
2533
|
+
lines.push(` Max nesting: ${cq.maxNestingDepth} \xB7 Circular deps: ${cq.circularDependencies} \xB7 Dead code: ${cq.deadCodePercent}%`);
|
|
2534
|
+
if (cq.godFiles.length > 0) {
|
|
2535
|
+
const preview = cq.godFiles.slice(0, 3).map((f) => `${f.path} (${f.lines} lines)`).join(", ");
|
|
2536
|
+
lines.push(` ${chalk5.yellow("God files")}: ${preview}`);
|
|
2537
|
+
}
|
|
2538
|
+
lines.push("");
|
|
2539
|
+
}
|
|
2540
|
+
if (ext.dependencyGraph) {
|
|
2541
|
+
const dg = ext.dependencyGraph;
|
|
2542
|
+
if (dg.lockfileType) {
|
|
2543
|
+
lines.push(chalk5.bold.underline(" Dependency Graph"));
|
|
2544
|
+
lines.push(` ${dg.lockfileType}: ${chalk5.white(`${dg.totalUnique}`)} unique, ${chalk5.white(`${dg.totalInstalled}`)} installed`);
|
|
2545
|
+
if (dg.duplicatedPackages.length > 0) {
|
|
2546
|
+
lines.push(` ${chalk5.yellow(`${dg.duplicatedPackages.length} duplicated`)} packages`);
|
|
2547
|
+
}
|
|
2548
|
+
if (dg.phantomDependencies.length > 0) {
|
|
2549
|
+
lines.push(` ${chalk5.red(`${dg.phantomDependencies.length} phantom`)} dependencies`);
|
|
2550
|
+
}
|
|
2551
|
+
lines.push("");
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
return lines;
|
|
2555
|
+
}
|
|
2556
|
+
function formatArchitectureDiagram(arch) {
|
|
2557
|
+
const lines = [];
|
|
2558
|
+
lines.push(...titleBox("Architecture Layers"));
|
|
2559
|
+
lines.push("");
|
|
2560
|
+
lines.push(chalk5.bold(" Archetype: ") + `${arch.archetype}` + chalk5.dim(` (${Math.round(arch.archetypeConfidence * 100)}% confidence)`));
|
|
2561
|
+
lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ? chalk5.dim(` (${arch.unclassified} unclassified)`) : ""));
|
|
2562
|
+
lines.push("");
|
|
2563
|
+
if (arch.layers.length > 0) {
|
|
2564
|
+
for (const layer of arch.layers) {
|
|
2565
|
+
const risk = layer.riskLevel === "none" ? chalk5.dim("none") : layer.riskLevel === "low" ? chalk5.green("low") : layer.riskLevel === "moderate" ? chalk5.yellow("moderate") : chalk5.red("high");
|
|
2566
|
+
lines.push(` ${chalk5.bold(layer.layer)} ${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""} drift ${scoreBar(layer.driftScore)} risk ${risk}`);
|
|
2567
|
+
}
|
|
2568
|
+
lines.push("");
|
|
2569
|
+
}
|
|
2570
|
+
return lines;
|
|
2571
|
+
}
|
|
2572
|
+
function generatePriorityActions(artifact) {
|
|
2573
|
+
const actions = [];
|
|
2574
|
+
const eolProjects = artifact.projects.filter(
|
|
2575
|
+
(p) => p.runtimeMajorsBehind !== void 0 && p.runtimeMajorsBehind >= 3
|
|
2576
|
+
);
|
|
2577
|
+
if (eolProjects.length > 0) {
|
|
2578
|
+
const names = eolProjects.map((p) => p.name).join(", ");
|
|
2579
|
+
let detail = `End-of-life runtimes no longer receive security patches and block ecosystem upgrades.`;
|
|
2580
|
+
const fileLines = [];
|
|
2581
|
+
for (const p of eolProjects) {
|
|
2582
|
+
fileLines.push(`
|
|
2583
|
+
./${p.path}`);
|
|
2584
|
+
fileLines.push(`
|
|
2585
|
+
${p.runtime} \u2192 ${p.runtimeLatest} (${p.runtimeMajorsBehind} major${p.runtimeMajorsBehind > 1 ? "s" : ""} behind)`);
|
|
2586
|
+
}
|
|
2587
|
+
detail += fileLines.join("");
|
|
2588
|
+
actions.push({
|
|
2589
|
+
title: `Upgrade EOL runtime${eolProjects.length > 1 ? "s" : ""} in ${names}`,
|
|
2590
|
+
explanation: detail,
|
|
2591
|
+
impact: `\u2212${Math.min(eolProjects.length * 10, 30)} drift points (runtime & EOL scores)`,
|
|
2592
|
+
severity: 100
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
const severeFrameworks = [];
|
|
2596
|
+
for (const p of artifact.projects) {
|
|
2597
|
+
for (const fw of p.frameworks) {
|
|
2598
|
+
if (fw.majorsBehind !== null && fw.majorsBehind >= 3) {
|
|
2599
|
+
severeFrameworks.push({ name: fw.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}`, behind: fw.majorsBehind, project: p.name, projectPath: p.path });
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
if (severeFrameworks.length > 0) {
|
|
2604
|
+
const worst = severeFrameworks.sort((a, b) => b.behind - a.behind)[0];
|
|
2605
|
+
const others = severeFrameworks.length > 1 ? ` (+${severeFrameworks.length - 1} more)` : "";
|
|
2606
|
+
let detail = `${worst.behind} major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.`;
|
|
2607
|
+
const fileLines = [];
|
|
2608
|
+
let shown = 0;
|
|
2609
|
+
for (const sf of severeFrameworks) {
|
|
2610
|
+
if (shown >= 8) break;
|
|
2611
|
+
fileLines.push(`
|
|
2612
|
+
./${sf.projectPath}`);
|
|
2613
|
+
fileLines.push(`
|
|
2614
|
+
${sf.name}: ${sf.fw} (${sf.behind} major${sf.behind > 1 ? "s" : ""} behind)`);
|
|
2615
|
+
shown++;
|
|
2616
|
+
}
|
|
2617
|
+
const remaining = severeFrameworks.length - shown;
|
|
2618
|
+
detail += fileLines.join("");
|
|
2619
|
+
if (remaining > 0) detail += `
|
|
2620
|
+
... and ${remaining} more`;
|
|
2621
|
+
actions.push({
|
|
2622
|
+
title: `Upgrade ${worst.name} ${worst.fw} in ${worst.project}${others}`,
|
|
2623
|
+
explanation: detail,
|
|
2624
|
+
impact: `\u22125\u201315 drift points`,
|
|
2625
|
+
severity: 90
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
for (const p of artifact.projects) {
|
|
2629
|
+
const b = p.dependencyAgeBuckets;
|
|
2630
|
+
const total = b.current + b.oneBehind + b.twoPlusBehind;
|
|
2631
|
+
if (total === 0) continue;
|
|
2632
|
+
const twoPlusPct = Math.round(b.twoPlusBehind / total * 100);
|
|
2633
|
+
if (twoPlusPct >= 40) {
|
|
2634
|
+
let detail = `${b.twoPlusBehind} of ${total} dependencies are 2+ majors behind. Run \`npm outdated\` and prioritise packages with known CVEs or breaking API changes.`;
|
|
2635
|
+
const worstDeps = p.dependencies.filter((d) => d.majorsBehind !== null && d.majorsBehind >= 2).sort((a, b2) => (b2.majorsBehind ?? 0) - (a.majorsBehind ?? 0));
|
|
2636
|
+
if (worstDeps.length > 0) {
|
|
2637
|
+
const depLines = [];
|
|
2638
|
+
let shown = 0;
|
|
2639
|
+
depLines.push(`
|
|
2640
|
+
./${p.path}`);
|
|
2641
|
+
for (const dep of worstDeps) {
|
|
2642
|
+
if (shown >= 8) break;
|
|
2643
|
+
const current = dep.resolvedVersion ?? dep.currentSpec;
|
|
2644
|
+
const latest = dep.latestStable ?? "?";
|
|
2645
|
+
depLines.push(`
|
|
2646
|
+
${dep.package}: ${current} \u2192 ${latest} (${dep.majorsBehind} major${dep.majorsBehind > 1 ? "s" : ""} behind)`);
|
|
2647
|
+
shown++;
|
|
2648
|
+
}
|
|
2649
|
+
const remaining = worstDeps.length - shown;
|
|
2650
|
+
detail += depLines.join("");
|
|
2651
|
+
if (remaining > 0) detail += `
|
|
2652
|
+
... and ${remaining} more`;
|
|
2653
|
+
}
|
|
2654
|
+
actions.push({
|
|
2655
|
+
title: `Reduce dependency rot in ${p.name} (${twoPlusPct}% severely outdated)`,
|
|
2656
|
+
explanation: detail,
|
|
2657
|
+
impact: `\u22125\u201310 drift points`,
|
|
2658
|
+
severity: 80 + twoPlusPct / 10
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
const twoMajorFrameworks = [];
|
|
2663
|
+
for (const p of artifact.projects) {
|
|
2664
|
+
for (const fw of p.frameworks) {
|
|
2665
|
+
if (fw.majorsBehind === 2) {
|
|
2666
|
+
twoMajorFrameworks.push({ name: fw.name, project: p.name, projectPath: p.path, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}` });
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
const uniqueTwo = [...new Map(twoMajorFrameworks.map((f) => [f.name, f])).values()];
|
|
2671
|
+
if (uniqueTwo.length > 0) {
|
|
2672
|
+
const list = uniqueTwo.slice(0, 3).map((f) => `${f.name} (${f.fw})`).join(", ");
|
|
2673
|
+
const moreCount = uniqueTwo.length > 3 ? ` +${uniqueTwo.length - 3} more` : "";
|
|
2674
|
+
let detail = `These frameworks are 2 major versions behind. Create upgrade tickets and check migration guides \u2014 the gap will widen with each new release.`;
|
|
2675
|
+
const fileLines = [];
|
|
2676
|
+
let shown = 0;
|
|
2677
|
+
for (const tf of twoMajorFrameworks) {
|
|
2678
|
+
if (shown >= 8) break;
|
|
2679
|
+
fileLines.push(`
|
|
2680
|
+
./${tf.projectPath}`);
|
|
2681
|
+
fileLines.push(`
|
|
2682
|
+
${tf.name}: ${tf.fw}`);
|
|
2683
|
+
shown++;
|
|
2684
|
+
}
|
|
2685
|
+
const remaining = twoMajorFrameworks.length - shown;
|
|
2686
|
+
detail += fileLines.join("");
|
|
2687
|
+
if (remaining > 0) detail += `
|
|
2688
|
+
... and ${remaining} more`;
|
|
2689
|
+
actions.push({
|
|
2690
|
+
title: `Plan major framework upgrades: ${list}${moreCount}`,
|
|
2691
|
+
explanation: detail,
|
|
2692
|
+
impact: `\u22125\u201310 drift points`,
|
|
2693
|
+
severity: 60
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
if (artifact.extended?.breakingChangeExposure) {
|
|
2697
|
+
const bc = artifact.extended.breakingChangeExposure;
|
|
2698
|
+
const total = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
|
|
2699
|
+
if (total > 0) {
|
|
2700
|
+
const items = [...bc.deprecatedPackages, ...bc.legacyPolyfills].slice(0, 5).join(", ");
|
|
2701
|
+
const moreCount = total > 5 ? ` +${total - 5} more` : "";
|
|
2702
|
+
let detail = `${total} package${total !== 1 ? "s" : ""} are deprecated or legacy polyfills. These receive no updates and may have known vulnerabilities.`;
|
|
2703
|
+
const allPkgNames = /* @__PURE__ */ new Set([...bc.deprecatedPackages, ...bc.legacyPolyfills]);
|
|
2704
|
+
const fileLines = [];
|
|
2705
|
+
let shown = 0;
|
|
2706
|
+
for (const p of artifact.projects) {
|
|
2707
|
+
const matches = p.dependencies.filter((d) => allPkgNames.has(d.package));
|
|
2708
|
+
if (matches.length === 0) continue;
|
|
2709
|
+
if (shown >= 10) break;
|
|
2710
|
+
fileLines.push(`
|
|
2711
|
+
./${p.path}`);
|
|
2712
|
+
for (const dep of matches) {
|
|
2713
|
+
if (shown >= 10) break;
|
|
2714
|
+
const ver = dep.resolvedVersion ?? dep.currentSpec;
|
|
2715
|
+
const label = bc.deprecatedPackages.includes(dep.package) ? "deprecated" : "polyfill";
|
|
2716
|
+
fileLines.push(`
|
|
2717
|
+
${dep.package}: ${ver} (${label})`);
|
|
2718
|
+
shown++;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
const remaining = total - shown;
|
|
2722
|
+
detail += fileLines.join("");
|
|
2723
|
+
if (remaining > 0) detail += `
|
|
2724
|
+
... and ${remaining} more`;
|
|
2725
|
+
actions.push({
|
|
2726
|
+
title: `Replace deprecated/legacy packages: ${items}${moreCount}`,
|
|
2727
|
+
explanation: detail,
|
|
2728
|
+
severity: 55
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
if (artifact.extended?.dependencyGraph) {
|
|
2733
|
+
const dg = artifact.extended.dependencyGraph;
|
|
2734
|
+
const phantomCount = dg.phantomDependencies.length;
|
|
2735
|
+
if (phantomCount >= 10) {
|
|
2736
|
+
let detail = `Packages used in code but not declared in package.json. These rely on transitive installs and can break unpredictably when other packages update.`;
|
|
2737
|
+
const details = dg.phantomDependencyDetails;
|
|
2738
|
+
if (details && details.length > 0) {
|
|
2739
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2740
|
+
for (const d of details) {
|
|
2741
|
+
if (!byPath.has(d.sourcePath)) byPath.set(d.sourcePath, []);
|
|
2742
|
+
byPath.get(d.sourcePath).push({ package: d.package, spec: d.spec });
|
|
2743
|
+
}
|
|
2744
|
+
const pathLines = [];
|
|
2745
|
+
let shown = 0;
|
|
2746
|
+
for (const [srcPath, pkgs] of byPath) {
|
|
2747
|
+
if (shown >= 10) break;
|
|
2748
|
+
pathLines.push(`
|
|
2749
|
+
./${srcPath}`);
|
|
2750
|
+
for (const pkg of pkgs) {
|
|
2751
|
+
if (shown >= 10) break;
|
|
2752
|
+
pathLines.push(`
|
|
2753
|
+
${pkg.package}: ${pkg.spec}`);
|
|
2754
|
+
shown++;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
const remaining = phantomCount - shown;
|
|
2758
|
+
detail += pathLines.join("");
|
|
2759
|
+
if (remaining > 0) detail += `
|
|
2760
|
+
... and ${remaining} more`;
|
|
2761
|
+
}
|
|
2762
|
+
actions.push({
|
|
2763
|
+
title: `Fix ${phantomCount} phantom dependencies`,
|
|
2764
|
+
explanation: detail,
|
|
2765
|
+
severity: 45
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
if (artifact.extended?.securityPosture) {
|
|
2770
|
+
const sec = artifact.extended.securityPosture;
|
|
2771
|
+
if (sec.envFilesTracked || !sec.lockfilePresent) {
|
|
2772
|
+
const issues = [];
|
|
2773
|
+
if (sec.envFilesTracked) issues.push(".env files are tracked in git");
|
|
2774
|
+
if (!sec.lockfilePresent) issues.push("no lockfile found");
|
|
2775
|
+
let detail;
|
|
2776
|
+
if (sec.envFilesTracked) {
|
|
2777
|
+
detail = "Environment files may contain secrets. Add them to .gitignore and rotate any exposed credentials immediately.";
|
|
2778
|
+
detail += "\n ./.gitignore";
|
|
2779
|
+
detail += "\n Add: .env, .env.*, .env.local";
|
|
2780
|
+
} else {
|
|
2781
|
+
detail = "Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.";
|
|
2782
|
+
detail += "\n ./";
|
|
2783
|
+
detail += `
|
|
2784
|
+
Missing: ${sec.lockfileTypes.length > 0 ? sec.lockfileTypes.join(", ") + " (multiple types detected)" : "package-lock.json, pnpm-lock.yaml, or yarn.lock"}`;
|
|
2785
|
+
}
|
|
2786
|
+
actions.push({
|
|
2787
|
+
title: `Fix security posture: ${issues.join(", ")}`,
|
|
2788
|
+
explanation: detail,
|
|
2789
|
+
severity: 95
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
if (artifact.extended?.dependencyGraph) {
|
|
2794
|
+
const dupes = artifact.extended.dependencyGraph.duplicatedPackages;
|
|
2795
|
+
const highImpactDupes = dupes.filter((d) => d.versions.length >= 3);
|
|
2796
|
+
if (highImpactDupes.length >= 3) {
|
|
2797
|
+
let detail = `${highImpactDupes.length} packages have 3+ versions installed. Run \`npm dedupe\` to reduce bundle size and install time.`;
|
|
2798
|
+
const dupeLines = [];
|
|
2799
|
+
let shown = 0;
|
|
2800
|
+
for (const d of highImpactDupes) {
|
|
2801
|
+
if (shown >= 8) break;
|
|
2802
|
+
dupeLines.push(`
|
|
2803
|
+
${d.name}: ${d.versions.join(", ")} (${d.consumers} consumer${d.consumers !== 1 ? "s" : ""})`);
|
|
2804
|
+
shown++;
|
|
2805
|
+
}
|
|
2806
|
+
const remaining = highImpactDupes.length - shown;
|
|
2807
|
+
detail += dupeLines.join("");
|
|
2808
|
+
if (remaining > 0) detail += `
|
|
2809
|
+
... and ${remaining} more`;
|
|
2810
|
+
actions.push({
|
|
2811
|
+
title: `Deduplicate heavily-versioned packages`,
|
|
2812
|
+
explanation: detail,
|
|
2813
|
+
severity: 35
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
actions.sort((a, b) => b.severity - a.severity);
|
|
2818
|
+
return actions.slice(0, 5);
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
// src/reporting/formatters/markdown.ts
|
|
2822
|
+
function formatMarkdown(artifact) {
|
|
2823
|
+
const lines = [];
|
|
2824
|
+
lines.push("# Vibgrate Drift Report");
|
|
2825
|
+
lines.push("");
|
|
2826
|
+
lines.push(`| Metric | Value |`);
|
|
2827
|
+
lines.push(`|--------|-------|`);
|
|
2828
|
+
lines.push(`| **Drift Score** | ${artifact.drift.score}/100 _(lower is better; 0 = no drift)_ |`);
|
|
2829
|
+
lines.push(`| **Risk Level** | ${artifact.drift.riskLevel.toUpperCase()} |`);
|
|
2830
|
+
lines.push(`| **Projects** | ${artifact.projects.length} |`);
|
|
2831
|
+
const scannedMeta = [artifact.timestamp];
|
|
2832
|
+
if (artifact.durationMs !== void 0) scannedMeta.push(`${(artifact.durationMs / 1e3).toFixed(1)}s`);
|
|
2833
|
+
if (artifact.filesScanned !== void 0) scannedMeta.push(`${artifact.filesScanned} files`);
|
|
2834
|
+
if (artifact.treeSummary) scannedMeta.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files \xB7 ${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
|
|
2835
|
+
lines.push(`| **Scanned** | ${scannedMeta.join(" \xB7 ")} |`);
|
|
2836
|
+
if (artifact.vcs) {
|
|
2837
|
+
lines.push(`| **VCS** | ${artifact.vcs.type} |`);
|
|
2838
|
+
if (artifact.vcs.branch) lines.push(`| **Branch** | ${artifact.vcs.branch} |`);
|
|
2839
|
+
if (artifact.vcs.sha) lines.push(`| **Commit** | \`${artifact.vcs.shortSha}\` |`);
|
|
2840
|
+
}
|
|
2841
|
+
lines.push("");
|
|
2842
|
+
lines.push("## Score Breakdown");
|
|
2843
|
+
lines.push("");
|
|
2844
|
+
lines.push(`| Component | Score |`);
|
|
2845
|
+
lines.push(`|-----------|-------|`);
|
|
2846
|
+
lines.push(`| Runtime | ${artifact.drift.components.runtimeScore} |`);
|
|
2847
|
+
lines.push(`| Frameworks | ${artifact.drift.components.frameworkScore} |`);
|
|
2848
|
+
lines.push(`| Dependencies | ${artifact.drift.components.dependencyScore} |`);
|
|
2849
|
+
lines.push(`| EOL Risk | ${artifact.drift.components.eolScore} |`);
|
|
2850
|
+
lines.push("");
|
|
2851
|
+
lines.push("## Projects");
|
|
2852
|
+
lines.push("");
|
|
2853
|
+
for (const project of artifact.projects) {
|
|
2854
|
+
lines.push(`### ${project.name} (${project.type})`);
|
|
2855
|
+
lines.push("");
|
|
2856
|
+
if (project.runtime) {
|
|
2857
|
+
const lag = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ? ` \u2014 ${project.runtimeMajorsBehind} major(s) behind` : " \u2014 current";
|
|
2858
|
+
lines.push(`- **Runtime:** ${project.runtime}${lag}`);
|
|
2859
|
+
}
|
|
2860
|
+
if (project.frameworks.length > 0) {
|
|
2861
|
+
lines.push("- **Frameworks:**");
|
|
2862
|
+
for (const fw of project.frameworks) {
|
|
2863
|
+
const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ? "current" : `${fw.majorsBehind} behind` : "unknown";
|
|
2864
|
+
lines.push(` - ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
const b = project.dependencyAgeBuckets;
|
|
2868
|
+
const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
|
|
2869
|
+
if (total > 0) {
|
|
2870
|
+
lines.push(`- **Dependencies:** ${b.current} current, ${b.oneBehind} 1-behind, ${b.twoPlusBehind} 2+ behind, ${b.unknown} unknown`);
|
|
2871
|
+
}
|
|
2872
|
+
lines.push("");
|
|
2873
|
+
}
|
|
2874
|
+
if (artifact.extended?.uiPurpose) {
|
|
2875
|
+
const up = artifact.extended.uiPurpose;
|
|
2876
|
+
lines.push("## Product Purpose Signals");
|
|
2877
|
+
lines.push("");
|
|
2878
|
+
lines.push(`- **Frameworks:** ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : "unknown"}`);
|
|
2879
|
+
lines.push(`- **Evidence Items:** ${up.topEvidence.length}${up.capped ? ` (capped from ${up.evidenceCount})` : ""}`);
|
|
2880
|
+
if (up.topEvidence.length > 0) {
|
|
2881
|
+
lines.push("- **Top Evidence:**");
|
|
2882
|
+
for (const item of up.topEvidence.slice(0, 10)) {
|
|
2883
|
+
lines.push(` - [${item.kind}] ${item.value} (${item.file})`);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
if (up.unknownSignals.length > 0) {
|
|
2887
|
+
lines.push("- **Unknowns:**");
|
|
2888
|
+
for (const u of up.unknownSignals.slice(0, 5)) {
|
|
2889
|
+
lines.push(` - ${u}`);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
lines.push("");
|
|
2893
|
+
}
|
|
2894
|
+
if (artifact.extended?.standards && artifact.extended.standards.recommended.length > 0) {
|
|
2895
|
+
const std = artifact.extended.standards;
|
|
2896
|
+
lines.push("## Recommended Standards");
|
|
2897
|
+
lines.push("");
|
|
2898
|
+
const purposes = std.projectPurposes.map((p) => `${p.project} \u2192 ${p.category}`).join(", ");
|
|
2899
|
+
if (purposes) lines.push(`- **Detected purpose:** ${purposes}`);
|
|
2900
|
+
if (std.frameworks.length > 0) {
|
|
2901
|
+
lines.push("- **Compliance framework coverage:**");
|
|
2902
|
+
for (const f of std.frameworks) {
|
|
2903
|
+
lines.push(` - ${f.name}: ${f.recommendedMembers}/${f.totalMembers} member standards apply`);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
lines.push("- **Top standards to consider:**");
|
|
2907
|
+
for (const rec of std.recommended.slice(0, 10)) {
|
|
2908
|
+
const flag = rec.complianceRelevant ? " _(compliance)_" : "";
|
|
2909
|
+
lines.push(` - **${rec.name}** \u2014 ${rec.reason}${flag}`);
|
|
2910
|
+
}
|
|
2911
|
+
lines.push("");
|
|
2912
|
+
}
|
|
2913
|
+
if (artifact.findings.length > 0) {
|
|
2914
|
+
lines.push("## Findings");
|
|
2915
|
+
lines.push("");
|
|
2916
|
+
lines.push(`| Level | Rule | Message | Location |`);
|
|
2917
|
+
lines.push(`|-------|------|---------|----------|`);
|
|
2918
|
+
for (const f of artifact.findings) {
|
|
2919
|
+
const emoji = f.level === "error" ? "\u{1F534}" : f.level === "warning" ? "\u{1F7E1}" : "\u{1F535}";
|
|
2920
|
+
lines.push(`| ${emoji} ${f.level} | ${f.ruleId} | ${f.message} | ${f.location} |`);
|
|
2921
|
+
}
|
|
2922
|
+
lines.push("");
|
|
2923
|
+
}
|
|
2924
|
+
if (artifact.delta !== void 0) {
|
|
2925
|
+
const dir = artifact.delta > 0 ? "\u{1F4C8}" : artifact.delta < 0 ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
2926
|
+
const trend = artifact.delta > 0 ? " _(worsened)_" : artifact.delta < 0 ? " _(improved)_" : "";
|
|
2927
|
+
lines.push(`## Drift Delta: ${dir} ${artifact.delta > 0 ? "+" : ""}${artifact.delta} vs baseline${trend}`);
|
|
2928
|
+
lines.push("");
|
|
2929
|
+
}
|
|
2930
|
+
return lines.join("\n");
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// src/reporting/commands/report.ts
|
|
2934
|
+
var reportCommand = new Command("report").description("Generate a drift report from a scan artifact").option("--in <file>", "Input artifact file", ".vibgrate/scan_result.json").option("--format <format>", "Output format (md|text|json)", "text").action(async (opts) => {
|
|
2935
|
+
const artifactPath = path9.resolve(opts.in);
|
|
2936
|
+
if (!await pathExists(artifactPath)) {
|
|
2937
|
+
console.error(chalk5.red(`Artifact not found: ${artifactPath}`));
|
|
2938
|
+
console.error(chalk5.dim('Run "vibgrate scan" first to generate a scan artifact.'));
|
|
2939
|
+
process.exit(1);
|
|
2940
|
+
}
|
|
2941
|
+
const artifact = await readJsonFile(artifactPath);
|
|
2942
|
+
switch (opts.format) {
|
|
2943
|
+
case "md":
|
|
2944
|
+
console.log(formatMarkdown(artifact));
|
|
2945
|
+
break;
|
|
2946
|
+
case "json":
|
|
2947
|
+
console.log(JSON.stringify(artifact, null, 2));
|
|
2948
|
+
break;
|
|
2949
|
+
case "text":
|
|
2950
|
+
default:
|
|
2951
|
+
console.log(formatText(artifact));
|
|
2952
|
+
break;
|
|
2953
|
+
}
|
|
2954
|
+
});
|
|
2955
|
+
function openUrl(url) {
|
|
2956
|
+
const platform = process.platform;
|
|
2957
|
+
let command;
|
|
2958
|
+
let args;
|
|
2959
|
+
if (platform === "darwin") {
|
|
2960
|
+
command = "open";
|
|
2961
|
+
args = [url];
|
|
2962
|
+
} else if (platform === "win32") {
|
|
2963
|
+
command = "cmd";
|
|
2964
|
+
args = ["/c", "start", "", url];
|
|
2965
|
+
} else {
|
|
2966
|
+
command = "xdg-open";
|
|
2967
|
+
args = [url];
|
|
2968
|
+
}
|
|
2969
|
+
try {
|
|
2970
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
2971
|
+
child.on("error", () => {
|
|
2972
|
+
});
|
|
2973
|
+
child.unref();
|
|
2974
|
+
return true;
|
|
2975
|
+
} catch {
|
|
2976
|
+
return false;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
// src/reporting/commands/login.ts
|
|
2981
|
+
var delay = (ms) => new Promise((resolve15) => setTimeout(resolve15, ms));
|
|
2982
|
+
var loginCommand = new Command("login").description("Authenticate the CLI with your Vibgrate workspace via the browser").option("--ingest <url>", "Ingest API URL (overrides --region)").option("--region <region>", `Data residency region (${availableRegionIds().join(", ")})`, "us").option("--no-browser", "Do not attempt to open a browser automatically").action(async (opts) => {
|
|
2983
|
+
let ingestHost;
|
|
2984
|
+
try {
|
|
2985
|
+
ingestHost = resolveIngestHost(opts.region, opts.ingest);
|
|
2986
|
+
} catch (e) {
|
|
2987
|
+
console.error(chalk5.red(e instanceof Error ? e.message : String(e)));
|
|
2988
|
+
process.exit(1);
|
|
2989
|
+
}
|
|
2990
|
+
const base = `https://${ingestHost}/v1/auth/device`;
|
|
2991
|
+
let start;
|
|
2992
|
+
try {
|
|
2993
|
+
const res = await fetch(`${base}/start`, {
|
|
2994
|
+
method: "POST",
|
|
2995
|
+
headers: { "Content-Type": "application/json", Connection: "close" },
|
|
2996
|
+
body: "{}"
|
|
2997
|
+
});
|
|
2998
|
+
if (!res.ok) {
|
|
2999
|
+
console.error(chalk5.red(`Failed to start login (HTTP ${res.status}).`));
|
|
3000
|
+
process.exit(1);
|
|
3001
|
+
}
|
|
3002
|
+
start = await res.json();
|
|
3003
|
+
} catch (e) {
|
|
3004
|
+
console.error(chalk5.red(`Could not reach ${ingestHost}: ${e instanceof Error ? e.message : String(e)}`));
|
|
3005
|
+
process.exit(1);
|
|
3006
|
+
}
|
|
3007
|
+
console.log("");
|
|
3008
|
+
console.log("To finish signing in, open this URL and approve the request:");
|
|
3009
|
+
console.log("");
|
|
3010
|
+
console.log(" " + chalk5.cyan(start.verificationUri));
|
|
3011
|
+
console.log("");
|
|
3012
|
+
console.log(" Your code: " + chalk5.bold(start.userCode));
|
|
3013
|
+
console.log("");
|
|
3014
|
+
if (opts.browser) {
|
|
3015
|
+
const opened = openUrl(start.verificationUriComplete);
|
|
3016
|
+
if (opened) {
|
|
3017
|
+
console.log(chalk5.dim("Opening your browser\u2026 (if it does not open, use the URL above)"));
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
console.log(chalk5.dim("Waiting for approval\u2026"));
|
|
3021
|
+
const intervalMs = Math.max(2, start.interval || 5) * 1e3;
|
|
3022
|
+
const deadline = Date.now() + (start.expiresIn || 900) * 1e3;
|
|
3023
|
+
while (Date.now() < deadline) {
|
|
3024
|
+
await delay(intervalMs);
|
|
3025
|
+
let token;
|
|
3026
|
+
try {
|
|
3027
|
+
const res = await fetch(`${base}/token`, {
|
|
3028
|
+
method: "POST",
|
|
3029
|
+
headers: { "Content-Type": "application/json", Connection: "close" },
|
|
3030
|
+
body: JSON.stringify({ deviceCode: start.deviceCode })
|
|
3031
|
+
});
|
|
3032
|
+
token = await res.json();
|
|
3033
|
+
} catch {
|
|
3034
|
+
continue;
|
|
3035
|
+
}
|
|
3036
|
+
if (token.status === "authorization_pending") continue;
|
|
3037
|
+
if (token.status === "complete") {
|
|
3038
|
+
let creds;
|
|
3039
|
+
if (token.dsn) {
|
|
3040
|
+
creds = {
|
|
3041
|
+
dsn: token.dsn,
|
|
3042
|
+
workspaceId: token.workspaceId,
|
|
3043
|
+
keyId: token.keyId,
|
|
3044
|
+
ingestHost: token.ingestHost ?? ingestHost,
|
|
3045
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3046
|
+
};
|
|
3047
|
+
} else {
|
|
3048
|
+
console.log(chalk5.dim("Setting up your workspace\u2026"));
|
|
3049
|
+
try {
|
|
3050
|
+
const provisioned = await createWorkspaceDsn({
|
|
3051
|
+
region: opts.region,
|
|
3052
|
+
ingest: opts.ingest
|
|
3053
|
+
});
|
|
3054
|
+
creds = {
|
|
3055
|
+
dsn: provisioned.dsn,
|
|
3056
|
+
workspaceId: provisioned.workspaceId,
|
|
3057
|
+
keyId: provisioned.keyId,
|
|
3058
|
+
ingestHost: provisioned.ingestHost,
|
|
3059
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3060
|
+
};
|
|
3061
|
+
} catch (e) {
|
|
3062
|
+
console.error(
|
|
3063
|
+
chalk5.red("\u2716 Signed in, but workspace setup failed: ") + (e instanceof Error ? e.message : String(e))
|
|
3064
|
+
);
|
|
3065
|
+
console.error(
|
|
3066
|
+
chalk5.dim(' Finish setup with "vibgrate dsn create --workspace new".')
|
|
3067
|
+
);
|
|
3068
|
+
process.exit(1);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
writeStoredCredentials(creds);
|
|
3072
|
+
console.log("");
|
|
3073
|
+
console.log(chalk5.green("\u2714") + " Logged in.");
|
|
3074
|
+
if (creds.workspaceId) {
|
|
3075
|
+
console.log(" Workspace: " + chalk5.bold(creds.workspaceId));
|
|
3076
|
+
}
|
|
3077
|
+
console.log(chalk5.dim(` Credentials saved to ${credentialsPath()}`));
|
|
3078
|
+
try {
|
|
3079
|
+
const root = findGitRoot();
|
|
3080
|
+
if (root) {
|
|
3081
|
+
const res = ensureGitignored(gitignoreEntryForCredentials(root), root);
|
|
3082
|
+
if (res.status === "created") {
|
|
3083
|
+
console.log(chalk5.dim(` Created .gitignore and ignored ${res.entry}`));
|
|
3084
|
+
} else if (res.status === "added") {
|
|
3085
|
+
console.log(chalk5.dim(` Added ${res.entry} to .gitignore`));
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
} catch {
|
|
3089
|
+
}
|
|
3090
|
+
console.log(chalk5.dim(' You can now run "vibgrate scan --push".'));
|
|
3091
|
+
return;
|
|
3092
|
+
}
|
|
3093
|
+
if (token.status === "access_denied") {
|
|
3094
|
+
console.error(chalk5.red("\u2716 Login was denied in the browser."));
|
|
3095
|
+
process.exit(1);
|
|
3096
|
+
}
|
|
3097
|
+
if (token.status === "expired" || token.status === "invalid") {
|
|
3098
|
+
console.error(chalk5.red('\u2716 Login request expired. Run "vibgrate login" again.'));
|
|
3099
|
+
process.exit(1);
|
|
3100
|
+
}
|
|
3101
|
+
if (token.status === "error") {
|
|
3102
|
+
console.error(chalk5.red(`\u2716 ${token.error ?? "Login failed."}`));
|
|
3103
|
+
process.exit(1);
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
console.error(chalk5.red('\u2716 Timed out waiting for approval. Run "vibgrate login" again.'));
|
|
3107
|
+
process.exit(1);
|
|
3108
|
+
});
|
|
3109
|
+
|
|
3110
|
+
// src/reporting/utils/shell.ts
|
|
3111
|
+
function unsetEnvCommand(varName, env = {}) {
|
|
3112
|
+
const platform = env.platform ?? process.platform;
|
|
3113
|
+
if (platform === "win32") {
|
|
3114
|
+
return `PowerShell: Remove-Item Env:${varName} | cmd.exe: set ${varName}=`;
|
|
3115
|
+
}
|
|
3116
|
+
const shell = env.shell ?? process.env.SHELL ?? "";
|
|
3117
|
+
const base = shell.split("/").pop() ?? "";
|
|
3118
|
+
if (base === "fish") return `set -e ${varName}`;
|
|
3119
|
+
if (base === "csh" || base === "tcsh") return `unsetenv ${varName}`;
|
|
3120
|
+
return `unset ${varName}`;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
// src/reporting/commands/logout.ts
|
|
3124
|
+
var logoutCommand = new Command("logout").description("Clear stored Vibgrate login credentials").action(() => {
|
|
3125
|
+
const cleared = clearStoredCredentials();
|
|
3126
|
+
if (cleared) {
|
|
3127
|
+
console.log(chalk5.green("\u2714") + " Logged out. Stored credentials removed.");
|
|
3128
|
+
} else {
|
|
3129
|
+
console.log(chalk5.dim(`No stored credentials found at ${credentialsPath()}.`));
|
|
3130
|
+
}
|
|
3131
|
+
if (process.env.VIBGRATE_DSN) {
|
|
3132
|
+
console.log("");
|
|
3133
|
+
console.log(
|
|
3134
|
+
chalk5.yellow("\u26A0") + " VIBGRATE_DSN is still set in your environment, so commands will keep using it."
|
|
3135
|
+
);
|
|
3136
|
+
console.log(
|
|
3137
|
+
chalk5.dim(` Unset it to fully sign out: ${unsetEnvCommand("VIBGRATE_DSN")}`)
|
|
3138
|
+
);
|
|
3139
|
+
}
|
|
3140
|
+
});
|
|
3141
|
+
var REGISTRY_URL = "https://registry.npmjs.org/@vibgrate%2fcli/latest";
|
|
3142
|
+
var CACHE_DIR = path9.join(os.homedir(), ".vibgrate");
|
|
3143
|
+
var CACHE_FILE = path9.join(CACHE_DIR, "update-check.json");
|
|
3144
|
+
async function fetchLatestVersion() {
|
|
3145
|
+
try {
|
|
3146
|
+
const controller = new AbortController();
|
|
3147
|
+
const timeout = setTimeout(() => controller.abort(), 1e4);
|
|
3148
|
+
timeout.unref?.();
|
|
3149
|
+
let response;
|
|
3150
|
+
try {
|
|
3151
|
+
response = await fetch(REGISTRY_URL, {
|
|
3152
|
+
headers: { Accept: "application/json" },
|
|
3153
|
+
signal: controller.signal
|
|
3154
|
+
});
|
|
3155
|
+
} finally {
|
|
3156
|
+
clearTimeout(timeout);
|
|
3157
|
+
}
|
|
3158
|
+
if (!response.ok) return null;
|
|
3159
|
+
const data = await response.json();
|
|
3160
|
+
const latest = data.version;
|
|
3161
|
+
if (!latest || !semver.valid(latest)) return null;
|
|
3162
|
+
await writeCache({ latest, checkedAt: Date.now() });
|
|
3163
|
+
return latest;
|
|
3164
|
+
} catch {
|
|
3165
|
+
return null;
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
async function writeCache(data) {
|
|
3169
|
+
try {
|
|
3170
|
+
await fs10.mkdir(CACHE_DIR, { recursive: true });
|
|
3171
|
+
await fs10.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
3172
|
+
} catch {
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// src/reporting/commands/update.ts
|
|
3177
|
+
function detectGlobalInstall() {
|
|
3178
|
+
const execPath = process.argv[1] || "";
|
|
3179
|
+
if (execPath.includes("/lib/node_modules/") || execPath.includes("\\node_modules\\")) {
|
|
3180
|
+
if (!execPath.includes(process.cwd())) {
|
|
3181
|
+
if (execPath.includes("pnpm")) return "pnpm";
|
|
3182
|
+
if (execPath.includes("yarn")) return "yarn";
|
|
3183
|
+
if (execPath.includes("bun")) return "bun";
|
|
3184
|
+
return "npm";
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
return null;
|
|
3188
|
+
}
|
|
3189
|
+
function getGlobalUpdateCommand(pm, pkg, version) {
|
|
3190
|
+
const spec = `${pkg}@${version}`;
|
|
3191
|
+
switch (pm) {
|
|
3192
|
+
case "pnpm":
|
|
3193
|
+
return `pnpm add -g ${spec}`;
|
|
3194
|
+
case "yarn":
|
|
3195
|
+
return `yarn global add ${spec}`;
|
|
3196
|
+
case "bun":
|
|
3197
|
+
return `bun add -g ${spec}`;
|
|
3198
|
+
case "npm":
|
|
3199
|
+
default:
|
|
3200
|
+
return `npm install -g ${spec}`;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
async function detectPackageManager(cwd) {
|
|
3204
|
+
if (await pathExists(path9.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
3205
|
+
if (await pathExists(path9.join(cwd, "bun.lockb"))) return "bun";
|
|
3206
|
+
if (await pathExists(path9.join(cwd, "yarn.lock"))) return "yarn";
|
|
3207
|
+
return "npm";
|
|
3208
|
+
}
|
|
3209
|
+
function getInstallCommand(pm, pkg, version, isDev) {
|
|
3210
|
+
const spec = `${pkg}@${version}`;
|
|
3211
|
+
switch (pm) {
|
|
3212
|
+
case "pnpm":
|
|
3213
|
+
return isDev ? `pnpm add -D ${spec}` : `pnpm add ${spec}`;
|
|
3214
|
+
case "yarn":
|
|
3215
|
+
return isDev ? `yarn add --dev ${spec}` : `yarn add ${spec}`;
|
|
3216
|
+
case "bun":
|
|
3217
|
+
return isDev ? `bun add -d ${spec}` : `bun add ${spec}`;
|
|
3218
|
+
case "npm":
|
|
3219
|
+
default:
|
|
3220
|
+
return isDev ? `npm install --save-dev ${spec}` : `npm install ${spec}`;
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
async function isDevDependency(cwd) {
|
|
3224
|
+
try {
|
|
3225
|
+
const pkgPath = path9.join(cwd, "package.json");
|
|
3226
|
+
const raw = await (await import('fs/promises')).readFile(pkgPath, "utf-8");
|
|
3227
|
+
const pkg = JSON.parse(raw);
|
|
3228
|
+
return Boolean(pkg.devDependencies?.["@vibgrate/cli"]);
|
|
3229
|
+
} catch {
|
|
3230
|
+
return true;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
var updateCommand = new Command("update").description("Update vibgrate to the latest version").option("--check", "Only check for updates, do not install").option("--pm <manager>", "Package manager to use (npm, pnpm, yarn, bun)").option("--global", "Update global installation").action(async (opts) => {
|
|
3234
|
+
console.log(chalk5.dim(`Current version: ${VERSION}`));
|
|
3235
|
+
console.log(chalk5.dim("Checking npm registry..."));
|
|
3236
|
+
const latest = await fetchLatestVersion();
|
|
3237
|
+
if (!latest) {
|
|
3238
|
+
console.error(chalk5.red("Could not reach the npm registry. Check your network connection."));
|
|
3239
|
+
process.exit(1);
|
|
3240
|
+
}
|
|
3241
|
+
const semver2 = await import('semver');
|
|
3242
|
+
if (!semver2.gt(latest, VERSION)) {
|
|
3243
|
+
console.log(chalk5.green("\u2714") + ` You are on the latest version (${VERSION}).`);
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
console.log(chalk5.yellow(`Update available: ${VERSION} \u2192 ${latest}`));
|
|
3247
|
+
if (opts.check) {
|
|
3248
|
+
console.log(chalk5.dim('Run "vibgrate update" to install.'));
|
|
3249
|
+
return;
|
|
3250
|
+
}
|
|
3251
|
+
const cwd = process.cwd();
|
|
3252
|
+
const globalPm = detectGlobalInstall();
|
|
3253
|
+
const isGlobal = opts.global || globalPm !== null;
|
|
3254
|
+
const pm = opts.pm || (globalPm ?? await detectPackageManager(cwd));
|
|
3255
|
+
let cmd;
|
|
3256
|
+
if (isGlobal) {
|
|
3257
|
+
cmd = getGlobalUpdateCommand(pm, "@vibgrate/cli", latest);
|
|
3258
|
+
console.log(chalk5.dim(`Updating global installation with ${pm}: ${cmd}`));
|
|
3259
|
+
} else {
|
|
3260
|
+
const isDev = await isDevDependency(cwd);
|
|
3261
|
+
cmd = getInstallCommand(pm, "@vibgrate/cli", latest, isDev);
|
|
3262
|
+
console.log(chalk5.dim(`Using ${pm}: ${cmd}`));
|
|
3263
|
+
}
|
|
3264
|
+
try {
|
|
3265
|
+
execSync(cmd, { cwd, stdio: "inherit" });
|
|
3266
|
+
console.log(chalk5.green("\u2714") + ` Updated to @vibgrate/cli@${latest}`);
|
|
3267
|
+
} catch {
|
|
3268
|
+
console.error(chalk5.red(`Update failed. Run manually: ${cmd}`));
|
|
3269
|
+
process.exit(1);
|
|
3270
|
+
}
|
|
3271
|
+
});
|
|
3272
|
+
var OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
|
|
3273
|
+
var VEX_STATUSES = ["not_affected", "affected", "fixed", "under_investigation"];
|
|
3274
|
+
var VEX_JUSTIFICATIONS = [
|
|
3275
|
+
"component_not_present",
|
|
3276
|
+
"vulnerable_code_not_present",
|
|
3277
|
+
"vulnerable_code_not_in_execute_path",
|
|
3278
|
+
"vulnerable_code_cannot_be_controlled_by_adversary",
|
|
3279
|
+
"inline_mitigations_already_exist"
|
|
3280
|
+
];
|
|
3281
|
+
var VexValidationError = class extends Error {
|
|
3282
|
+
};
|
|
3283
|
+
function buildVexDocument(opts) {
|
|
3284
|
+
const statements = opts.statements.map((s, i) => {
|
|
3285
|
+
if (!s.vulnerability || typeof s.vulnerability !== "string") {
|
|
3286
|
+
throw new VexValidationError(`statement[${i}]: "vulnerability" is required`);
|
|
3287
|
+
}
|
|
3288
|
+
if (!VEX_STATUSES.includes(s.status)) {
|
|
3289
|
+
throw new VexValidationError(
|
|
3290
|
+
`statement[${i}] (${s.vulnerability}): invalid status "${s.status}". Expected one of: ${VEX_STATUSES.join(", ")}`
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
const products = s.products && s.products.length > 0 ? s.products : opts.defaultProduct ? [opts.defaultProduct] : [];
|
|
3294
|
+
if (products.length === 0) {
|
|
3295
|
+
throw new VexValidationError(
|
|
3296
|
+
`statement[${i}] (${s.vulnerability}): no product. Set "products" on the statement or pass --product.`
|
|
3297
|
+
);
|
|
3298
|
+
}
|
|
3299
|
+
if (s.status === "not_affected" && !s.justification && !s.impact_statement) {
|
|
3300
|
+
throw new VexValidationError(
|
|
3301
|
+
`statement[${i}] (${s.vulnerability}): "not_affected" requires a justification or impact_statement.`
|
|
3302
|
+
);
|
|
3303
|
+
}
|
|
3304
|
+
if (s.justification && !VEX_JUSTIFICATIONS.includes(s.justification)) {
|
|
3305
|
+
throw new VexValidationError(
|
|
3306
|
+
`statement[${i}] (${s.vulnerability}): invalid justification "${s.justification}". Expected one of: ${VEX_JUSTIFICATIONS.join(", ")}`
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
3309
|
+
if (s.status === "affected" && !s.action_statement) {
|
|
3310
|
+
throw new VexValidationError(
|
|
3311
|
+
`statement[${i}] (${s.vulnerability}): "affected" requires an action_statement.`
|
|
3312
|
+
);
|
|
3313
|
+
}
|
|
3314
|
+
const out2 = {
|
|
3315
|
+
vulnerability: { name: s.vulnerability },
|
|
3316
|
+
products: products.map((p) => ({ "@id": p })),
|
|
3317
|
+
status: s.status
|
|
3318
|
+
};
|
|
3319
|
+
if (s.justification) out2.justification = s.justification;
|
|
3320
|
+
if (s.impact_statement) out2.impact_statement = s.impact_statement;
|
|
3321
|
+
if (s.action_statement) out2.action_statement = s.action_statement;
|
|
3322
|
+
return out2;
|
|
3323
|
+
});
|
|
3324
|
+
return {
|
|
3325
|
+
"@context": OPENVEX_CONTEXT,
|
|
3326
|
+
"@id": opts.id ?? `https://vibgrate.com/vex/${randomUUID()}`,
|
|
3327
|
+
author: opts.author,
|
|
3328
|
+
timestamp: opts.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3329
|
+
version: 1,
|
|
3330
|
+
statements
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
async function loadStatementsFile(filePath) {
|
|
3334
|
+
const data = await readJsonFile(filePath);
|
|
3335
|
+
const arr = Array.isArray(data) ? data : data && typeof data === "object" && Array.isArray(data.statements) ? data.statements : null;
|
|
3336
|
+
if (!arr) {
|
|
3337
|
+
throw new VexValidationError(
|
|
3338
|
+
`${filePath}: expected a JSON array of statements or an object with a "statements" array.`
|
|
3339
|
+
);
|
|
3340
|
+
}
|
|
3341
|
+
return arr;
|
|
3342
|
+
}
|
|
3343
|
+
function collectStatement(value, previous) {
|
|
3344
|
+
let parsed;
|
|
3345
|
+
try {
|
|
3346
|
+
parsed = JSON.parse(value);
|
|
3347
|
+
} catch {
|
|
3348
|
+
throw new VexValidationError(`--statement must be a JSON object: ${value.slice(0, 60)}\u2026`);
|
|
3349
|
+
}
|
|
3350
|
+
return [...previous, parsed];
|
|
3351
|
+
}
|
|
3352
|
+
var vexCommand = new Command("vex").description("Generate an OpenVEX document (exploitability statements) for attestation").option("--from <file>", 'Read statements from a JSON file (array, or an object with a "statements" array)').option(
|
|
3353
|
+
"--statement <json>",
|
|
3354
|
+
`Add a statement as inline JSON, e.g. '{"vulnerability":"CVE-2024-1","status":"not_affected","justification":"vulnerable_code_not_present"}'. Repeatable.`,
|
|
3355
|
+
collectStatement,
|
|
3356
|
+
[]
|
|
3357
|
+
).option("--product <ref>", "Default product reference applied to statements without their own (e.g. an image digest)").option("--author <name>", "Document author", "Vibgrate").option("--timestamp <iso>", "Override the document timestamp (ISO 8601) for reproducible output").option("--id <uri>", "Override the document @id (otherwise a uuid URN is generated)").option("--out <file>", "Write the document to a file (default: stdout)").action(async (opts) => {
|
|
3358
|
+
try {
|
|
3359
|
+
const statements = [];
|
|
3360
|
+
if (opts.from) {
|
|
3361
|
+
const fromPath = path9.resolve(opts.from);
|
|
3362
|
+
if (!await pathExists(fromPath)) {
|
|
3363
|
+
process.stderr.write(chalk5.red(`Statements file not found: ${fromPath}
|
|
3364
|
+
`));
|
|
3365
|
+
process.exit(1);
|
|
3366
|
+
}
|
|
3367
|
+
statements.push(...await loadStatementsFile(fromPath));
|
|
3368
|
+
}
|
|
3369
|
+
statements.push(...opts.statement);
|
|
3370
|
+
const doc = buildVexDocument({
|
|
3371
|
+
author: opts.author,
|
|
3372
|
+
defaultProduct: opts.product,
|
|
3373
|
+
timestamp: opts.timestamp,
|
|
3374
|
+
id: opts.id,
|
|
3375
|
+
statements
|
|
3376
|
+
});
|
|
3377
|
+
const json2 = JSON.stringify(doc, null, 2);
|
|
3378
|
+
if (opts.out) {
|
|
3379
|
+
await writeTextFile(path9.resolve(opts.out), json2 + "\n");
|
|
3380
|
+
process.stderr.write(
|
|
3381
|
+
chalk5.green("\u2714") + ` Wrote OpenVEX document (${doc.statements.length} statement(s)) to ${path9.resolve(opts.out)}
|
|
3382
|
+
`
|
|
3383
|
+
);
|
|
3384
|
+
} else {
|
|
3385
|
+
process.stdout.write(json2 + "\n");
|
|
3386
|
+
}
|
|
3387
|
+
} catch (err) {
|
|
3388
|
+
if (err instanceof VexValidationError) {
|
|
3389
|
+
process.stderr.write(chalk5.red(`VEX validation error: ${err.message}
|
|
3390
|
+
`));
|
|
3391
|
+
process.exit(1);
|
|
3392
|
+
}
|
|
3393
|
+
throw err;
|
|
3394
|
+
}
|
|
3395
|
+
});
|
|
3396
|
+
|
|
3397
|
+
// src/reporting/commands/sbom.ts
|
|
3398
|
+
function deterministicUuid(seed) {
|
|
3399
|
+
const bytes = new Uint8Array(16);
|
|
3400
|
+
for (let block = 0; block < 4; block++) {
|
|
3401
|
+
let h = 2166136261;
|
|
3402
|
+
const s = `${block} ${seed}`;
|
|
3403
|
+
for (let i = 0; i < s.length; i++) {
|
|
3404
|
+
h ^= s.charCodeAt(i);
|
|
3405
|
+
h = Math.imul(h, 16777619);
|
|
3406
|
+
}
|
|
3407
|
+
h >>>= 0;
|
|
3408
|
+
bytes[block * 4] = h >>> 24 & 255;
|
|
3409
|
+
bytes[block * 4 + 1] = h >>> 16 & 255;
|
|
3410
|
+
bytes[block * 4 + 2] = h >>> 8 & 255;
|
|
3411
|
+
bytes[block * 4 + 3] = h & 255;
|
|
3412
|
+
}
|
|
3413
|
+
bytes[6] = bytes[6] & 15 | 128;
|
|
3414
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
3415
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
3416
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
3417
|
+
}
|
|
3418
|
+
function sbomSerialSeed(format, artifact, deps) {
|
|
3419
|
+
return [
|
|
3420
|
+
format,
|
|
3421
|
+
artifact.rootPath ?? "",
|
|
3422
|
+
artifact.timestamp ?? "",
|
|
3423
|
+
artifact.vibgrateVersion ?? "",
|
|
3424
|
+
...deps.map((d) => `${d.package}|${d.version}|${d.currentSpec}|${d.project}|${d.drift}|${d.majorsBehind ?? ""}`)
|
|
3425
|
+
].join("\n");
|
|
3426
|
+
}
|
|
3427
|
+
function flattenDependencies(artifact) {
|
|
3428
|
+
const rows = [];
|
|
3429
|
+
for (const project of artifact.projects) {
|
|
3430
|
+
for (const dep of project.dependencies) {
|
|
3431
|
+
rows.push({
|
|
3432
|
+
project: project.name,
|
|
3433
|
+
package: dep.package,
|
|
3434
|
+
version: dep.resolvedVersion ?? dep.currentSpec,
|
|
3435
|
+
currentSpec: dep.currentSpec,
|
|
3436
|
+
drift: dep.drift,
|
|
3437
|
+
majorsBehind: dep.majorsBehind
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
return rows;
|
|
3442
|
+
}
|
|
3443
|
+
function toCycloneDx(artifact) {
|
|
3444
|
+
const dependencies = flattenDependencies(artifact);
|
|
3445
|
+
return {
|
|
3446
|
+
bomFormat: "CycloneDX",
|
|
3447
|
+
specVersion: "1.5",
|
|
3448
|
+
serialNumber: `urn:uuid:${deterministicUuid(sbomSerialSeed("cyclonedx", artifact, dependencies))}`,
|
|
3449
|
+
version: 1,
|
|
3450
|
+
metadata: {
|
|
3451
|
+
timestamp: artifact.timestamp,
|
|
3452
|
+
tools: [
|
|
3453
|
+
{
|
|
3454
|
+
vendor: "Vibgrate",
|
|
3455
|
+
name: "@vibgrate/cli",
|
|
3456
|
+
version: artifact.vibgrateVersion
|
|
3457
|
+
}
|
|
3458
|
+
],
|
|
3459
|
+
component: {
|
|
3460
|
+
type: "application",
|
|
3461
|
+
name: artifact.rootPath
|
|
3462
|
+
}
|
|
3463
|
+
},
|
|
3464
|
+
components: dependencies.map((dep) => ({
|
|
3465
|
+
type: "library",
|
|
3466
|
+
name: dep.package,
|
|
3467
|
+
version: dep.version,
|
|
3468
|
+
properties: [
|
|
3469
|
+
{ name: "vibgrate:project", value: dep.project },
|
|
3470
|
+
{ name: "vibgrate:currentSpec", value: dep.currentSpec },
|
|
3471
|
+
{ name: "vibgrate:drift", value: dep.drift },
|
|
3472
|
+
{ name: "vibgrate:majorsBehind", value: String(dep.majorsBehind ?? "unknown") }
|
|
3473
|
+
]
|
|
3474
|
+
}))
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
function toSpdx(artifact) {
|
|
3478
|
+
const dependencies = flattenDependencies(artifact);
|
|
3479
|
+
return {
|
|
3480
|
+
spdxVersion: "SPDX-2.3",
|
|
3481
|
+
dataLicense: "CC0-1.0",
|
|
3482
|
+
SPDXID: "SPDXRef-DOCUMENT",
|
|
3483
|
+
name: `${artifact.rootPath}-sbom`,
|
|
3484
|
+
documentNamespace: `https://vibgrate.com/spdx/${artifact.rootPath}/${deterministicUuid(sbomSerialSeed("spdx", artifact, dependencies))}`,
|
|
3485
|
+
creationInfo: {
|
|
3486
|
+
created: artifact.timestamp,
|
|
3487
|
+
creators: [`Tool: @vibgrate/cli-${artifact.vibgrateVersion}`]
|
|
3488
|
+
},
|
|
3489
|
+
packages: dependencies.map((dep, i) => ({
|
|
3490
|
+
name: dep.package,
|
|
3491
|
+
SPDXID: `SPDXRef-Package-${i + 1}`,
|
|
3492
|
+
versionInfo: dep.version,
|
|
3493
|
+
downloadLocation: "NOASSERTION",
|
|
3494
|
+
filesAnalyzed: false,
|
|
3495
|
+
externalRefs: [
|
|
3496
|
+
{
|
|
3497
|
+
referenceCategory: "PACKAGE-MANAGER",
|
|
3498
|
+
referenceType: "purl",
|
|
3499
|
+
referenceLocator: `pkg:npm/${encodeURIComponent(dep.package)}@${encodeURIComponent(dep.version)}`
|
|
3500
|
+
}
|
|
3501
|
+
],
|
|
3502
|
+
annotations: [
|
|
3503
|
+
{
|
|
3504
|
+
annotationType: "OTHER",
|
|
3505
|
+
annotator: "Tool: @vibgrate/cli",
|
|
3506
|
+
annotationDate: artifact.timestamp,
|
|
3507
|
+
comment: `project=${dep.project}; drift=${dep.drift}; majorsBehind=${dep.majorsBehind ?? "unknown"}`
|
|
3508
|
+
}
|
|
3509
|
+
]
|
|
3510
|
+
}))
|
|
3511
|
+
};
|
|
3512
|
+
}
|
|
3513
|
+
function projectDependencyMap(artifact) {
|
|
3514
|
+
const map = /* @__PURE__ */ new Map();
|
|
3515
|
+
for (const project of artifact.projects) {
|
|
3516
|
+
for (const dep of project.dependencies) {
|
|
3517
|
+
map.set(`${project.name}:${dep.package}`, dep);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
return map;
|
|
3521
|
+
}
|
|
3522
|
+
function formatDeltaText(base, current) {
|
|
3523
|
+
const baseMap = projectDependencyMap(base);
|
|
3524
|
+
const currentMap = projectDependencyMap(current);
|
|
3525
|
+
const added = [];
|
|
3526
|
+
const removed = [];
|
|
3527
|
+
const changed = [];
|
|
3528
|
+
for (const [key, dep] of currentMap.entries()) {
|
|
3529
|
+
if (!baseMap.has(key)) {
|
|
3530
|
+
added.push(`${key} @ ${dep.resolvedVersion ?? dep.currentSpec}`);
|
|
3531
|
+
continue;
|
|
3532
|
+
}
|
|
3533
|
+
const prev = baseMap.get(key);
|
|
3534
|
+
const prevVersion = prev.resolvedVersion ?? prev.currentSpec;
|
|
3535
|
+
const nowVersion = dep.resolvedVersion ?? dep.currentSpec;
|
|
3536
|
+
if (prevVersion !== nowVersion || prev.majorsBehind !== dep.majorsBehind) {
|
|
3537
|
+
changed.push(`${key} ${prevVersion} -> ${nowVersion} (majorsBehind ${prev.majorsBehind ?? "unknown"} -> ${dep.majorsBehind ?? "unknown"})`);
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
for (const [key, dep] of baseMap.entries()) {
|
|
3541
|
+
if (!currentMap.has(key)) {
|
|
3542
|
+
removed.push(`${key} @ ${dep.resolvedVersion ?? dep.currentSpec}`);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
const lines = [
|
|
3546
|
+
"Vibgrate SBOM Delta",
|
|
3547
|
+
"===================",
|
|
3548
|
+
`Baseline: ${base.timestamp}`,
|
|
3549
|
+
`Current: ${current.timestamp}`,
|
|
3550
|
+
`Drift score delta: ${(current.drift.score - base.drift.score).toFixed(2)} points`,
|
|
3551
|
+
"",
|
|
3552
|
+
`Added dependencies (${added.length})`,
|
|
3553
|
+
...added.map((d) => ` + ${d}`),
|
|
3554
|
+
"",
|
|
3555
|
+
`Removed dependencies (${removed.length})`,
|
|
3556
|
+
...removed.map((d) => ` - ${d}`),
|
|
3557
|
+
"",
|
|
3558
|
+
`Changed dependencies (${changed.length})`,
|
|
3559
|
+
...changed.map((d) => ` * ${d}`)
|
|
3560
|
+
];
|
|
3561
|
+
return lines.join("\n");
|
|
3562
|
+
}
|
|
3563
|
+
async function readArtifactOrExit(filePath) {
|
|
3564
|
+
const absolutePath = path9.resolve(filePath);
|
|
3565
|
+
if (!await pathExists(absolutePath)) {
|
|
3566
|
+
console.error(chalk5.red(`Artifact not found: ${absolutePath}`));
|
|
3567
|
+
process.exit(1);
|
|
3568
|
+
}
|
|
3569
|
+
return readJsonFile(absolutePath);
|
|
3570
|
+
}
|
|
3571
|
+
var exportCommand = new Command("export").description("Export scan artifact as SBOM").option("--in <file>", "Input artifact file", ".vibgrate/scan_result.json").option("--out <file>", "Output SBOM file").option("--format <format>", "SBOM format (cyclonedx|spdx)", "cyclonedx").action(async (opts) => {
|
|
3572
|
+
const artifact = await readArtifactOrExit(opts.in);
|
|
3573
|
+
const format = opts.format.toLowerCase();
|
|
3574
|
+
if (format !== "cyclonedx" && format !== "spdx") {
|
|
3575
|
+
console.error(chalk5.red("Invalid SBOM format. Use cyclonedx or spdx."));
|
|
3576
|
+
process.exit(1);
|
|
3577
|
+
}
|
|
3578
|
+
const sbom = format === "cyclonedx" ? toCycloneDx(artifact) : toSpdx(artifact);
|
|
3579
|
+
const body = JSON.stringify(sbom, null, 2);
|
|
3580
|
+
if (opts.out) {
|
|
3581
|
+
await writeTextFile(path9.resolve(opts.out), body);
|
|
3582
|
+
console.log(chalk5.green("\u2714") + ` SBOM written to ${opts.out}`);
|
|
3583
|
+
} else {
|
|
3584
|
+
console.log(body);
|
|
3585
|
+
}
|
|
3586
|
+
});
|
|
3587
|
+
var deltaCommand = new Command("delta").description("Show SBOM delta between two scan artifacts").requiredOption("--from <file>", "Baseline scan artifact path").requiredOption("--to <file>", "Current scan artifact path").option("--out <file>", "Write report to file").action(async (opts) => {
|
|
3588
|
+
const base = await readArtifactOrExit(opts.from);
|
|
3589
|
+
const current = await readArtifactOrExit(opts.to);
|
|
3590
|
+
const report = formatDeltaText(base, current);
|
|
3591
|
+
if (opts.out) {
|
|
3592
|
+
await writeTextFile(path9.resolve(opts.out), report);
|
|
3593
|
+
console.log(chalk5.green("\u2714") + ` SBOM delta report written to ${opts.out}`);
|
|
3594
|
+
} else {
|
|
3595
|
+
console.log(report);
|
|
3596
|
+
}
|
|
3597
|
+
});
|
|
3598
|
+
var sbomCommand = new Command("sbom").description("Supply-chain evidence: SBOM export/delta and OpenVEX generation").addCommand(exportCommand).addCommand(deltaCommand).addCommand(vexCommand);
|
|
3599
|
+
|
|
3600
|
+
// src/cli.ts
|
|
3601
|
+
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
3602
|
+
"build",
|
|
3603
|
+
"status",
|
|
3604
|
+
"verify",
|
|
3605
|
+
"ask",
|
|
3606
|
+
"embed",
|
|
3607
|
+
"show",
|
|
3608
|
+
"path",
|
|
3609
|
+
"tree",
|
|
3610
|
+
"impact",
|
|
3611
|
+
"tests",
|
|
3612
|
+
"facts",
|
|
3613
|
+
"guide",
|
|
3614
|
+
"why",
|
|
3615
|
+
"bisect",
|
|
3616
|
+
"drift",
|
|
3617
|
+
"models",
|
|
3618
|
+
"savings",
|
|
3619
|
+
"lib",
|
|
3620
|
+
"export",
|
|
3621
|
+
"push",
|
|
3622
|
+
"bundle",
|
|
3623
|
+
"map",
|
|
3624
|
+
"hubs",
|
|
3625
|
+
"areas",
|
|
3626
|
+
"oddities",
|
|
3627
|
+
"serve",
|
|
3628
|
+
"install",
|
|
3629
|
+
"uninstall",
|
|
3630
|
+
"share",
|
|
3631
|
+
"benchmark",
|
|
3632
|
+
"help",
|
|
3633
|
+
// Drift-reporting verbs (merged from the Vibgrate CLI).
|
|
3634
|
+
"init",
|
|
3635
|
+
"scan",
|
|
3636
|
+
"baseline",
|
|
3637
|
+
"report",
|
|
3638
|
+
"login",
|
|
3639
|
+
"logout",
|
|
3640
|
+
"dsn",
|
|
3641
|
+
"update",
|
|
3642
|
+
"sbom"
|
|
3643
|
+
]);
|
|
3644
|
+
function buildProgram() {
|
|
3645
|
+
const program = new Command();
|
|
3646
|
+
program.name("vg").description(
|
|
3647
|
+
"vg \u2014 local codebase intelligence for AI coding agents: a deterministic, no-API-key code graph + MCP server, drift reporting, and version-correct library docs"
|
|
3648
|
+
).version(VERSION, "--version", "output the version number").showSuggestionAfterError(true).addHelpText(
|
|
3649
|
+
"after",
|
|
3650
|
+
'\nRun `vg` to scan + map the current folder, `vg "<question>"` to ask, `vg status`/`vg verify` for state.\nDrift: `vg scan` / `vg report`. Wire into your AI agent: `vg install`.\nThe `vibgrate` command is an alias for `vg`.\nDocs: https://vibgrate.com/help'
|
|
3651
|
+
);
|
|
3652
|
+
registerBuild(program);
|
|
3653
|
+
registerStatus(program);
|
|
3654
|
+
registerVerify(program);
|
|
3655
|
+
registerAsk(program);
|
|
3656
|
+
registerEmbed(program);
|
|
3657
|
+
registerShow(program);
|
|
3658
|
+
registerPath(program);
|
|
3659
|
+
registerTree(program);
|
|
3660
|
+
registerInsights(program);
|
|
3661
|
+
registerImpact(program);
|
|
3662
|
+
registerServe(program);
|
|
3663
|
+
registerInstall(program);
|
|
3664
|
+
registerShare(program);
|
|
3665
|
+
registerBenchmark(program);
|
|
3666
|
+
registerTests(program);
|
|
3667
|
+
registerFacts(program);
|
|
3668
|
+
registerGuide(program);
|
|
3669
|
+
registerWhy(program);
|
|
3670
|
+
registerBisect(program);
|
|
3671
|
+
registerDrift(program);
|
|
3672
|
+
registerModels(program);
|
|
3673
|
+
registerSavings(program);
|
|
3674
|
+
registerLib(program);
|
|
3675
|
+
registerExport(program);
|
|
3676
|
+
registerBundle(program);
|
|
3677
|
+
program.addCommand(initCommand);
|
|
3678
|
+
program.addCommand(scanCommand);
|
|
3679
|
+
program.addCommand(baselineCommand);
|
|
3680
|
+
program.addCommand(reportCommand);
|
|
3681
|
+
program.addCommand(loginCommand);
|
|
3682
|
+
program.addCommand(logoutCommand);
|
|
3683
|
+
program.addCommand(dsnCommand);
|
|
3684
|
+
program.addCommand(pushCommand);
|
|
3685
|
+
program.addCommand(updateCommand);
|
|
3686
|
+
program.addCommand(sbomCommand);
|
|
3687
|
+
return program;
|
|
3688
|
+
}
|
|
3689
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
3690
|
+
"-C",
|
|
3691
|
+
"--cwd",
|
|
3692
|
+
"--generated-at",
|
|
3693
|
+
"--graph",
|
|
3694
|
+
"--jobs",
|
|
3695
|
+
"--only",
|
|
3696
|
+
"--exclude",
|
|
3697
|
+
"--export",
|
|
3698
|
+
"-o",
|
|
3699
|
+
"--budget",
|
|
3700
|
+
"-b"
|
|
3701
|
+
]);
|
|
3702
|
+
var BUILD_ONLY_FLAGS = /* @__PURE__ */ new Set([
|
|
3703
|
+
"--json",
|
|
3704
|
+
"--jobs",
|
|
3705
|
+
"--only",
|
|
3706
|
+
"--export",
|
|
3707
|
+
"--graph",
|
|
3708
|
+
"--generated-at",
|
|
3709
|
+
"--no-cache",
|
|
3710
|
+
"--deep",
|
|
3711
|
+
"--scip",
|
|
3712
|
+
"--no-scip",
|
|
3713
|
+
"--tsc",
|
|
3714
|
+
"--no-tsc",
|
|
3715
|
+
"--ground",
|
|
3716
|
+
"--no-ground",
|
|
3717
|
+
"--html",
|
|
3718
|
+
"--no-html",
|
|
3719
|
+
"--report",
|
|
3720
|
+
"--no-report",
|
|
3721
|
+
"--warm"
|
|
3722
|
+
]);
|
|
3723
|
+
function hasBuildOnlyFlag(args) {
|
|
3724
|
+
return args.some((a) => BUILD_ONLY_FLAGS.has(a.split("=")[0]));
|
|
3725
|
+
}
|
|
3726
|
+
function dispatch(argv, cwd) {
|
|
3727
|
+
const args = [...argv];
|
|
3728
|
+
const firstPositionalIdx = findFirstPositional(args);
|
|
3729
|
+
if (firstPositionalIdx === -1) {
|
|
3730
|
+
if (args.includes("--help") || args.includes("-h") || args.includes("--version")) {
|
|
3731
|
+
return args;
|
|
3732
|
+
}
|
|
3733
|
+
return [hasBuildOnlyFlag(args) ? "build" : "scan", ...args];
|
|
3734
|
+
}
|
|
3735
|
+
const first = args[firstPositionalIdx];
|
|
3736
|
+
if (KNOWN_COMMANDS.has(first)) {
|
|
3737
|
+
const rest = [...args];
|
|
3738
|
+
rest.splice(firstPositionalIdx, 1);
|
|
3739
|
+
return [first, ...rest];
|
|
3740
|
+
}
|
|
3741
|
+
const looksLikeQuestion = /\s/.test(first) || first.endsWith("?");
|
|
3742
|
+
if (looksLikeQuestion) return ["ask", ...args];
|
|
3743
|
+
if (fs4.existsSync(path9.resolve(cwd, first))) {
|
|
3744
|
+
return [hasBuildOnlyFlag(args) ? "build" : "scan", ...args];
|
|
3745
|
+
}
|
|
3746
|
+
return ["ask", ...args];
|
|
3747
|
+
}
|
|
3748
|
+
function findFirstPositional(args) {
|
|
3749
|
+
for (let i = 0; i < args.length; i++) {
|
|
3750
|
+
const a = args[i];
|
|
3751
|
+
if (a.startsWith("-")) {
|
|
3752
|
+
if (VALUE_FLAGS.has(a)) i++;
|
|
3753
|
+
continue;
|
|
3754
|
+
}
|
|
3755
|
+
return i;
|
|
3756
|
+
}
|
|
3757
|
+
return -1;
|
|
3758
|
+
}
|
|
3759
|
+
async function main(argv = process.argv) {
|
|
3760
|
+
const raw = argv.slice(2);
|
|
3761
|
+
if (process.env.NO_COLOR || raw.includes("--no-color")) disableColor();
|
|
3762
|
+
const cwd = readCwd(raw);
|
|
3763
|
+
const dispatched = dispatch(raw, cwd);
|
|
3764
|
+
const program = buildProgram();
|
|
3765
|
+
program.exitOverride((err) => {
|
|
3766
|
+
throw err;
|
|
3767
|
+
});
|
|
3768
|
+
program.configureOutput({
|
|
3769
|
+
writeErr: (str) => process.stderr.write(str),
|
|
3770
|
+
writeOut: (str) => process.stdout.write(str)
|
|
3771
|
+
});
|
|
3772
|
+
try {
|
|
3773
|
+
await program.parseAsync(dispatched, { from: "user" });
|
|
3774
|
+
} catch (err) {
|
|
3775
|
+
handleError(err);
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
function readCwd(raw) {
|
|
3779
|
+
const i = raw.findIndex((a) => a === "-C" || a === "--cwd");
|
|
3780
|
+
if (i >= 0 && raw[i + 1]) return path9.resolve(raw[i + 1]);
|
|
3781
|
+
const eq = raw.find((a) => a.startsWith("--cwd="));
|
|
3782
|
+
if (eq) return path9.resolve(eq.slice("--cwd=".length));
|
|
3783
|
+
return process.cwd();
|
|
3784
|
+
}
|
|
3785
|
+
function handleError(err) {
|
|
3786
|
+
if (err instanceof CommanderError) {
|
|
3787
|
+
if (err.code === "commander.helpDisplayed" || err.code === "commander.help") {
|
|
3788
|
+
process.exit(ExitCode.OK);
|
|
3789
|
+
}
|
|
3790
|
+
if (err.code === "commander.version") process.exit(ExitCode.OK);
|
|
3791
|
+
process.exit(ExitCode.USAGE_ERROR);
|
|
3792
|
+
}
|
|
3793
|
+
if (err instanceof CliError) {
|
|
3794
|
+
info(c.red(`error: ${err.message}`));
|
|
3795
|
+
process.exit(err.code);
|
|
3796
|
+
}
|
|
3797
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3798
|
+
const correlation = Math.random().toString(36).slice(2, 10);
|
|
3799
|
+
info(c.red(`error: ${message}`));
|
|
3800
|
+
info(c.dim(` (ref ${correlation}) \u2014 re-run with --json for detail, or report at https://vibgrate.com/help`));
|
|
3801
|
+
process.exit(ExitCode.ERROR);
|
|
3802
|
+
}
|
|
3803
|
+
function isEntry() {
|
|
3804
|
+
try {
|
|
3805
|
+
if (!process.argv[1]) return false;
|
|
3806
|
+
return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
3807
|
+
} catch {
|
|
3808
|
+
return false;
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
if (isEntry()) {
|
|
3812
|
+
void main();
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
export { KNOWN_COMMANDS, buildProgram, dispatch, main, runBuild };
|
|
3816
|
+
//# sourceMappingURL=cli.js.map
|
|
3817
|
+
//# sourceMappingURL=cli.js.map
|