@vibgrate/cli 2026.715.1 → 2026.717.1
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 +1 -1
- package/dist/baseline-YWGSHCKW.js +6 -0
- package/dist/{baseline-BTLZWSNS.js.map → baseline-YWGSHCKW.js.map} +1 -1
- package/dist/{chunk-D4BPWARI.js → chunk-L42NVMD6.js} +77 -27
- package/dist/chunk-L42NVMD6.js.map +1 -0
- package/dist/{chunk-C5AVF7PT.js → chunk-M62BGJMK.js} +44 -12
- package/dist/chunk-M62BGJMK.js.map +1 -0
- package/dist/{chunk-6DKWB2OI.js → chunk-WZNIDXZP.js} +3 -3
- package/dist/{chunk-6DKWB2OI.js.map → chunk-WZNIDXZP.js.map} +1 -1
- package/dist/cli.js +822 -173
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/dist/baseline-BTLZWSNS.js +0 -6
- package/dist/chunk-C5AVF7PT.js.map +0 -1
- package/dist/chunk-D4BPWARI.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, ingestHostForRegionId, dsnCommand, pushCommand, CliError, ExitCode, NPX_INVOCATION, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, recordCliCall, CLI_TOOL_ALIASES, countPending, resolveOne, indexFor, countTokens, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, usageError, loadGraph, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, readUsage, exportGraph, catalogPath, libDir, stableStringify, buildGraph, writeArtifacts, writeSnapshot, UsageError, ResourceLimitError, epistemicBreakdown, cacheDir, probeFreshness, discover, vibgrateDir, modelCacheInfo, clearModelCache, GraphSource, createServer, assistantById, serializeGraph, resolveLimits, loadCatalog, driftFor, addLibrary, parseDsn as parseDsn$1, publishPrivateLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface, assessDocQuality, symbolsFromApi, fetchHostedDocsCached, selectForBudget, formatForExt, verifyDeterminism, renderHtml, isModelReady } from './chunk-
|
|
3
|
-
import { resolvedGrammarFiles, grammarSetVersion, canonicalize } from './chunk-VFO5UDAT.js';
|
|
4
|
-
import { loadAdvancedScanHook, baselineCommand } from './chunk-
|
|
5
|
-
import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, loadConfig, prepareCompressedUpload, computeDriftScore, titleBox, projectTypeToVulnEcosystem, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, PROJECT_TYPE_TO_OSV_ECOSYSTEM, fetchRiskySymbols, brandProgressBar, resolveHead, workingTreeDirty } from './chunk-
|
|
2
|
+
import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, ingestHostForRegionId, dsnCommand, pushCommand, CliError, ExitCode, NPX_INVOCATION, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, recordCliCall, CLI_TOOL_ALIASES, countPending, resolveOne, indexFor, countTokens, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, detectAssistants, usageError, loadGraph, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, readUsage, exportGraph, catalogPath, libDir, stableStringify, buildGraph, writeArtifacts, writeSnapshot, UsageError, ResourceLimitError, epistemicBreakdown, cacheDir, probeFreshness, discover, vibgrateDir, modelCacheInfo, clearModelCache, GraphSource, createServer, assistantById, serializeGraph, resolveLimits, loadCatalog, driftFor, addLibrary, parseDsn as parseDsn$1, publishPrivateLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface, assessDocQuality, symbolsFromApi, fetchHostedDocsCached, selectForBudget, formatForExt, verifyDeterminism, renderHtml, isModelReady, SKIP_DIRS, SKIP_FILES } from './chunk-L42NVMD6.js';
|
|
3
|
+
import { resolvedGrammarFiles, grammarSetVersion, hashString, canonicalize, hashBytes } from './chunk-VFO5UDAT.js';
|
|
4
|
+
import { loadAdvancedScanHook, baselineCommand } from './chunk-WZNIDXZP.js';
|
|
5
|
+
import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, loadConfig, prepareCompressedUpload, computeDriftScore, titleBox, projectTypeToVulnEcosystem, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, PROJECT_TYPE_TO_OSV_ECOSYSTEM, fetchRiskySymbols, brandProgressBar, resolveHead, workingTreeDirty } from './chunk-M62BGJMK.js';
|
|
6
6
|
import './chunk-RXP66R2E.js';
|
|
7
7
|
import { pathExists as pathExists$1, parseExcludePatterns } from './chunk-GI6W53LM.js';
|
|
8
|
-
import * as
|
|
9
|
-
import
|
|
8
|
+
import * as fs8 from 'fs';
|
|
9
|
+
import fs8__default, { realpathSync } from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import path__default from 'path';
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
@@ -16,7 +16,7 @@ import * as crypto from 'crypto';
|
|
|
16
16
|
import { randomUUID } from 'crypto';
|
|
17
17
|
import chalk8 from 'chalk';
|
|
18
18
|
import * as os from 'os';
|
|
19
|
-
import * as
|
|
19
|
+
import * as fs14 from 'fs/promises';
|
|
20
20
|
import * as readline from 'readline';
|
|
21
21
|
import semver from 'semver';
|
|
22
22
|
|
|
@@ -178,14 +178,14 @@ async function signGraphAttestation(root, graph, opts) {
|
|
|
178
178
|
const explicitKey = opts.key ?? process.env.VG_ATTEST_KEY;
|
|
179
179
|
const keyPath = explicitKey ? path.resolve(explicitKey) : path.join(root, ".vibgrate", DEFAULT_KEY);
|
|
180
180
|
let keyGeneratedAt;
|
|
181
|
-
if (!
|
|
181
|
+
if (!fs8.existsSync(keyPath)) {
|
|
182
182
|
if (explicitKey) {
|
|
183
183
|
throw new CliError(`signing key not found: ${explicitKey}`, ExitCode.USAGE_ERROR);
|
|
184
184
|
}
|
|
185
185
|
const kp = generateKeypair();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
fs8.mkdirSync(path.dirname(keyPath), { recursive: true });
|
|
187
|
+
fs8.writeFileSync(keyPath, kp.privatePem, { mode: 384 });
|
|
188
|
+
fs8.writeFileSync(`${keyPath}.pub`, kp.publicPem);
|
|
189
189
|
keyGeneratedAt = keyPath;
|
|
190
190
|
notices.push(
|
|
191
191
|
`minted a new Ed25519 signing key at ${rel(root, keyPath)} (keyid ${kp.keyid}) \u2014 keep it, add it to .gitignore, and reuse it to re-sign reproducibly`
|
|
@@ -193,7 +193,7 @@ async function signGraphAttestation(root, graph, opts) {
|
|
|
193
193
|
}
|
|
194
194
|
let privateKey;
|
|
195
195
|
try {
|
|
196
|
-
privateKey = crypto.createPrivateKey(
|
|
196
|
+
privateKey = crypto.createPrivateKey(fs8.readFileSync(keyPath, "utf8"));
|
|
197
197
|
} catch {
|
|
198
198
|
throw new CliError(`could not read an Ed25519 private key from ${rel(root, keyPath)}`, ExitCode.USAGE_ERROR);
|
|
199
199
|
}
|
|
@@ -207,8 +207,8 @@ async function signGraphAttestation(root, graph, opts) {
|
|
|
207
207
|
const statement = buildStatement(graph, { commit });
|
|
208
208
|
const envelope = signStatement(statement, privateKey);
|
|
209
209
|
const outPath = opts.attestation ? path.resolve(opts.attestation) : path.join(root, ".vibgrate", DEFAULT_ATTESTATION);
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
fs8.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
211
|
+
fs8.writeFileSync(outPath, serializeEnvelope(envelope));
|
|
212
212
|
return {
|
|
213
213
|
summary: {
|
|
214
214
|
keyid: envelope.signatures[0]?.keyid ?? "",
|
|
@@ -223,7 +223,7 @@ async function signGraphAttestation(root, graph, opts) {
|
|
|
223
223
|
}
|
|
224
224
|
function verifyGraphAttestation(root, opts) {
|
|
225
225
|
const bundlePath = opts.attestation ? path.resolve(opts.attestation) : path.join(root, ".vibgrate", DEFAULT_ATTESTATION);
|
|
226
|
-
if (!
|
|
226
|
+
if (!fs8.existsSync(bundlePath)) {
|
|
227
227
|
if (opts.attestation || opts.pub) {
|
|
228
228
|
throw new CliError(
|
|
229
229
|
`no attestation at ${rel(root, bundlePath)} \u2014 sign one with \`vg build --attest\``,
|
|
@@ -232,8 +232,8 @@ function verifyGraphAttestation(root, opts) {
|
|
|
232
232
|
}
|
|
233
233
|
return { bundlePath, missing: true };
|
|
234
234
|
}
|
|
235
|
-
const envelope = parseEnvelope(
|
|
236
|
-
const publicKeyPem = opts.pub ?
|
|
235
|
+
const envelope = parseEnvelope(fs8.readFileSync(bundlePath, "utf8"));
|
|
236
|
+
const publicKeyPem = opts.pub ? fs8.readFileSync(path.resolve(opts.pub), "utf8") : void 0;
|
|
237
237
|
const graph = loadGraph(root) ?? void 0;
|
|
238
238
|
return { bundlePath, missing: false, result: verifyEnvelope(envelope, { publicKeyPem, graph }) };
|
|
239
239
|
}
|
|
@@ -584,8 +584,8 @@ function writeExport(graph, target) {
|
|
|
584
584
|
ExitCode.USAGE_ERROR
|
|
585
585
|
);
|
|
586
586
|
}
|
|
587
|
-
|
|
588
|
-
|
|
587
|
+
fs8.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
|
|
588
|
+
fs8.writeFileSync(target, content);
|
|
589
589
|
}
|
|
590
590
|
function collect(value, prev) {
|
|
591
591
|
return [...prev, value];
|
|
@@ -604,7 +604,7 @@ async function runStatus(global) {
|
|
|
604
604
|
const root = path.resolve(global.cwd ?? ".");
|
|
605
605
|
const graphPath = global.graph ?? defaultGraphPath(root);
|
|
606
606
|
const graph = loadGraph(root, graphPath);
|
|
607
|
-
const hasCache =
|
|
607
|
+
const hasCache = fs8.existsSync(path.join(cacheDir(root), "parse-cache.json"));
|
|
608
608
|
let stale = null;
|
|
609
609
|
let staleExact = false;
|
|
610
610
|
if (graph) {
|
|
@@ -1289,14 +1289,14 @@ var INSTALL_ID_FILE = path.join(INSTALL_ID_DIR, "install-id");
|
|
|
1289
1289
|
var SEND_TIMEOUT_MS = 4e3;
|
|
1290
1290
|
function installId() {
|
|
1291
1291
|
try {
|
|
1292
|
-
const existing =
|
|
1292
|
+
const existing = fs8.readFileSync(INSTALL_ID_FILE, "utf8").trim();
|
|
1293
1293
|
if (isUuid(existing)) return existing;
|
|
1294
1294
|
} catch {
|
|
1295
1295
|
}
|
|
1296
1296
|
const id = randomUuid();
|
|
1297
1297
|
try {
|
|
1298
|
-
|
|
1299
|
-
|
|
1298
|
+
fs8.mkdirSync(INSTALL_ID_DIR, { recursive: true });
|
|
1299
|
+
fs8.writeFileSync(INSTALL_ID_FILE, id + "\n", { mode: 384 });
|
|
1300
1300
|
} catch {
|
|
1301
1301
|
}
|
|
1302
1302
|
return id;
|
|
@@ -1320,7 +1320,7 @@ function ledgerFilePath(root) {
|
|
|
1320
1320
|
}
|
|
1321
1321
|
function readOffset(root) {
|
|
1322
1322
|
try {
|
|
1323
|
-
const raw = JSON.parse(
|
|
1323
|
+
const raw = JSON.parse(fs8.readFileSync(offsetPath(root), "utf8"));
|
|
1324
1324
|
return typeof raw.offset === "number" && raw.offset >= 0 ? raw.offset : 0;
|
|
1325
1325
|
} catch {
|
|
1326
1326
|
return 0;
|
|
@@ -1328,8 +1328,8 @@ function readOffset(root) {
|
|
|
1328
1328
|
}
|
|
1329
1329
|
function writeOffset(root, offset) {
|
|
1330
1330
|
try {
|
|
1331
|
-
|
|
1332
|
-
|
|
1331
|
+
fs8.mkdirSync(cacheDir(root), { recursive: true });
|
|
1332
|
+
fs8.writeFileSync(offsetPath(root), JSON.stringify({ offset }) + "\n");
|
|
1333
1333
|
} catch {
|
|
1334
1334
|
}
|
|
1335
1335
|
}
|
|
@@ -1337,7 +1337,7 @@ function buildBatch(root, fromOffset, id = installId()) {
|
|
|
1337
1337
|
const file = ledgerFilePath(root);
|
|
1338
1338
|
let size = 0;
|
|
1339
1339
|
try {
|
|
1340
|
-
size =
|
|
1340
|
+
size = fs8.statSync(file).size;
|
|
1341
1341
|
} catch {
|
|
1342
1342
|
return { batch: null, newOffset: 0 };
|
|
1343
1343
|
}
|
|
@@ -1345,13 +1345,13 @@ function buildBatch(root, fromOffset, id = installId()) {
|
|
|
1345
1345
|
if (start >= size) return { batch: null, newOffset: size };
|
|
1346
1346
|
let text = "";
|
|
1347
1347
|
try {
|
|
1348
|
-
const fd =
|
|
1348
|
+
const fd = fs8.openSync(file, "r");
|
|
1349
1349
|
try {
|
|
1350
1350
|
const buf = Buffer.alloc(size - start);
|
|
1351
|
-
|
|
1351
|
+
fs8.readSync(fd, buf, 0, buf.length, start);
|
|
1352
1352
|
text = buf.toString("utf8");
|
|
1353
1353
|
} finally {
|
|
1354
|
-
|
|
1354
|
+
fs8.closeSync(fd);
|
|
1355
1355
|
}
|
|
1356
1356
|
} catch {
|
|
1357
1357
|
return { batch: null, newOffset: fromOffset };
|
|
@@ -1374,8 +1374,8 @@ function buildBatch(root, fromOffset, id = installId()) {
|
|
|
1374
1374
|
const source = e.source ?? "mcp";
|
|
1375
1375
|
const client = e.client ?? "unknown";
|
|
1376
1376
|
const outcome = e.outcome ?? "complete";
|
|
1377
|
-
const
|
|
1378
|
-
const row = rows.get(
|
|
1377
|
+
const key2 = `${source}|${client}|${e.tool}|${outcome}`;
|
|
1378
|
+
const row = rows.get(key2) ?? {
|
|
1379
1379
|
source,
|
|
1380
1380
|
client,
|
|
1381
1381
|
tool: e.tool,
|
|
@@ -1387,7 +1387,7 @@ function buildBatch(root, fromOffset, id = installId()) {
|
|
|
1387
1387
|
row.calls++;
|
|
1388
1388
|
row.vgTokens += e.vgTokens ?? 0;
|
|
1389
1389
|
row.baselineTokens += e.baselineTokens ?? 0;
|
|
1390
|
-
rows.set(
|
|
1390
|
+
rows.set(key2, row);
|
|
1391
1391
|
total++;
|
|
1392
1392
|
if (typeof e.ts === "number") {
|
|
1393
1393
|
if (e.ts < minTs) minTs = e.ts;
|
|
@@ -1497,7 +1497,7 @@ function registerServe(program) {
|
|
|
1497
1497
|
}
|
|
1498
1498
|
async function ensureServableGraph(root, graphPath, global, refresh, opts = {}) {
|
|
1499
1499
|
if (refresh) {
|
|
1500
|
-
if (!
|
|
1500
|
+
if (!fs8.existsSync(graphPath)) {
|
|
1501
1501
|
info(c.dim("vg \xB7 no map found \u2014 building it before serving\u2026"));
|
|
1502
1502
|
await runBuild(
|
|
1503
1503
|
[],
|
|
@@ -1514,7 +1514,7 @@ async function ensureServableGraph(root, graphPath, global, refresh, opts = {})
|
|
|
1514
1514
|
}
|
|
1515
1515
|
}
|
|
1516
1516
|
}
|
|
1517
|
-
if (!
|
|
1517
|
+
if (!fs8.existsSync(graphPath)) {
|
|
1518
1518
|
throw new CliError(
|
|
1519
1519
|
`no map found at ${graphPath} \u2014 run \`vg\` to build one first`,
|
|
1520
1520
|
ExitCode.NOT_FOUND
|
|
@@ -1780,6 +1780,318 @@ function endOfLine(text, line) {
|
|
|
1780
1780
|
const lines = text.split(/\r?\n/);
|
|
1781
1781
|
return (lines[line] ?? "").length;
|
|
1782
1782
|
}
|
|
1783
|
+
var CACHE_VERSION = "vg-lsp-scan-cache/2";
|
|
1784
|
+
function cachePath(root) {
|
|
1785
|
+
return path.join(cacheDir(root), "lsp-scan.json");
|
|
1786
|
+
}
|
|
1787
|
+
function manifestHash(root) {
|
|
1788
|
+
const files = [];
|
|
1789
|
+
const walk2 = (dir) => {
|
|
1790
|
+
let entries;
|
|
1791
|
+
try {
|
|
1792
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
1793
|
+
} catch {
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
for (const entry of entries) {
|
|
1797
|
+
const abs = path.join(dir, entry.name);
|
|
1798
|
+
if (entry.isDirectory()) {
|
|
1799
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1800
|
+
walk2(abs);
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
if (!entry.isFile()) continue;
|
|
1804
|
+
const base = entry.name.toLowerCase();
|
|
1805
|
+
if (!isManifest(abs) && !SKIP_FILES.has(base)) continue;
|
|
1806
|
+
try {
|
|
1807
|
+
const rel3 = path.relative(root, abs).split(path.sep).join("/");
|
|
1808
|
+
files.push({ rel: rel3, hash: hashBytes(fs8.readFileSync(abs)) });
|
|
1809
|
+
} catch {
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
walk2(root);
|
|
1814
|
+
files.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
1815
|
+
return hashString(canonicalize(files));
|
|
1816
|
+
}
|
|
1817
|
+
function loadScanCache(root, key2) {
|
|
1818
|
+
try {
|
|
1819
|
+
const raw = JSON.parse(fs8.readFileSync(cachePath(root), "utf8"));
|
|
1820
|
+
if (raw.version === CACHE_VERSION && raw.manifestHash === key2.manifestHash && raw.toolVersion === key2.toolVersion && raw.offline === key2.offline && raw.artifact) {
|
|
1821
|
+
return { manifestHash: raw.manifestHash, artifact: raw.artifact };
|
|
1822
|
+
}
|
|
1823
|
+
} catch {
|
|
1824
|
+
}
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
function writeScanCache(root, key2, artifact) {
|
|
1828
|
+
try {
|
|
1829
|
+
fs8.mkdirSync(cacheDir(root), { recursive: true });
|
|
1830
|
+
fs8.writeFileSync(
|
|
1831
|
+
cachePath(root),
|
|
1832
|
+
stableStringify(
|
|
1833
|
+
{ version: CACHE_VERSION, manifestHash: key2.manifestHash, toolVersion: key2.toolVersion, offline: key2.offline, artifact },
|
|
1834
|
+
0
|
|
1835
|
+
)
|
|
1836
|
+
);
|
|
1837
|
+
} catch {
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
// src/lsp/graph-query.ts
|
|
1842
|
+
async function runGraphQuery(graph, params, ctx) {
|
|
1843
|
+
switch (params.mode) {
|
|
1844
|
+
case "ask":
|
|
1845
|
+
return runAsk(graph, params, ctx);
|
|
1846
|
+
case "areas":
|
|
1847
|
+
return runAreas(graph, params);
|
|
1848
|
+
case "hubs":
|
|
1849
|
+
return runHubs(graph, params);
|
|
1850
|
+
case "impact":
|
|
1851
|
+
return runImpact(graph, params);
|
|
1852
|
+
case "path":
|
|
1853
|
+
return runPath(graph, params);
|
|
1854
|
+
case "show":
|
|
1855
|
+
return runShow(graph, params);
|
|
1856
|
+
case "tree":
|
|
1857
|
+
return runTree(graph, params);
|
|
1858
|
+
default:
|
|
1859
|
+
return { ok: false, mode: params.mode, error: "bad-request", message: `unknown mode "${String(params.mode)}"` };
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
async function runAsk(graph, params, ctx) {
|
|
1863
|
+
const question = (params.question ?? "").trim();
|
|
1864
|
+
if (!question) return { ok: false, mode: "ask", error: "bad-request", message: "question is required" };
|
|
1865
|
+
const budget = params.budget ?? 2e3;
|
|
1866
|
+
const wantSemantic = !!params.semantic && !ctx.offline && ctx.semantic !== false;
|
|
1867
|
+
let result;
|
|
1868
|
+
let mode = "lexical";
|
|
1869
|
+
let note;
|
|
1870
|
+
if (wantSemantic) {
|
|
1871
|
+
let reason;
|
|
1872
|
+
const embedder = await loadEmbedder({ onUnavailable: (r) => reason = r });
|
|
1873
|
+
if (embedder) {
|
|
1874
|
+
const vectors = await getNodeEmbeddings(graph, embedder, ctx.root);
|
|
1875
|
+
result = await queryGraphSemantic(graph, question, { budget, embedder, nodeVectors: vectors });
|
|
1876
|
+
mode = `semantic (${embedder.id})`;
|
|
1877
|
+
} else {
|
|
1878
|
+
result = queryGraph(graph, question, { budget });
|
|
1879
|
+
note = reason ? unavailableMessage(reason) : "semantic unavailable; used lexical";
|
|
1880
|
+
}
|
|
1881
|
+
} else {
|
|
1882
|
+
result = queryGraph(graph, question, { budget });
|
|
1883
|
+
if (params.semantic && ctx.semantic === false) note = "semantic search is turned off \u2014 used lexical";
|
|
1884
|
+
else if (params.semantic && ctx.offline) note = "semantic skipped \u2014 Vibgrate is running offline";
|
|
1885
|
+
}
|
|
1886
|
+
return {
|
|
1887
|
+
ok: true,
|
|
1888
|
+
mode: "ask",
|
|
1889
|
+
data: {
|
|
1890
|
+
question: result.question,
|
|
1891
|
+
mode,
|
|
1892
|
+
note,
|
|
1893
|
+
tokensEstimate: result.tokensEstimate,
|
|
1894
|
+
matches: result.matches.map((m) => ({
|
|
1895
|
+
name: m.node.qualifiedName,
|
|
1896
|
+
kind: m.node.kind,
|
|
1897
|
+
file: m.node.file,
|
|
1898
|
+
line: m.node.span.start,
|
|
1899
|
+
score: m.score,
|
|
1900
|
+
why: m.why
|
|
1901
|
+
})),
|
|
1902
|
+
context: result.context
|
|
1903
|
+
}
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
function runAreas(graph, params) {
|
|
1907
|
+
const limit = params.limit ?? 30;
|
|
1908
|
+
const list = [...graph.areas].sort((a, b) => b.size - a.size || a.id - b.id).slice(0, limit);
|
|
1909
|
+
return { ok: true, mode: "areas", data: list };
|
|
1910
|
+
}
|
|
1911
|
+
function runHubs(graph, params) {
|
|
1912
|
+
const limit = params.limit ?? 20;
|
|
1913
|
+
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).map(nodeSummary2);
|
|
1914
|
+
return { ok: true, mode: "hubs", data: list };
|
|
1915
|
+
}
|
|
1916
|
+
function nodeSummary2(n) {
|
|
1917
|
+
return {
|
|
1918
|
+
id: n.id,
|
|
1919
|
+
name: n.qualifiedName,
|
|
1920
|
+
kind: n.kind,
|
|
1921
|
+
file: n.file,
|
|
1922
|
+
line: n.span.start,
|
|
1923
|
+
importance: n.importance,
|
|
1924
|
+
isHub: n.isHub,
|
|
1925
|
+
area: n.area
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
function resolveOrError(graph, name, mode) {
|
|
1929
|
+
const { node, candidates } = resolveOne(graph, name);
|
|
1930
|
+
if (node) return { node };
|
|
1931
|
+
if (candidates.length === 0) {
|
|
1932
|
+
return { ok: false, mode, error: "not-found", message: `no node matches "${name}"` };
|
|
1933
|
+
}
|
|
1934
|
+
return {
|
|
1935
|
+
ok: false,
|
|
1936
|
+
mode,
|
|
1937
|
+
error: "ambiguous",
|
|
1938
|
+
message: `"${name}" is ambiguous`,
|
|
1939
|
+
candidates: candidates.slice(0, 10).map((n) => ({ name: n.qualifiedName, kind: n.kind, file: n.file, line: n.span.start }))
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
function runImpact(graph, params) {
|
|
1943
|
+
const name = (params.name ?? "").trim();
|
|
1944
|
+
if (!name) return { ok: false, mode: "impact", error: "bad-request", message: "name is required" };
|
|
1945
|
+
const resolved = resolveOrError(graph, name, "impact");
|
|
1946
|
+
if (!("node" in resolved)) return resolved;
|
|
1947
|
+
const result = impactOf(graph, resolved.node.id, { depth: params.depth ?? 4 });
|
|
1948
|
+
return { ok: true, mode: "impact", data: result };
|
|
1949
|
+
}
|
|
1950
|
+
function runPath(graph, params) {
|
|
1951
|
+
const a = (params.a ?? "").trim();
|
|
1952
|
+
const b = (params.b ?? "").trim();
|
|
1953
|
+
if (!a || !b) return { ok: false, mode: "path", error: "bad-request", message: "a and b are required" };
|
|
1954
|
+
const ra = resolveOrError(graph, a, "path");
|
|
1955
|
+
if (!("node" in ra)) return ra;
|
|
1956
|
+
const rb = resolveOrError(graph, b, "path");
|
|
1957
|
+
if (!("node" in rb)) return rb;
|
|
1958
|
+
const result = shortestPath(graph, ra.node.id, rb.node.id);
|
|
1959
|
+
if (!result) {
|
|
1960
|
+
return {
|
|
1961
|
+
ok: false,
|
|
1962
|
+
mode: "path",
|
|
1963
|
+
error: "not-found",
|
|
1964
|
+
message: `no path between ${ra.node.qualifiedName} and ${rb.node.qualifiedName}`
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
1968
|
+
const names = result.ids.map((id) => byId.get(id)?.qualifiedName ?? id);
|
|
1969
|
+
return {
|
|
1970
|
+
ok: true,
|
|
1971
|
+
mode: "path",
|
|
1972
|
+
data: { from: ra.node.qualifiedName, to: rb.node.qualifiedName, hops: names.length - 1, direction: result.direction, path: names }
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
function runShow(graph, params) {
|
|
1976
|
+
const name = (params.name ?? "").trim();
|
|
1977
|
+
if (!name) return { ok: false, mode: "show", error: "bad-request", message: "name is required" };
|
|
1978
|
+
const resolved = resolveOrError(graph, name, "show");
|
|
1979
|
+
if (!("node" in resolved)) return resolved;
|
|
1980
|
+
const node = resolved.node;
|
|
1981
|
+
const index = indexFor(graph);
|
|
1982
|
+
const callees = dedupeNodes(index.callees(node.id).map((x) => x.node));
|
|
1983
|
+
const callers = dedupeNodes(index.callers(node.id).map((x) => x.node));
|
|
1984
|
+
const extendsEdges = index.out(node.id, "extends").concat(index.out(node.id, "implements"));
|
|
1985
|
+
const supertypes = extendsEdges.map((e) => index.node(e.dst)?.qualifiedName).filter(Boolean);
|
|
1986
|
+
const area = graph.areas.find((a) => a.id === node.area);
|
|
1987
|
+
return {
|
|
1988
|
+
ok: true,
|
|
1989
|
+
mode: "show",
|
|
1990
|
+
data: {
|
|
1991
|
+
id: node.id,
|
|
1992
|
+
name: node.qualifiedName,
|
|
1993
|
+
kind: node.kind,
|
|
1994
|
+
file: node.file,
|
|
1995
|
+
line: node.span.start,
|
|
1996
|
+
signature: node.signature ?? null,
|
|
1997
|
+
importance: node.importance,
|
|
1998
|
+
centrality: node.centrality,
|
|
1999
|
+
isHub: node.isHub,
|
|
2000
|
+
area: node.area,
|
|
2001
|
+
areaLabel: area?.label ?? null,
|
|
2002
|
+
tested: node.tested,
|
|
2003
|
+
calls: callees.map((n) => n.qualifiedName),
|
|
2004
|
+
calledBy: callers.map((n) => n.qualifiedName),
|
|
2005
|
+
extends: supertypes
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
function dedupeNodes(nodes) {
|
|
2010
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2011
|
+
const out2 = [];
|
|
2012
|
+
for (const n of nodes) {
|
|
2013
|
+
if (seen.has(n.id)) continue;
|
|
2014
|
+
seen.add(n.id);
|
|
2015
|
+
out2.push(n);
|
|
2016
|
+
}
|
|
2017
|
+
return out2;
|
|
2018
|
+
}
|
|
2019
|
+
function runTree(graph, params) {
|
|
2020
|
+
const name = (params.name ?? "").trim();
|
|
2021
|
+
if (!name) return { ok: false, mode: "tree", error: "bad-request", message: "name is required" };
|
|
2022
|
+
const resolved = resolveOrError(graph, name, "tree");
|
|
2023
|
+
if (!("node" in resolved)) return resolved;
|
|
2024
|
+
const index = indexFor(graph);
|
|
2025
|
+
const direction = params.callers ? "callers" : "callees";
|
|
2026
|
+
const maxDepth = Math.max(1, params.depth ?? 3);
|
|
2027
|
+
const data = toJsonTree(index, resolved.node, direction, maxDepth, /* @__PURE__ */ new Set());
|
|
2028
|
+
return { ok: true, mode: "tree", data };
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// src/lsp/enrich.ts
|
|
2032
|
+
var OSV_QUERY_URL = "https://api.osv.dev/v1/query";
|
|
2033
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2034
|
+
function key(ecosystem, name, version) {
|
|
2035
|
+
return `${ecosystem}:${name}@${version}`;
|
|
2036
|
+
}
|
|
2037
|
+
function severityOf(v) {
|
|
2038
|
+
const dbSpecific = v.database_specific;
|
|
2039
|
+
const label = typeof dbSpecific?.severity === "string" ? dbSpecific.severity.toLowerCase() : "";
|
|
2040
|
+
if (label.includes("critical")) return "critical";
|
|
2041
|
+
if (label.includes("high")) return "high";
|
|
2042
|
+
if (label.includes("moderate") || label.includes("medium")) return "moderate";
|
|
2043
|
+
if (label.includes("low")) return "low";
|
|
2044
|
+
const sev = Array.isArray(v.severity) ? v.severity : [];
|
|
2045
|
+
const cvss = sev.find((s) => typeof s.score === "string" && String(s.type).startsWith("CVSS"));
|
|
2046
|
+
if (cvss) {
|
|
2047
|
+
const m = /\/AV:.*$/.test(String(cvss.score)) ? null : Number(cvss.score);
|
|
2048
|
+
if (m != null && !Number.isNaN(m)) {
|
|
2049
|
+
if (m >= 9) return "critical";
|
|
2050
|
+
if (m >= 7) return "high";
|
|
2051
|
+
if (m >= 4) return "moderate";
|
|
2052
|
+
if (m > 0) return "low";
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return "unknown";
|
|
2056
|
+
}
|
|
2057
|
+
function toSummary(raw) {
|
|
2058
|
+
const v = raw ?? {};
|
|
2059
|
+
const aliases = Array.isArray(v.aliases) ? v.aliases.map(String) : [];
|
|
2060
|
+
const details = typeof v.details === "string" ? v.details : "";
|
|
2061
|
+
const summary = typeof v.summary === "string" && v.summary || (details ? details.split("\n")[0].slice(0, 140) : "") || "Advisory";
|
|
2062
|
+
return {
|
|
2063
|
+
id: String(v.id ?? "unknown"),
|
|
2064
|
+
cves: aliases.filter((a) => a.startsWith("CVE-")),
|
|
2065
|
+
summary,
|
|
2066
|
+
severity: severityOf(v)
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
async function enrichVulns(ecosystem, name, version, opts = { offline: false }) {
|
|
2070
|
+
if (!ecosystem || !version) return { vulns: [], cveCount: 0, source: "error" };
|
|
2071
|
+
if (opts.offline) return { vulns: [], cveCount: 0, source: "offline" };
|
|
2072
|
+
const k = key(ecosystem, name, version);
|
|
2073
|
+
const cached = cache.get(k);
|
|
2074
|
+
if (cached) return { ...cached, source: "cache" };
|
|
2075
|
+
try {
|
|
2076
|
+
const controller = new AbortController();
|
|
2077
|
+
const timer = setTimeout(() => controller.abort(), 8e3);
|
|
2078
|
+
const res = await fetch(OSV_QUERY_URL, {
|
|
2079
|
+
method: "POST",
|
|
2080
|
+
headers: { "content-type": "application/json" },
|
|
2081
|
+
body: JSON.stringify({ version, package: { name, ecosystem } }),
|
|
2082
|
+
signal: controller.signal
|
|
2083
|
+
}).finally(() => clearTimeout(timer));
|
|
2084
|
+
if (!res.ok) return { vulns: [], cveCount: 0, source: "error" };
|
|
2085
|
+
const data = await res.json();
|
|
2086
|
+
const vulns = (data.vulns ?? []).map(toSummary);
|
|
2087
|
+
const cveCount = new Set(vulns.flatMap((v) => v.cves)).size;
|
|
2088
|
+
const result = { vulns, cveCount, source: "osv" };
|
|
2089
|
+
cache.set(k, result);
|
|
2090
|
+
return result;
|
|
2091
|
+
} catch {
|
|
2092
|
+
return { vulns: [], cveCount: 0, source: "error" };
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
1783
2095
|
|
|
1784
2096
|
// src/lsp/server.ts
|
|
1785
2097
|
function bandForDependency(dep) {
|
|
@@ -1816,6 +2128,8 @@ var VibgrateLanguageServer = class {
|
|
|
1816
2128
|
queued = false;
|
|
1817
2129
|
debounce = null;
|
|
1818
2130
|
shuttingDown = false;
|
|
2131
|
+
graph = null;
|
|
2132
|
+
graphBuilding = false;
|
|
1819
2133
|
register() {
|
|
1820
2134
|
this.conn.onRequest("initialize", () => ({
|
|
1821
2135
|
capabilities: {
|
|
@@ -1830,7 +2144,10 @@ var VibgrateLanguageServer = class {
|
|
|
1830
2144
|
},
|
|
1831
2145
|
serverInfo: { name: "Vibgrate", version: VERSION }
|
|
1832
2146
|
}));
|
|
1833
|
-
this.conn.onNotification("initialized", () =>
|
|
2147
|
+
this.conn.onNotification("initialized", () => {
|
|
2148
|
+
this.scheduleScan(0);
|
|
2149
|
+
void this.ensureGraph();
|
|
2150
|
+
});
|
|
1834
2151
|
this.conn.onNotification("textDocument/didOpen", (_m, params) => {
|
|
1835
2152
|
const p = params;
|
|
1836
2153
|
const uri = p.textDocument?.uri;
|
|
@@ -1859,6 +2176,9 @@ var VibgrateLanguageServer = class {
|
|
|
1859
2176
|
});
|
|
1860
2177
|
this.conn.onRequest("textDocument/hover", (_m, params) => this.onHover(params));
|
|
1861
2178
|
this.conn.onRequest("textDocument/codeLens", (_m, params) => this.onCodeLens(params));
|
|
2179
|
+
this.conn.onRequest("vibgrate/graph/query", (_m, params) => this.onGraphQuery(params));
|
|
2180
|
+
this.conn.onRequest("vibgrate/score/forFile", (_m, params) => this.onScoreForFile(params));
|
|
2181
|
+
this.conn.onRequest("vibgrate/enrich", (_m, params) => this.onEnrich(params));
|
|
1862
2182
|
this.conn.onRequest("workspace/executeCommand", (_m, params) => {
|
|
1863
2183
|
const p = params;
|
|
1864
2184
|
if (p.command === "vibgrate.rescan") {
|
|
@@ -1888,20 +2208,27 @@ var VibgrateLanguageServer = class {
|
|
|
1888
2208
|
}
|
|
1889
2209
|
this.scanning = true;
|
|
1890
2210
|
try {
|
|
1891
|
-
const
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2211
|
+
const cacheKey = { manifestHash: manifestHash(this.opts.root), toolVersion: VERSION, offline: this.opts.offline };
|
|
2212
|
+
const cached = loadScanCache(this.opts.root, cacheKey);
|
|
2213
|
+
if (cached) {
|
|
2214
|
+
this.artifact = cached.artifact;
|
|
2215
|
+
} else {
|
|
2216
|
+
const scanOpts = {
|
|
2217
|
+
format: "json",
|
|
2218
|
+
concurrency: 8,
|
|
2219
|
+
offline: this.opts.offline,
|
|
2220
|
+
noLocalArtifacts: true,
|
|
2221
|
+
// an editor scan must not litter the repo
|
|
2222
|
+
noGraph: true,
|
|
2223
|
+
// the code map is not needed to score drift; skip the cost
|
|
2224
|
+
quiet: true,
|
|
2225
|
+
// no spinners, no artifact dump — an editor is not a terminal
|
|
2226
|
+
vibgrateVersion: VERSION
|
|
2227
|
+
};
|
|
2228
|
+
const advanced = await loadAdvancedScanHook();
|
|
2229
|
+
this.artifact = await runCoreScan(this.opts.root, scanOpts, advanced);
|
|
2230
|
+
writeScanCache(this.opts.root, cacheKey, this.artifact);
|
|
2231
|
+
}
|
|
1905
2232
|
this.publishScore();
|
|
1906
2233
|
for (const uri of this.docs.keys()) this.publishForDoc(uri);
|
|
1907
2234
|
} catch (err) {
|
|
@@ -1943,12 +2270,15 @@ var VibgrateLanguageServer = class {
|
|
|
1943
2270
|
band,
|
|
1944
2271
|
// v3 §2.4: `estimated` means "no timestamps at all" — it is NOT an
|
|
1945
2272
|
// offline marker. An air-gapped scan against a dated snapshot is Verified.
|
|
1946
|
-
|
|
2273
|
+
// The score now carries the authoritative provenance (driftscore-3.0
|
|
2274
|
+
// envelope); fall back to the artifact heuristic for pre-v3 engines.
|
|
2275
|
+
mode: a.drift.mode ?? (hasReleaseDates(a) ? "verified" : "estimated"),
|
|
1947
2276
|
methodology: a.drift.methodologyVersion ?? "unknown",
|
|
1948
2277
|
scale: "0 best, 100 worst",
|
|
1949
2278
|
counts: { behind, eol, unmaintained, total },
|
|
1950
2279
|
rootPath: a.rootPath,
|
|
1951
|
-
scannedAt: a.timestamp
|
|
2280
|
+
scannedAt: a.timestamp,
|
|
2281
|
+
...buildBreakdown(a.drift, a.projects ?? []) ? { breakdown: buildBreakdown(a.drift, a.projects ?? []) } : {}
|
|
1952
2282
|
};
|
|
1953
2283
|
this.conn.notify("vibgrate/score", payload);
|
|
1954
2284
|
}
|
|
@@ -2009,8 +2339,9 @@ var VibgrateLanguageServer = class {
|
|
|
2009
2339
|
(d) => findPackageLine(text, d.package, kind) === line
|
|
2010
2340
|
);
|
|
2011
2341
|
if (!dep) return null;
|
|
2342
|
+
const contribution = a.drift.dependencyDrift?.top.find((t) => t.package === dep.package)?.drift ?? null;
|
|
2012
2343
|
return {
|
|
2013
|
-
contents: { kind: "markdown", value: hoverMarkdown(dep) }
|
|
2344
|
+
contents: { kind: "markdown", value: hoverMarkdown(dep, contribution) }
|
|
2014
2345
|
};
|
|
2015
2346
|
}
|
|
2016
2347
|
onCodeLens(params) {
|
|
@@ -2027,7 +2358,8 @@ var VibgrateLanguageServer = class {
|
|
|
2027
2358
|
const eol = (a.findings ?? []).filter(
|
|
2028
2359
|
(f) => f.ruleId === "vibgrate/runtime-eol" || f.ruleId === "vibgrate/eol"
|
|
2029
2360
|
).length;
|
|
2030
|
-
const
|
|
2361
|
+
const estimated = (a.drift.mode ?? (hasReleaseDates(a) ? "verified" : "estimated")) === "estimated";
|
|
2362
|
+
const mode = estimated ? "~" : "";
|
|
2031
2363
|
const title = `Vibgrate \xB7 drift ${mode}${a.drift.score} (${a.drift.riskLevel}) \xB7 ${behind} behind \xB7 ${eol} EOL`;
|
|
2032
2364
|
return [
|
|
2033
2365
|
{
|
|
@@ -2036,6 +2368,78 @@ var VibgrateLanguageServer = class {
|
|
|
2036
2368
|
}
|
|
2037
2369
|
];
|
|
2038
2370
|
}
|
|
2371
|
+
// ── Graph ────────────────────────────────────────────────────────────────
|
|
2372
|
+
/**
|
|
2373
|
+
* Load the committed map if one exists, else build it once (this is the
|
|
2374
|
+
* "first activation sets up the graph" step). Fire-and-forget from
|
|
2375
|
+
* `initialized` so it never blocks the drift score; a query that arrives
|
|
2376
|
+
* mid-build awaits the same in-flight promise via the `graphBuilding` guard.
|
|
2377
|
+
*/
|
|
2378
|
+
async ensureGraph() {
|
|
2379
|
+
if (!this.opts.graph) {
|
|
2380
|
+
this.conn.notify("vibgrate/graph/status", { state: "disabled" });
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
if (this.graph || this.graphBuilding) return;
|
|
2384
|
+
this.graphBuilding = true;
|
|
2385
|
+
try {
|
|
2386
|
+
const existing = loadGraph(this.opts.root);
|
|
2387
|
+
if (existing) {
|
|
2388
|
+
this.graph = existing;
|
|
2389
|
+
this.conn.notify("vibgrate/graph/status", { state: "ready" });
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
2392
|
+
this.conn.notify("vibgrate/graph/status", { state: "building" });
|
|
2393
|
+
const result = await buildGraph({ root: this.opts.root });
|
|
2394
|
+
writeArtifacts(result.graph, { root: this.opts.root, html: false, report: false });
|
|
2395
|
+
writeSnapshot(this.opts.root, result.graph.provenance.corpusHash, result.fileStats, {});
|
|
2396
|
+
this.graph = result.graph;
|
|
2397
|
+
this.conn.notify("vibgrate/graph/status", { state: "ready" });
|
|
2398
|
+
} catch (err) {
|
|
2399
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2400
|
+
this.conn.notify("window/logMessage", { type: 3, message: `Vibgrate graph build failed: ${message}` });
|
|
2401
|
+
this.conn.notify("vibgrate/graph/status", { state: "error", message });
|
|
2402
|
+
} finally {
|
|
2403
|
+
this.graphBuilding = false;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
/** The graph, refreshed if the working tree drifted since the last build (same probe `vg ask` uses). */
|
|
2407
|
+
async graphForQuery() {
|
|
2408
|
+
await this.ensureGraph();
|
|
2409
|
+
if (!this.graph) return null;
|
|
2410
|
+
const refreshed = await refreshIfStale(this.opts.root);
|
|
2411
|
+
if (refreshed.status === "refreshed" && refreshed.wrote) {
|
|
2412
|
+
const reloaded = loadGraph(this.opts.root);
|
|
2413
|
+
if (reloaded) this.graph = reloaded;
|
|
2414
|
+
}
|
|
2415
|
+
return this.graph;
|
|
2416
|
+
}
|
|
2417
|
+
async onGraphQuery(params) {
|
|
2418
|
+
const p = params;
|
|
2419
|
+
if (!this.opts.graph) {
|
|
2420
|
+
return { ok: false, mode: p.mode, error: "disabled", message: "the local Vibgrate Graph is turned off" };
|
|
2421
|
+
}
|
|
2422
|
+
const graph = await this.graphForQuery();
|
|
2423
|
+
if (!graph) {
|
|
2424
|
+
return { ok: false, mode: p.mode, error: "not-found", message: "no code map yet \u2014 it is still building" };
|
|
2425
|
+
}
|
|
2426
|
+
return runGraphQuery(graph, p, { root: this.opts.root, offline: this.opts.offline, semantic: this.opts.semantic });
|
|
2427
|
+
}
|
|
2428
|
+
/**
|
|
2429
|
+
* `vibgrate/enrich` — lazy vulnerability enrichment for one expanded package,
|
|
2430
|
+
* straight from OSV.dev (never our API). Honours the server's offline flag.
|
|
2431
|
+
*/
|
|
2432
|
+
onEnrich(params) {
|
|
2433
|
+
const p = params ?? {};
|
|
2434
|
+
return enrichVulns(p.ecosystem ?? null, p.package ?? "", p.version ?? null, { offline: this.opts.offline });
|
|
2435
|
+
}
|
|
2436
|
+
onScoreForFile(params) {
|
|
2437
|
+
const p = params;
|
|
2438
|
+
const a = this.artifact;
|
|
2439
|
+
if (!a || !p.uri) return null;
|
|
2440
|
+
const project = projectForFile(this.opts.root, a, uriToPath(p.uri));
|
|
2441
|
+
return project ? scoreForProject(a, project) : null;
|
|
2442
|
+
}
|
|
2039
2443
|
};
|
|
2040
2444
|
function uriToPath(uri) {
|
|
2041
2445
|
try {
|
|
@@ -2068,6 +2472,99 @@ function hasReleaseDates(a) {
|
|
|
2068
2472
|
}
|
|
2069
2473
|
return false;
|
|
2070
2474
|
}
|
|
2475
|
+
function scoreForProject(a, proj) {
|
|
2476
|
+
if (!proj.drift) return null;
|
|
2477
|
+
let behind = 0;
|
|
2478
|
+
let total = 0;
|
|
2479
|
+
for (const dep of proj.dependencies ?? []) {
|
|
2480
|
+
total++;
|
|
2481
|
+
if (dep.drift === "major-behind" || dep.drift === "minor-behind") behind++;
|
|
2482
|
+
}
|
|
2483
|
+
const projFindings = (a.findings ?? []).filter((f) => sameProject(f.location, proj.path));
|
|
2484
|
+
const eol = projFindings.filter((f) => f.ruleId === "vibgrate/runtime-eol" || f.ruleId === "vibgrate/eol").length;
|
|
2485
|
+
const unmaintained = projFindings.filter(
|
|
2486
|
+
(f) => f.ruleId === "vibgrate/unmaintained" || f.ruleId === "vibgrate/abandoned"
|
|
2487
|
+
).length;
|
|
2488
|
+
const hasDates = (proj.dependencies ?? []).some((d) => d.ageDays !== null && d.ageDays !== void 0);
|
|
2489
|
+
return {
|
|
2490
|
+
score: proj.drift.score,
|
|
2491
|
+
band: proj.drift.riskLevel ?? "low",
|
|
2492
|
+
mode: proj.drift.mode ?? (hasDates ? "verified" : "estimated"),
|
|
2493
|
+
methodology: proj.drift.methodologyVersion ?? a.drift.methodologyVersion ?? "unknown",
|
|
2494
|
+
scale: "0 best, 100 worst",
|
|
2495
|
+
counts: { behind, eol, unmaintained, total },
|
|
2496
|
+
rootPath: proj.path,
|
|
2497
|
+
scannedAt: a.timestamp,
|
|
2498
|
+
...buildBreakdown(proj.drift, [proj]) ? { breakdown: buildBreakdown(proj.drift, [proj]) } : {}
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
function bandForScore(n) {
|
|
2502
|
+
if (n <= 30) return "low";
|
|
2503
|
+
if (n <= 60) return "moderate";
|
|
2504
|
+
return "high";
|
|
2505
|
+
}
|
|
2506
|
+
function buildBreakdown(drift, projects) {
|
|
2507
|
+
const top = drift.dependencyDrift?.top;
|
|
2508
|
+
if (!top || top.length === 0) return void 0;
|
|
2509
|
+
const byPkg = /* @__PURE__ */ new Map();
|
|
2510
|
+
for (const p of projects) {
|
|
2511
|
+
const ecosystem = osvEcosystem(p.type);
|
|
2512
|
+
for (const d of p.dependencies ?? []) if (!byPkg.has(d.package)) byPkg.set(d.package, { row: d, ecosystem });
|
|
2513
|
+
}
|
|
2514
|
+
const items = top.filter((t) => t.drift > 0).map((t) => {
|
|
2515
|
+
const found = byPkg.get(t.package);
|
|
2516
|
+
const row = found?.row;
|
|
2517
|
+
return {
|
|
2518
|
+
package: t.package,
|
|
2519
|
+
section: row?.section ?? "dependencies",
|
|
2520
|
+
ecosystem: found?.ecosystem ?? null,
|
|
2521
|
+
drift: t.drift,
|
|
2522
|
+
band: bandForScore(t.drift),
|
|
2523
|
+
currentSpec: row?.currentSpec ?? "",
|
|
2524
|
+
resolvedVersion: row?.resolvedVersion ?? null,
|
|
2525
|
+
latestStable: row?.latestStable ?? null,
|
|
2526
|
+
majorsBehind: row?.majorsBehind ?? null,
|
|
2527
|
+
ageDays: row?.ageDays ?? null,
|
|
2528
|
+
mode: t.mode,
|
|
2529
|
+
unsupported: t.unsupported,
|
|
2530
|
+
abandoned: t.flags.includes("abandoned-floor"),
|
|
2531
|
+
flags: t.flags
|
|
2532
|
+
};
|
|
2533
|
+
});
|
|
2534
|
+
return items.length > 0 ? items : void 0;
|
|
2535
|
+
}
|
|
2536
|
+
function osvEcosystem(type) {
|
|
2537
|
+
switch (type) {
|
|
2538
|
+
case "node":
|
|
2539
|
+
case "typescript":
|
|
2540
|
+
return "npm";
|
|
2541
|
+
case "python":
|
|
2542
|
+
return "PyPI";
|
|
2543
|
+
case "java":
|
|
2544
|
+
case "kotlin":
|
|
2545
|
+
case "scala":
|
|
2546
|
+
case "groovy":
|
|
2547
|
+
return "Maven";
|
|
2548
|
+
case "go":
|
|
2549
|
+
return "Go";
|
|
2550
|
+
case "rust":
|
|
2551
|
+
return "crates.io";
|
|
2552
|
+
case "ruby":
|
|
2553
|
+
return "RubyGems";
|
|
2554
|
+
case "php":
|
|
2555
|
+
return "Packagist";
|
|
2556
|
+
case "dotnet":
|
|
2557
|
+
return "NuGet";
|
|
2558
|
+
case "dart":
|
|
2559
|
+
return "Pub";
|
|
2560
|
+
case "swift":
|
|
2561
|
+
return "SwiftURL";
|
|
2562
|
+
case "elixir":
|
|
2563
|
+
return "Hex";
|
|
2564
|
+
default:
|
|
2565
|
+
return null;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2071
2568
|
function inlineLabel(dep) {
|
|
2072
2569
|
const bits = [];
|
|
2073
2570
|
if (dep.drift === "major-behind") {
|
|
@@ -2082,16 +2579,42 @@ function inlineLabel(dep) {
|
|
|
2082
2579
|
}
|
|
2083
2580
|
return bits.join(" \xB7 ");
|
|
2084
2581
|
}
|
|
2085
|
-
function
|
|
2582
|
+
function commandUri(command, args) {
|
|
2583
|
+
return `command:${command}?${encodeURIComponent(JSON.stringify([args]))}`;
|
|
2584
|
+
}
|
|
2585
|
+
function driftBadge(dep) {
|
|
2586
|
+
if (dep.drift === "major-behind") {
|
|
2587
|
+
const n = dep.majorsBehind ?? 1;
|
|
2588
|
+
return `${n} major${n === 1 ? "" : "s"} behind`;
|
|
2589
|
+
}
|
|
2590
|
+
if (dep.drift === "minor-behind") return "minor behind";
|
|
2591
|
+
return null;
|
|
2592
|
+
}
|
|
2593
|
+
function versionJourney(yours, latest) {
|
|
2594
|
+
const from = Number(/^(\d+)/.exec(yours)?.[1]);
|
|
2595
|
+
const to = Number(/^(\d+)/.exec(latest)?.[1]);
|
|
2596
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || to <= from) return null;
|
|
2597
|
+
const majors = [];
|
|
2598
|
+
for (let m = from; m <= to; m++) majors.push(m);
|
|
2599
|
+
const shown = majors.length > 5 ? [majors[0], majors[1], -1, majors[majors.length - 2], majors[majors.length - 1]] : majors;
|
|
2600
|
+
return shown.map((m) => m === -1 ? "\u2026" : `${m}.x`).join(" \u2192 ");
|
|
2601
|
+
}
|
|
2602
|
+
function hoverMarkdown(dep, contribution) {
|
|
2086
2603
|
const lines = [];
|
|
2087
|
-
|
|
2604
|
+
const badge = driftBadge(dep);
|
|
2605
|
+
const contributionText = contribution != null ? ` \xB7 contributes +${Math.round(contribution)} to drift` : "";
|
|
2606
|
+
lines.push(badge ? `**${dep.package}** \`${badge}\`${contributionText}` : `**${dep.package}**`);
|
|
2088
2607
|
lines.push("");
|
|
2089
2608
|
const yours = dep.resolvedVersion ?? dep.currentSpec;
|
|
2609
|
+
const drifted = dep.drift === "major-behind" || dep.drift === "minor-behind";
|
|
2090
2610
|
if (dep.latestStable && dep.latestStable !== yours) {
|
|
2611
|
+
lines.push("**Currency**");
|
|
2091
2612
|
lines.push(`| | |`);
|
|
2092
2613
|
lines.push(`|---|---|`);
|
|
2093
2614
|
lines.push(`| yours | \`${yours}\` |`);
|
|
2094
2615
|
lines.push(`| latest | \`${dep.latestStable}\` |`);
|
|
2616
|
+
const journey = versionJourney(yours, dep.latestStable);
|
|
2617
|
+
if (journey) lines.push(`| journey | ${journey} |`);
|
|
2095
2618
|
if (dep.majorsBehind) lines.push(`| majors behind | ${dep.majorsBehind} |`);
|
|
2096
2619
|
if (dep.ageDays != null && dep.ageDays >= 1) {
|
|
2097
2620
|
lines.push(`| age | ${Math.round(dep.ageDays).toLocaleString("en-US")} days |`);
|
|
@@ -2104,12 +2627,31 @@ function hoverMarkdown(dep) {
|
|
|
2104
2627
|
} else {
|
|
2105
2628
|
lines.push(`\`${yours}\` \u2014 current.`);
|
|
2106
2629
|
}
|
|
2630
|
+
if (dep.unsupported || dep.abandoned) {
|
|
2631
|
+
lines.push("");
|
|
2632
|
+
lines.push("**Maintenance**");
|
|
2633
|
+
if (dep.unsupported) lines.push("$(warning) Unsupported / deprecated \u2014 floored at 70.");
|
|
2634
|
+
if (dep.abandoned) lines.push("$(warning) Abandoned \u2014 no recent releases \u2014 floored at 50.");
|
|
2635
|
+
}
|
|
2636
|
+
if (drifted) {
|
|
2637
|
+
lines.push("");
|
|
2638
|
+
lines.push("---");
|
|
2639
|
+
const routeFixUri = commandUri("vibgrate.routeFix", {
|
|
2640
|
+
package: dep.package,
|
|
2641
|
+
from: yours,
|
|
2642
|
+
to: dep.latestStable ?? yours,
|
|
2643
|
+
majorsBehind: dep.majorsBehind ?? 0
|
|
2644
|
+
});
|
|
2645
|
+
const explainUri = commandUri("vibgrate.explain", { package: dep.package });
|
|
2646
|
+
lines.push(`[Route this fix \u2192](${routeFixUri}) \xB7 [Explain this drift](${explainUri})`);
|
|
2647
|
+
}
|
|
2107
2648
|
lines.push("");
|
|
2649
|
+
lines.push("---");
|
|
2108
2650
|
lines.push("$(shield) Resolved locally from your lockfile. No source uploaded.");
|
|
2109
2651
|
return lines.join("\n");
|
|
2110
2652
|
}
|
|
2111
2653
|
function startLanguageServer(opts) {
|
|
2112
|
-
if (!
|
|
2654
|
+
if (!fs8.existsSync(opts.root)) {
|
|
2113
2655
|
process.stderr.write(`vg lsp: root does not exist: ${opts.root}
|
|
2114
2656
|
`);
|
|
2115
2657
|
process.exit(1);
|
|
@@ -2134,7 +2676,7 @@ function registerLsp(program) {
|
|
|
2134
2676
|
"--diagnostics",
|
|
2135
2677
|
"also publish Problems-panel diagnostics (EOL runtime, unmaintained, license change). Off by default: drift is not a defect, and the Problems panel is not ours to fill",
|
|
2136
2678
|
false
|
|
2137
|
-
).action(async function(opts) {
|
|
2679
|
+
).option("--no-graph", "skip the local Vibgrate Graph entirely: no background build, and graph queries report it as turned off").option("--no-semantic", "never use semantic search for graph queries (lexical only; the embedding model is not downloaded)").action(async function(opts) {
|
|
2138
2680
|
const global = readGlobal(this);
|
|
2139
2681
|
const root = rootOf(global);
|
|
2140
2682
|
startLanguageServer({
|
|
@@ -2142,7 +2684,9 @@ function registerLsp(program) {
|
|
|
2142
2684
|
// `--local` is the established air-gap flag across the CLI; honour it
|
|
2143
2685
|
// here too, so an offline editor session stays offline.
|
|
2144
2686
|
offline: global.local === true,
|
|
2145
|
-
diagnostics: opts.diagnostics === true
|
|
2687
|
+
diagnostics: opts.diagnostics === true,
|
|
2688
|
+
graph: opts.graph !== false,
|
|
2689
|
+
semantic: opts.semantic !== false
|
|
2146
2690
|
});
|
|
2147
2691
|
await new Promise(() => {
|
|
2148
2692
|
});
|
|
@@ -2153,10 +2697,25 @@ function registerLsp(program) {
|
|
|
2153
2697
|
// src/commands/install.ts
|
|
2154
2698
|
var SMALL_REPO_FILES = 150;
|
|
2155
2699
|
function registerInstall(program) {
|
|
2156
|
-
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) {
|
|
2700
|
+
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("--detect", "detect assistants in use (repo footprint, home config, PATH) and install for those; with --list, only report what was detected").option("--list", "show the support matrix and exit").option("--no-hook", "skip the advisory nudge").action(function(tools, opts) {
|
|
2157
2701
|
const global = readGlobal(this);
|
|
2158
2702
|
const root = rootOf(global);
|
|
2703
|
+
const detected = opts.detect ? detectAssistants(root) : [];
|
|
2159
2704
|
if (opts.list) {
|
|
2705
|
+
if (opts.detect) {
|
|
2706
|
+
if (global.json) {
|
|
2707
|
+
json(detected.map((d) => ({ id: d.assistant.id, label: d.assistant.label, via: d.via, marker: d.marker })));
|
|
2708
|
+
} else if (detected.length === 0) {
|
|
2709
|
+
info(`${c.cyan("vg install")} \xB7 no AI assistants detected here`);
|
|
2710
|
+
} else {
|
|
2711
|
+
info(`${c.cyan("vg install")} \xB7 detected assistants`);
|
|
2712
|
+
const pad4 = Math.max(...detected.map((d) => d.assistant.id.length)) + 2;
|
|
2713
|
+
for (const d of detected) {
|
|
2714
|
+
info(` ${c.bold(d.assistant.id.padEnd(pad4))} ${d.assistant.label} ${c.dim(`via ${d.via}: ${d.marker}`)}`);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2160
2719
|
if (global.json) {
|
|
2161
2720
|
json(ASSISTANTS.map((a) => ({ id: a.id, label: a.label, mcp: !!a.mcp, skill: !!a.skill, nudge: !!a.nudge })));
|
|
2162
2721
|
} else {
|
|
@@ -2168,8 +2727,15 @@ function registerInstall(program) {
|
|
|
2168
2727
|
}
|
|
2169
2728
|
return;
|
|
2170
2729
|
}
|
|
2171
|
-
const targets = opts.all ? ASSISTANTS : tools.map(resolve6);
|
|
2172
|
-
if (targets.length === 0)
|
|
2730
|
+
const targets = opts.all ? ASSISTANTS : opts.detect ? detected.map((d) => d.assistant) : tools.map(resolve6);
|
|
2731
|
+
if (targets.length === 0) {
|
|
2732
|
+
if (opts.detect) {
|
|
2733
|
+
if (global.json) json({ root, detected: [], results: [] });
|
|
2734
|
+
else info(`${c.cyan("vg install")} \xB7 no AI assistants detected here \u2014 name one (e.g. \`vg install claude\`) or use --all`);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
throw usageError("name an assistant (e.g. `vg install claude`) or use --all / --detect / --list");
|
|
2738
|
+
}
|
|
2173
2739
|
const graph = loadGraph(root, global.graph);
|
|
2174
2740
|
const fileCount = graph ? graph.nodes.filter((n) => n.kind === "file").length : 0;
|
|
2175
2741
|
const smallRepo = graph !== null && fileCount > 0 && fileCount < SMALL_REPO_FILES;
|
|
@@ -2224,7 +2790,7 @@ function registerShare(program) {
|
|
|
2224
2790
|
const global = readGlobal(this);
|
|
2225
2791
|
const root = rootOf(global);
|
|
2226
2792
|
const gitDir = path.join(root, ".git");
|
|
2227
|
-
if (!
|
|
2793
|
+
if (!fs8.existsSync(gitDir)) {
|
|
2228
2794
|
throw new CliError("not a git repository \u2014 run `vg share` inside a git repo", ExitCode.USAGE_ERROR);
|
|
2229
2795
|
}
|
|
2230
2796
|
if (opts.undo) {
|
|
@@ -2246,22 +2812,22 @@ function registerShare(program) {
|
|
|
2246
2812
|
}
|
|
2247
2813
|
function writeVibgrateGitignore(root, reports) {
|
|
2248
2814
|
const dir = path.join(root, ".vibgrate");
|
|
2249
|
-
|
|
2815
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
2250
2816
|
const lines = ["# Managed by `vg share` \u2014 keep graph.json committed, ignore the rest", "cache/", "facts.jsonl"];
|
|
2251
2817
|
if (!reports) lines.push("graph.html", "GRAPH_REPORT.md");
|
|
2252
|
-
|
|
2818
|
+
fs8.writeFileSync(path.join(dir, ".gitignore"), `${lines.join("\n")}
|
|
2253
2819
|
`);
|
|
2254
2820
|
}
|
|
2255
2821
|
function writeGitAttributes(root) {
|
|
2256
2822
|
const file = path.join(root, ".gitattributes");
|
|
2257
2823
|
const line = ".vibgrate/graph.json merge=vg";
|
|
2258
|
-
let content =
|
|
2824
|
+
let content = fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : "";
|
|
2259
2825
|
if (!content.split("\n").some((l) => l.trim() === line)) {
|
|
2260
2826
|
content = content.length ? `${content.replace(/\s*$/, "")}
|
|
2261
2827
|
${line}
|
|
2262
2828
|
` : `${line}
|
|
2263
2829
|
`;
|
|
2264
|
-
|
|
2830
|
+
fs8.writeFileSync(file, content);
|
|
2265
2831
|
}
|
|
2266
2832
|
}
|
|
2267
2833
|
function configureMergeDriver(root) {
|
|
@@ -2270,7 +2836,7 @@ function configureMergeDriver(root) {
|
|
|
2270
2836
|
}
|
|
2271
2837
|
function installPreCommitHook(root) {
|
|
2272
2838
|
const hooksDir = hookDir(root);
|
|
2273
|
-
|
|
2839
|
+
fs8.mkdirSync(hooksDir, { recursive: true });
|
|
2274
2840
|
const file = path.join(hooksDir, "pre-commit");
|
|
2275
2841
|
const block = [
|
|
2276
2842
|
HOOK_BEGIN,
|
|
@@ -2280,7 +2846,7 @@ function installPreCommitHook(root) {
|
|
|
2280
2846
|
"fi",
|
|
2281
2847
|
HOOK_END
|
|
2282
2848
|
].join("\n");
|
|
2283
|
-
let content =
|
|
2849
|
+
let content = fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : "#!/bin/sh\n";
|
|
2284
2850
|
const re = new RegExp(`${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}`);
|
|
2285
2851
|
if (re.test(content)) content = content.replace(re, block);
|
|
2286
2852
|
else content = `${content.replace(/\s*$/, "")}
|
|
@@ -2289,21 +2855,21 @@ ${block}
|
|
|
2289
2855
|
`;
|
|
2290
2856
|
if (!content.startsWith("#!")) content = `#!/bin/sh
|
|
2291
2857
|
${content}`;
|
|
2292
|
-
|
|
2293
|
-
|
|
2858
|
+
fs8.writeFileSync(file, content);
|
|
2859
|
+
fs8.chmodSync(file, 493);
|
|
2294
2860
|
}
|
|
2295
2861
|
function undo(root) {
|
|
2296
2862
|
const hookFile = path.join(hookDir(root), "pre-commit");
|
|
2297
|
-
if (
|
|
2863
|
+
if (fs8.existsSync(hookFile)) {
|
|
2298
2864
|
const re = new RegExp(`\\n*${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}\\n*`);
|
|
2299
|
-
const next2 =
|
|
2300
|
-
if (next2.trim() === "#!/bin/sh")
|
|
2301
|
-
else
|
|
2865
|
+
const next2 = fs8.readFileSync(hookFile, "utf8").replace(re, "\n");
|
|
2866
|
+
if (next2.trim() === "#!/bin/sh") fs8.rmSync(hookFile);
|
|
2867
|
+
else fs8.writeFileSync(hookFile, next2);
|
|
2302
2868
|
}
|
|
2303
2869
|
const attr = path.join(root, ".gitattributes");
|
|
2304
|
-
if (
|
|
2305
|
-
const next2 =
|
|
2306
|
-
|
|
2870
|
+
if (fs8.existsSync(attr)) {
|
|
2871
|
+
const next2 = fs8.readFileSync(attr, "utf8").split("\n").filter((l) => l.trim() !== ".vibgrate/graph.json merge=vg").join("\n");
|
|
2872
|
+
fs8.writeFileSync(attr, next2);
|
|
2307
2873
|
}
|
|
2308
2874
|
try {
|
|
2309
2875
|
git(root, ["config", "--remove-section", "merge.vg"]);
|
|
@@ -2436,7 +3002,7 @@ function grepBaselineTokens(term, files) {
|
|
|
2436
3002
|
if (f.size > 512 * 1024) continue;
|
|
2437
3003
|
let content = "";
|
|
2438
3004
|
try {
|
|
2439
|
-
content =
|
|
3005
|
+
content = fs8.readFileSync(f.abs, "utf8");
|
|
2440
3006
|
} catch {
|
|
2441
3007
|
continue;
|
|
2442
3008
|
}
|
|
@@ -2446,7 +3012,7 @@ function grepBaselineTokens(term, files) {
|
|
|
2446
3012
|
}
|
|
2447
3013
|
function safeSize(abs) {
|
|
2448
3014
|
try {
|
|
2449
|
-
return
|
|
3015
|
+
return fs8.statSync(abs).size;
|
|
2450
3016
|
} catch {
|
|
2451
3017
|
return 0;
|
|
2452
3018
|
}
|
|
@@ -2768,9 +3334,9 @@ function normalizeStandards(raw) {
|
|
|
2768
3334
|
const name = asString(r.name);
|
|
2769
3335
|
if (!name) continue;
|
|
2770
3336
|
const ecosystem = asString(r.ecosystem)?.toLowerCase();
|
|
2771
|
-
const
|
|
2772
|
-
if (seen.has(
|
|
2773
|
-
seen.add(
|
|
3337
|
+
const key2 = `${ecosystem ?? "*"}\0${name.toLowerCase()}`;
|
|
3338
|
+
if (seen.has(key2)) continue;
|
|
3339
|
+
seen.add(key2);
|
|
2774
3340
|
banned.push({ name, ...ecosystem ? { ecosystem } : {}, ...asString(r.use) ? { use: asString(r.use) } : {}, ...asString(r.reason) ? { reason: asString(r.reason) } : {} });
|
|
2775
3341
|
}
|
|
2776
3342
|
return { banned };
|
|
@@ -2780,7 +3346,7 @@ function loadStandards(root) {
|
|
|
2780
3346
|
const p = path__default.join(root, rel3);
|
|
2781
3347
|
let text;
|
|
2782
3348
|
try {
|
|
2783
|
-
text =
|
|
3349
|
+
text = fs8__default.readFileSync(p, "utf8");
|
|
2784
3350
|
} catch {
|
|
2785
3351
|
continue;
|
|
2786
3352
|
}
|
|
@@ -2948,7 +3514,7 @@ function printBreakdown(usage) {
|
|
|
2948
3514
|
info(c.dim(` avg success across commands: ${usage.avgSuccessPct}%`));
|
|
2949
3515
|
}
|
|
2950
3516
|
function printSplit(usage) {
|
|
2951
|
-
const label = (
|
|
3517
|
+
const label = (key2) => key2 === "mcp" ? "MCP tools" : key2 === "cli" ? "vg CLI" : key2;
|
|
2952
3518
|
if (usage.sources.length) {
|
|
2953
3519
|
info("");
|
|
2954
3520
|
info(c.bold(" by source") + c.dim(" (how the call arrived)"));
|
|
@@ -3018,7 +3584,7 @@ async function addCmd(root, rest, opts, asJson) {
|
|
|
3018
3584
|
}
|
|
3019
3585
|
function readPackageVersion(root) {
|
|
3020
3586
|
try {
|
|
3021
|
-
const pkg = JSON.parse(
|
|
3587
|
+
const pkg = JSON.parse(fs8__default.readFileSync(path__default.join(root, "package.json"), "utf8"));
|
|
3022
3588
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
3023
3589
|
} catch {
|
|
3024
3590
|
return void 0;
|
|
@@ -3026,7 +3592,7 @@ function readPackageVersion(root) {
|
|
|
3026
3592
|
}
|
|
3027
3593
|
function readOptionalFile(p) {
|
|
3028
3594
|
try {
|
|
3029
|
-
const t =
|
|
3595
|
+
const t = fs8__default.readFileSync(p, "utf8");
|
|
3030
3596
|
return t.trim() ? t : void 0;
|
|
3031
3597
|
} catch {
|
|
3032
3598
|
return void 0;
|
|
@@ -3172,8 +3738,8 @@ function registerExport(program) {
|
|
|
3172
3738
|
out(content.trimEnd());
|
|
3173
3739
|
return;
|
|
3174
3740
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3741
|
+
fs8.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
3742
|
+
fs8.writeFileSync(file, content);
|
|
3177
3743
|
info(`${c.green("\u2714")} wrote ${file} ${c.dim(`(${format})`)}`);
|
|
3178
3744
|
});
|
|
3179
3745
|
applyGlobalOptions(cmd);
|
|
@@ -3196,24 +3762,24 @@ function registerBundle(program) {
|
|
|
3196
3762
|
} catch {
|
|
3197
3763
|
throw new CliError("no grammars found to bundle (run a build first)", ExitCode.ERROR);
|
|
3198
3764
|
}
|
|
3199
|
-
|
|
3765
|
+
fs8.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
|
|
3200
3766
|
let grammarCount = 0;
|
|
3201
3767
|
for (const { fileName, absPath } of grammarFiles) {
|
|
3202
|
-
|
|
3768
|
+
fs8.copyFileSync(absPath, path.join(outDir, "grammars", fileName));
|
|
3203
3769
|
grammarCount++;
|
|
3204
3770
|
}
|
|
3205
3771
|
const included = [`grammars/ (${grammarCount} .wasm)`];
|
|
3206
3772
|
const graphSrc = global.graph ?? defaultGraphPath(root);
|
|
3207
|
-
if (
|
|
3208
|
-
|
|
3773
|
+
if (fs8.existsSync(graphSrc)) {
|
|
3774
|
+
fs8.copyFileSync(graphSrc, path.join(outDir, "graph.json"));
|
|
3209
3775
|
included.push("graph.json");
|
|
3210
3776
|
}
|
|
3211
3777
|
const cat = catalogPath(root);
|
|
3212
|
-
if (
|
|
3213
|
-
|
|
3778
|
+
if (fs8.existsSync(cat)) {
|
|
3779
|
+
fs8.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
|
|
3214
3780
|
included.push("vibgrate.lib.json");
|
|
3215
|
-
if (
|
|
3216
|
-
|
|
3781
|
+
if (fs8.existsSync(libDir(root))) {
|
|
3782
|
+
fs8.cpSync(libDir(root), path.join(outDir, "lib"), { recursive: true });
|
|
3217
3783
|
included.push("lib/");
|
|
3218
3784
|
}
|
|
3219
3785
|
}
|
|
@@ -3225,7 +3791,7 @@ function registerBundle(program) {
|
|
|
3225
3791
|
included,
|
|
3226
3792
|
usage: "Run `vg build --grammars <this dir>/grammars` to build fully offline."
|
|
3227
3793
|
};
|
|
3228
|
-
|
|
3794
|
+
fs8.writeFileSync(path.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
|
|
3229
3795
|
`);
|
|
3230
3796
|
if (global.json) {
|
|
3231
3797
|
json({ out: path.relative(root, outDir), ...manifest });
|
|
@@ -3250,7 +3816,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
3250
3816
|
console.log(chalk8.green("\u2714") + ` Created ${chalk8.bold("vibgrate.config.ts")}`);
|
|
3251
3817
|
}
|
|
3252
3818
|
if (opts.baseline) {
|
|
3253
|
-
const { runBaseline } = await import('./baseline-
|
|
3819
|
+
const { runBaseline } = await import('./baseline-YWGSHCKW.js');
|
|
3254
3820
|
await runBaseline(rootDir);
|
|
3255
3821
|
}
|
|
3256
3822
|
console.log("");
|
|
@@ -3360,9 +3926,9 @@ async function fileReferencesIdentifier(rootDir, relFile, identifier, readFile3)
|
|
|
3360
3926
|
}
|
|
3361
3927
|
async function defaultReadFile(absPath) {
|
|
3362
3928
|
try {
|
|
3363
|
-
const stat2 = await
|
|
3929
|
+
const stat2 = await fs14.stat(absPath);
|
|
3364
3930
|
if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) return null;
|
|
3365
|
-
return await
|
|
3931
|
+
return await fs14.readFile(absPath, "utf8");
|
|
3366
3932
|
} catch {
|
|
3367
3933
|
return null;
|
|
3368
3934
|
}
|
|
@@ -3391,11 +3957,11 @@ async function analyzeReachability(input) {
|
|
|
3391
3957
|
const importedModules = [];
|
|
3392
3958
|
const seenPackages = /* @__PURE__ */ new Set();
|
|
3393
3959
|
for (const dep of input.dependencies) {
|
|
3394
|
-
const
|
|
3395
|
-
if (seenPackages.has(
|
|
3396
|
-
seenPackages.add(
|
|
3960
|
+
const key2 = `${dep.ecosystem}|${dep.package}`;
|
|
3961
|
+
if (seenPackages.has(key2)) continue;
|
|
3962
|
+
seenPackages.add(key2);
|
|
3397
3963
|
const evidence = collectPackageImports(input.graph, dep.ecosystem, dep.package);
|
|
3398
|
-
evidenceByPackage.set(
|
|
3964
|
+
evidenceByPackage.set(key2, evidence);
|
|
3399
3965
|
if (evidence.supported && importedModules.length < MAX_IMPORTED_MODULE_PACKAGES) {
|
|
3400
3966
|
importedModules.push({
|
|
3401
3967
|
ecosystem: dep.ecosystem,
|
|
@@ -3410,8 +3976,8 @@ async function analyzeReachability(input) {
|
|
|
3410
3976
|
);
|
|
3411
3977
|
const findings = [];
|
|
3412
3978
|
for (const entry of input.manifest) {
|
|
3413
|
-
const
|
|
3414
|
-
const evidence = evidenceByPackage.get(
|
|
3979
|
+
const key2 = `${entry.ecosystem}|${entry.package}`;
|
|
3980
|
+
const evidence = evidenceByPackage.get(key2) ?? collectPackageImports(input.graph, entry.ecosystem, entry.package);
|
|
3415
3981
|
const baseConfidence = ECOSYSTEM_MATCH_CONFIDENCE[entry.ecosystem] ?? 0;
|
|
3416
3982
|
if (!evidence.supported) {
|
|
3417
3983
|
findings.push({
|
|
@@ -3509,9 +4075,9 @@ function collectPreflightDependencies(projects, projectTypeToEcosystem) {
|
|
|
3509
4075
|
for (const dep of project.dependencies) {
|
|
3510
4076
|
const version = dep.resolvedVersion || normalizeVersionSpec(dep.currentSpec);
|
|
3511
4077
|
if (!version) continue;
|
|
3512
|
-
const
|
|
3513
|
-
if (!deduped.has(
|
|
3514
|
-
deduped.set(
|
|
4078
|
+
const key2 = `${ecosystem}:${dep.package}:${version}`;
|
|
4079
|
+
if (!deduped.has(key2)) {
|
|
4080
|
+
deduped.set(key2, { ecosystem, package: dep.package, version });
|
|
3515
4081
|
}
|
|
3516
4082
|
}
|
|
3517
4083
|
}
|
|
@@ -3986,7 +4552,7 @@ var WATCHED_FILES = /* @__PURE__ */ new Set([
|
|
|
3986
4552
|
"packages.lock.json"
|
|
3987
4553
|
]);
|
|
3988
4554
|
var WATCHED_EXT = /\.(cs|fs)proj$/i;
|
|
3989
|
-
var
|
|
4555
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3990
4556
|
"node_modules",
|
|
3991
4557
|
".git",
|
|
3992
4558
|
"dist",
|
|
@@ -4007,7 +4573,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
4007
4573
|
function scanStaleness(rootDir, artifactPath) {
|
|
4008
4574
|
let ref;
|
|
4009
4575
|
try {
|
|
4010
|
-
ref =
|
|
4576
|
+
ref = fs8.statSync(artifactPath).mtimeMs;
|
|
4011
4577
|
} catch {
|
|
4012
4578
|
return { stale: false };
|
|
4013
4579
|
}
|
|
@@ -4019,7 +4585,7 @@ function scanStaleness(rootDir, artifactPath) {
|
|
|
4019
4585
|
if (depth > 8 || scanned > MAX_ENTRIES) return;
|
|
4020
4586
|
let entries;
|
|
4021
4587
|
try {
|
|
4022
|
-
entries =
|
|
4588
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
4023
4589
|
} catch {
|
|
4024
4590
|
return;
|
|
4025
4591
|
}
|
|
@@ -4027,14 +4593,14 @@ function scanStaleness(rootDir, artifactPath) {
|
|
|
4027
4593
|
scanned++;
|
|
4028
4594
|
if (scanned > MAX_ENTRIES) break;
|
|
4029
4595
|
if (e.isDirectory()) {
|
|
4030
|
-
if (!
|
|
4596
|
+
if (!SKIP_DIRS2.has(e.name) && !e.name.startsWith(".")) walk2(path.join(dir, e.name), depth + 1);
|
|
4031
4597
|
continue;
|
|
4032
4598
|
}
|
|
4033
4599
|
if (!WATCHED_FILES.has(e.name) && !WATCHED_EXT.test(e.name)) continue;
|
|
4034
4600
|
const abs = path.join(dir, e.name);
|
|
4035
4601
|
let mtimeMs;
|
|
4036
4602
|
try {
|
|
4037
|
-
mtimeMs =
|
|
4603
|
+
mtimeMs = fs8.statSync(abs).mtimeMs;
|
|
4038
4604
|
} catch {
|
|
4039
4605
|
continue;
|
|
4040
4606
|
}
|
|
@@ -4101,7 +4667,7 @@ function extractContracts(content, pkg, ecosystem) {
|
|
|
4101
4667
|
}
|
|
4102
4668
|
|
|
4103
4669
|
// src/reporting/planning/usage.ts
|
|
4104
|
-
var
|
|
4670
|
+
var SKIP_DIRS3 = /* @__PURE__ */ new Set([
|
|
4105
4671
|
"node_modules",
|
|
4106
4672
|
".git",
|
|
4107
4673
|
"dist",
|
|
@@ -4168,7 +4734,7 @@ function analyzeTree(root, packages) {
|
|
|
4168
4734
|
if (depth > MAX_DEPTH || filesSeen >= MAX_FILES) return;
|
|
4169
4735
|
let entries;
|
|
4170
4736
|
try {
|
|
4171
|
-
entries =
|
|
4737
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
4172
4738
|
} catch {
|
|
4173
4739
|
return;
|
|
4174
4740
|
}
|
|
@@ -4176,7 +4742,7 @@ function analyzeTree(root, packages) {
|
|
|
4176
4742
|
for (const entry of entries) {
|
|
4177
4743
|
if (filesSeen >= MAX_FILES) return;
|
|
4178
4744
|
if (entry.isDirectory()) {
|
|
4179
|
-
if (
|
|
4745
|
+
if (SKIP_DIRS3.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
4180
4746
|
walk2(path.join(dir, entry.name), depth + 1);
|
|
4181
4747
|
continue;
|
|
4182
4748
|
}
|
|
@@ -4186,8 +4752,8 @@ function analyzeTree(root, packages) {
|
|
|
4186
4752
|
const full = path.join(dir, entry.name);
|
|
4187
4753
|
let content;
|
|
4188
4754
|
try {
|
|
4189
|
-
if (
|
|
4190
|
-
content =
|
|
4755
|
+
if (fs8.statSync(full).size > MAX_FILE_BYTES2) continue;
|
|
4756
|
+
content = fs8.readFileSync(full, "utf8");
|
|
4191
4757
|
} catch {
|
|
4192
4758
|
continue;
|
|
4193
4759
|
}
|
|
@@ -4283,9 +4849,6 @@ function planBlock(plan, recommended) {
|
|
|
4283
4849
|
if (plan.upgrades.length > shown.length) {
|
|
4284
4850
|
lines.push(chalk8.dim(` \u2026 and ${plan.upgrades.length - shown.length} more`));
|
|
4285
4851
|
}
|
|
4286
|
-
if (plan.upgrades.length === 0) {
|
|
4287
|
-
lines.push(chalk8.dim(" (no upgrades in this plan)"));
|
|
4288
|
-
}
|
|
4289
4852
|
return lines.join("\n");
|
|
4290
4853
|
}
|
|
4291
4854
|
function dataNote(report) {
|
|
@@ -4312,9 +4875,15 @@ function renderText(report) {
|
|
|
4312
4875
|
out2.push(chalk8.red(`\u26A0 ${report.exploitability.kevPackages} package(s) carry a KNOWN-EXPLOITED (KEV) advisory${epss} \u2014 prioritise these.`));
|
|
4313
4876
|
}
|
|
4314
4877
|
out2.push("");
|
|
4315
|
-
|
|
4316
|
-
|
|
4878
|
+
const visiblePlans = report.plans.filter((plan) => plan.upgrades.length > 0);
|
|
4879
|
+
if (visiblePlans.length === 0) {
|
|
4880
|
+
out2.push(chalk8.green("\u2714 Nothing to upgrade \u2014 every tracked dependency is current."));
|
|
4317
4881
|
out2.push("");
|
|
4882
|
+
} else {
|
|
4883
|
+
for (const plan of visiblePlans) {
|
|
4884
|
+
out2.push(planBlock(plan, plan.tier === report.recommended));
|
|
4885
|
+
out2.push("");
|
|
4886
|
+
}
|
|
4318
4887
|
}
|
|
4319
4888
|
out2.push(chalk8.bold("Recommendation"));
|
|
4320
4889
|
out2.push(` ${chalk8.green(report.plans.find((p) => p.tier === report.recommended)?.label ?? report.recommended)} \u2014 ${report.rationale}`);
|
|
@@ -4337,7 +4906,12 @@ function renderMarkdown(report) {
|
|
|
4337
4906
|
out2.push(`_${report.vulnerabilityData === "unavailable" ? "Advisory data was unavailable \u2014 vulnerability impact is not shown." : "Advisory data was partial \u2014 some ecosystems were not checked."}_`);
|
|
4338
4907
|
}
|
|
4339
4908
|
out2.push("");
|
|
4340
|
-
|
|
4909
|
+
const visiblePlans = report.plans.filter((plan) => plan.upgrades.length > 0);
|
|
4910
|
+
if (visiblePlans.length === 0) {
|
|
4911
|
+
out2.push("\u2714 Nothing to upgrade \u2014 every tracked dependency is current.");
|
|
4912
|
+
out2.push("");
|
|
4913
|
+
}
|
|
4914
|
+
for (const plan of visiblePlans) {
|
|
4341
4915
|
const rec = plan.tier === report.recommended ? " \u2705 **recommended**" : "";
|
|
4342
4916
|
out2.push(`## ${plan.label}${rec}`);
|
|
4343
4917
|
out2.push("");
|
|
@@ -4350,14 +4924,12 @@ function renderMarkdown(report) {
|
|
|
4350
4924
|
out2.push(`- \u26A0 Introduces advisories in target versions: ${deltaSummary(plan.introduces)}`);
|
|
4351
4925
|
}
|
|
4352
4926
|
out2.push("");
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
out2.push(`| \`${u.package}\` | ${u.from ?? "?"} | ${u.to ?? "?"} | ${u.kind} | ${u.reason} |`);
|
|
4358
|
-
}
|
|
4359
|
-
out2.push("");
|
|
4927
|
+
out2.push("| Package | From | To | Kind | Reason |");
|
|
4928
|
+
out2.push("|---|---|---|---|---|");
|
|
4929
|
+
for (const u of plan.upgrades) {
|
|
4930
|
+
out2.push(`| \`${u.package}\` | ${u.from ?? "?"} | ${u.to ?? "?"} | ${u.kind} | ${u.reason} |`);
|
|
4360
4931
|
}
|
|
4932
|
+
out2.push("");
|
|
4361
4933
|
}
|
|
4362
4934
|
out2.push("## Recommendation");
|
|
4363
4935
|
out2.push("");
|
|
@@ -4370,6 +4942,29 @@ function renderMarkdown(report) {
|
|
|
4370
4942
|
return out2.join("\n");
|
|
4371
4943
|
}
|
|
4372
4944
|
|
|
4945
|
+
// src/reporting/planning/dedupe.ts
|
|
4946
|
+
var TIER_ORDER = ["safe", "balanced", "aggressive"];
|
|
4947
|
+
function upgradeSetKey(plan) {
|
|
4948
|
+
return plan.upgrades.map((u) => `${u.ecosystem}\0${u.package}\0${u.from ?? ""}\0${u.to ?? ""}`).sort().join("\n");
|
|
4949
|
+
}
|
|
4950
|
+
function dedupePlans(plans) {
|
|
4951
|
+
const byTier = (plan) => {
|
|
4952
|
+
const i = TIER_ORDER.indexOf(plan.tier);
|
|
4953
|
+
return i === -1 ? TIER_ORDER.length : i;
|
|
4954
|
+
};
|
|
4955
|
+
const ordered = [...plans].sort((a, b) => byTier(a) - byTier(b));
|
|
4956
|
+
const survivorByKey = /* @__PURE__ */ new Map();
|
|
4957
|
+
const canonicalTier = /* @__PURE__ */ new Map();
|
|
4958
|
+
for (const plan of ordered) {
|
|
4959
|
+
const key2 = upgradeSetKey(plan);
|
|
4960
|
+
const survivor = survivorByKey.get(key2) ?? plan;
|
|
4961
|
+
if (!survivorByKey.has(key2)) survivorByKey.set(key2, plan);
|
|
4962
|
+
canonicalTier.set(plan.tier, survivor.tier);
|
|
4963
|
+
}
|
|
4964
|
+
const survivors = new Set(survivorByKey.values());
|
|
4965
|
+
return { plans: plans.filter((p) => survivors.has(p)), canonicalTier };
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4373
4968
|
// src/reporting/planning/expected-drift.ts
|
|
4374
4969
|
function estimateDriftScore(artifact, upgraded) {
|
|
4375
4970
|
const projects = JSON.parse(JSON.stringify(artifact.projects ?? []));
|
|
@@ -4526,22 +5121,56 @@ async function fetchLatestVersion() {
|
|
|
4526
5121
|
}
|
|
4527
5122
|
async function writeCache(data) {
|
|
4528
5123
|
try {
|
|
4529
|
-
await
|
|
4530
|
-
await
|
|
5124
|
+
await fs14.mkdir(CACHE_DIR, { recursive: true });
|
|
5125
|
+
await fs14.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
4531
5126
|
} catch {
|
|
4532
5127
|
}
|
|
4533
5128
|
}
|
|
4534
5129
|
|
|
4535
5130
|
// src/reporting/commands/update.ts
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
if (execPath
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
5131
|
+
var PACKAGE_NAME = "@vibgrate/cli";
|
|
5132
|
+
function detectGlobalInstall(execPath = process.argv[1] || "", cwd = process.cwd()) {
|
|
5133
|
+
if (!execPath) return null;
|
|
5134
|
+
let resolved = execPath;
|
|
5135
|
+
try {
|
|
5136
|
+
resolved = fs8.realpathSync(execPath);
|
|
5137
|
+
} catch {
|
|
5138
|
+
}
|
|
5139
|
+
const p = resolved.replace(/\\/g, "/");
|
|
5140
|
+
if (!p.includes("/node_modules/")) return null;
|
|
5141
|
+
if (p.includes("/_npx/") || p.includes("/dlx-") || p.includes("/.bunx/")) return null;
|
|
5142
|
+
const workdir = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
5143
|
+
if (workdir && (p === workdir || p.startsWith(workdir + "/"))) return null;
|
|
5144
|
+
if (p.includes("/pnpm/")) return "pnpm";
|
|
5145
|
+
if (p.includes("/yarn/") || p.includes("/.yarn/")) return "yarn";
|
|
5146
|
+
if (p.includes("/.bun/")) return "bun";
|
|
5147
|
+
return "npm";
|
|
5148
|
+
}
|
|
5149
|
+
var defaultRunner = (cmd) => execSync(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
|
|
5150
|
+
async function findGlobalInstall(pkg = PACKAGE_NAME, run = defaultRunner) {
|
|
5151
|
+
const tryRun = (cmd) => {
|
|
5152
|
+
try {
|
|
5153
|
+
const out2 = run(cmd).trim().split("\n")[0]?.trim();
|
|
5154
|
+
return out2 || null;
|
|
5155
|
+
} catch {
|
|
5156
|
+
return null;
|
|
4544
5157
|
}
|
|
5158
|
+
};
|
|
5159
|
+
const probes = [
|
|
5160
|
+
{ pm: "npm", root: () => tryRun("npm root -g") },
|
|
5161
|
+
{ pm: "pnpm", root: () => tryRun("pnpm root -g") },
|
|
5162
|
+
{
|
|
5163
|
+
pm: "yarn",
|
|
5164
|
+
root: () => {
|
|
5165
|
+
const dir = tryRun("yarn global dir");
|
|
5166
|
+
return dir ? path.join(dir, "node_modules") : null;
|
|
5167
|
+
}
|
|
5168
|
+
},
|
|
5169
|
+
{ pm: "bun", root: () => path.join(os.homedir(), ".bun", "install", "global", "node_modules") }
|
|
5170
|
+
];
|
|
5171
|
+
for (const probe of probes) {
|
|
5172
|
+
const root = probe.root();
|
|
5173
|
+
if (root && await pathExists(path.join(root, pkg))) return probe.pm;
|
|
4545
5174
|
}
|
|
4546
5175
|
return null;
|
|
4547
5176
|
}
|
|
@@ -4591,15 +5220,16 @@ function getInstallCommand(pm, pkg, version, isDev, opts = {}) {
|
|
|
4591
5220
|
return isDev ? `npm install --save-dev ${spec}` : `npm install ${spec}`;
|
|
4592
5221
|
}
|
|
4593
5222
|
}
|
|
4594
|
-
async function
|
|
5223
|
+
async function getLocalDependencyState(cwd, pkg = PACKAGE_NAME) {
|
|
4595
5224
|
try {
|
|
4596
5225
|
const pkgPath = path.join(cwd, "package.json");
|
|
4597
5226
|
const raw = await (await import('fs/promises')).readFile(pkgPath, "utf-8");
|
|
4598
|
-
const
|
|
4599
|
-
|
|
5227
|
+
const parsed = JSON.parse(raw);
|
|
5228
|
+
if (parsed.devDependencies?.[pkg]) return "dev";
|
|
5229
|
+
if (parsed.dependencies?.[pkg]) return "prod";
|
|
4600
5230
|
} catch {
|
|
4601
|
-
return true;
|
|
4602
5231
|
}
|
|
5232
|
+
return null;
|
|
4603
5233
|
}
|
|
4604
5234
|
async function confirmWorkspaceRoot(pm) {
|
|
4605
5235
|
if (!(process.stdin.isTTY && process.stdout.isTTY)) return false;
|
|
@@ -4632,15 +5262,29 @@ var updateCommand = new Command("update").description("Update vibgrate to the la
|
|
|
4632
5262
|
return;
|
|
4633
5263
|
}
|
|
4634
5264
|
const cwd = process.cwd();
|
|
4635
|
-
const
|
|
4636
|
-
|
|
4637
|
-
|
|
5265
|
+
const runtimeGlobalPm = detectGlobalInstall();
|
|
5266
|
+
let globalPm = runtimeGlobalPm;
|
|
5267
|
+
let isGlobal = Boolean(opts.global) || runtimeGlobalPm !== null;
|
|
5268
|
+
let localState = null;
|
|
5269
|
+
if (!isGlobal) {
|
|
5270
|
+
localState = await getLocalDependencyState(cwd);
|
|
5271
|
+
if (localState === null) {
|
|
5272
|
+
globalPm = await findGlobalInstall();
|
|
5273
|
+
if (globalPm) {
|
|
5274
|
+
isGlobal = true;
|
|
5275
|
+
console.log(
|
|
5276
|
+
chalk8.dim(`Found a global ${globalPm} installation of ${PACKAGE_NAME} \u2014 updating it instead of this project.`)
|
|
5277
|
+
);
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
const pm = opts.pm || globalPm || await detectPackageManager(cwd);
|
|
4638
5282
|
let cmd;
|
|
4639
5283
|
if (isGlobal) {
|
|
4640
|
-
cmd = getGlobalUpdateCommand(pm,
|
|
5284
|
+
cmd = getGlobalUpdateCommand(pm, PACKAGE_NAME, latest);
|
|
4641
5285
|
console.log(chalk8.dim(`Updating global installation with ${pm}: ${cmd}`));
|
|
4642
5286
|
} else {
|
|
4643
|
-
const isDev =
|
|
5287
|
+
const isDev = localState !== "prod";
|
|
4644
5288
|
let workspaceRoot = false;
|
|
4645
5289
|
if (pm === "pnpm" && await detectWorkspaceRoot(cwd)) {
|
|
4646
5290
|
console.log(
|
|
@@ -4648,19 +5292,19 @@ var updateCommand = new Command("update").description("Update vibgrate to the la
|
|
|
4648
5292
|
);
|
|
4649
5293
|
const proceed = opts.yes || opts.workspaceRoot || await confirmWorkspaceRoot(pm);
|
|
4650
5294
|
if (!proceed) {
|
|
4651
|
-
const rootCmd = getInstallCommand(pm,
|
|
5295
|
+
const rootCmd = getInstallCommand(pm, PACKAGE_NAME, latest, isDev, { workspaceRoot: true });
|
|
4652
5296
|
console.log(chalk8.dim(`Skipped. To update the workspace root, run: ${rootCmd}`));
|
|
4653
5297
|
console.log(chalk8.dim('or re-run "vg update --yes" (or --workspace-root) to let vg do it.'));
|
|
4654
5298
|
return;
|
|
4655
5299
|
}
|
|
4656
5300
|
workspaceRoot = true;
|
|
4657
5301
|
}
|
|
4658
|
-
cmd = getInstallCommand(pm,
|
|
5302
|
+
cmd = getInstallCommand(pm, PACKAGE_NAME, latest, isDev, { workspaceRoot });
|
|
4659
5303
|
console.log(chalk8.dim(`Using ${pm}: ${cmd}`));
|
|
4660
5304
|
}
|
|
4661
5305
|
try {
|
|
4662
5306
|
execSync(cmd, { cwd, stdio: "inherit" });
|
|
4663
|
-
console.log(chalk8.green("\u2714") + ` Updated to
|
|
5307
|
+
console.log(chalk8.green("\u2714") + ` Updated to ${PACKAGE_NAME}@${latest}`);
|
|
4664
5308
|
} catch {
|
|
4665
5309
|
console.error(chalk8.red(`Update failed. Run manually: ${cmd}`));
|
|
4666
5310
|
process.exit(1);
|
|
@@ -4684,11 +5328,11 @@ function collectCandidates(artifact) {
|
|
|
4684
5328
|
for (const project of artifact.projects ?? []) {
|
|
4685
5329
|
const ecosystem = ecosystemId(project.type);
|
|
4686
5330
|
for (const dep of project.dependencies ?? []) {
|
|
4687
|
-
const
|
|
4688
|
-
if (seen.has(
|
|
5331
|
+
const key2 = `${ecosystem}\0${dep.package}`;
|
|
5332
|
+
if (seen.has(key2)) continue;
|
|
4689
5333
|
if (!dep.latestStable || dep.latestStable === dep.resolvedVersion) continue;
|
|
4690
5334
|
if (dep.drift === "current" || dep.drift === "unknown") continue;
|
|
4691
|
-
seen.add(
|
|
5335
|
+
seen.add(key2);
|
|
4692
5336
|
out2.push({
|
|
4693
5337
|
package: dep.package,
|
|
4694
5338
|
ecosystem,
|
|
@@ -4826,6 +5470,9 @@ var fixCommand = new Command("fix").description("Get ranked, risk-tiered upgrade
|
|
|
4826
5470
|
if (response.requestId) console.error(chalk8.dim(` (ref ${response.requestId})`));
|
|
4827
5471
|
process.exit(1);
|
|
4828
5472
|
}
|
|
5473
|
+
const { plans: dedupedPlans, canonicalTier } = dedupePlans(response.plans);
|
|
5474
|
+
response.plans = dedupedPlans;
|
|
5475
|
+
response.recommended = canonicalTier.get(response.recommended) ?? response.recommended;
|
|
4829
5476
|
const currentDrift = artifact.drift?.score;
|
|
4830
5477
|
if (typeof currentDrift === "number") {
|
|
4831
5478
|
response.currentDriftScore = currentDrift;
|
|
@@ -4838,7 +5485,7 @@ var fixCommand = new Command("fix").description("Get ranked, risk-tiered upgrade
|
|
|
4838
5485
|
}
|
|
4839
5486
|
emit(response, opts.format);
|
|
4840
5487
|
if (opts.format === "text" && opts.apply !== false) {
|
|
4841
|
-
await runApplyFlow(rootDir, artifact, response, opts);
|
|
5488
|
+
await runApplyFlow(rootDir, artifact, response, opts, canonicalTier);
|
|
4842
5489
|
}
|
|
4843
5490
|
if (failOn) {
|
|
4844
5491
|
const recommended = response.plans.find((p) => p.tier === response.recommended);
|
|
@@ -4847,7 +5494,8 @@ var fixCommand = new Command("fix").description("Get ranked, risk-tiered upgrade
|
|
|
4847
5494
|
for (const sev of Object.keys(SEVERITY_RANK)) {
|
|
4848
5495
|
if (SEVERITY_RANK[sev] < threshold) continue;
|
|
4849
5496
|
const open = response.unresolved.bySeverity[sev] ?? 0;
|
|
4850
|
-
const
|
|
5497
|
+
const maxFixable = Math.max(0, ...response.plans.map((p) => p.fixes.bySeverity[sev] ?? 0));
|
|
5498
|
+
const notFixedByRecommended = maxFixable - (recommended?.fixes.bySeverity[sev] ?? 0);
|
|
4851
5499
|
stillOpen += open + Math.max(0, notFixedByRecommended);
|
|
4852
5500
|
}
|
|
4853
5501
|
if (stillOpen > 0) {
|
|
@@ -4904,8 +5552,8 @@ async function buildTargetResolver(rootDir, artifact) {
|
|
|
4904
5552
|
const ecosystem = ecosystemId(project.type);
|
|
4905
5553
|
const dir = relProjectDir(rootDir, project.path);
|
|
4906
5554
|
for (const dep of project.dependencies ?? []) {
|
|
4907
|
-
const
|
|
4908
|
-
(owners.get(
|
|
5555
|
+
const key2 = `${ecosystem}\0${dep.package}`;
|
|
5556
|
+
(owners.get(key2) ?? owners.set(key2, /* @__PURE__ */ new Set()).get(key2)).add(dir);
|
|
4909
5557
|
}
|
|
4910
5558
|
}
|
|
4911
5559
|
const rootFallback = [{ dir: ".", isWorkspaceRoot: rootIsWorkspace }];
|
|
@@ -4946,7 +5594,7 @@ function promptPlanSelection(response) {
|
|
|
4946
5594
|
});
|
|
4947
5595
|
});
|
|
4948
5596
|
}
|
|
4949
|
-
async function runApplyFlow(rootDir, artifact, response, opts) {
|
|
5597
|
+
async function runApplyFlow(rootDir, artifact, response, opts, canonicalTier) {
|
|
4950
5598
|
const nonEmpty = response.plans.filter((p) => p.upgrades.length > 0);
|
|
4951
5599
|
if (nonEmpty.length === 0) {
|
|
4952
5600
|
console.log(chalk8.green("\n\u2714 Nothing to upgrade \u2014 every tracked dependency is current."));
|
|
@@ -4954,7 +5602,8 @@ async function runApplyFlow(rootDir, artifact, response, opts) {
|
|
|
4954
5602
|
}
|
|
4955
5603
|
let chosen;
|
|
4956
5604
|
if (opts.plan) {
|
|
4957
|
-
|
|
5605
|
+
const tier = canonicalTier?.get(opts.plan) ?? opts.plan;
|
|
5606
|
+
chosen = response.plans.find((p) => p.tier === tier);
|
|
4958
5607
|
if (!chosen) {
|
|
4959
5608
|
console.error(chalk8.red(`Unknown plan '${opts.plan}'. Use safe, balanced, or aggressive.`));
|
|
4960
5609
|
process.exit(1);
|
|
@@ -6199,21 +6848,21 @@ function formatDeltaText(base, current) {
|
|
|
6199
6848
|
const added = [];
|
|
6200
6849
|
const removed = [];
|
|
6201
6850
|
const changed = [];
|
|
6202
|
-
for (const [
|
|
6203
|
-
if (!baseMap.has(
|
|
6204
|
-
added.push(`${
|
|
6851
|
+
for (const [key2, dep] of currentMap.entries()) {
|
|
6852
|
+
if (!baseMap.has(key2)) {
|
|
6853
|
+
added.push(`${key2} @ ${dep.resolvedVersion ?? dep.currentSpec}`);
|
|
6205
6854
|
continue;
|
|
6206
6855
|
}
|
|
6207
|
-
const prev = baseMap.get(
|
|
6856
|
+
const prev = baseMap.get(key2);
|
|
6208
6857
|
const prevVersion = prev.resolvedVersion ?? prev.currentSpec;
|
|
6209
6858
|
const nowVersion = dep.resolvedVersion ?? dep.currentSpec;
|
|
6210
6859
|
if (prevVersion !== nowVersion || prev.majorsBehind !== dep.majorsBehind) {
|
|
6211
|
-
changed.push(`${
|
|
6860
|
+
changed.push(`${key2} ${prevVersion} -> ${nowVersion} (majorsBehind ${prev.majorsBehind ?? "unknown"} -> ${dep.majorsBehind ?? "unknown"})`);
|
|
6212
6861
|
}
|
|
6213
6862
|
}
|
|
6214
|
-
for (const [
|
|
6215
|
-
if (!currentMap.has(
|
|
6216
|
-
removed.push(`${
|
|
6863
|
+
for (const [key2, dep] of baseMap.entries()) {
|
|
6864
|
+
if (!currentMap.has(key2)) {
|
|
6865
|
+
removed.push(`${key2} @ ${dep.resolvedVersion ?? dep.currentSpec}`);
|
|
6217
6866
|
}
|
|
6218
6867
|
}
|
|
6219
6868
|
const lines = [
|
|
@@ -6422,7 +7071,7 @@ function dispatch(argv, cwd) {
|
|
|
6422
7071
|
}
|
|
6423
7072
|
const looksLikeQuestion = /\s/.test(first) || first.endsWith("?");
|
|
6424
7073
|
if (looksLikeQuestion) return ["ask", ...args];
|
|
6425
|
-
if (
|
|
7074
|
+
if (fs8.existsSync(path.resolve(cwd, first))) {
|
|
6426
7075
|
return [hasBuildOnlyFlag(args) ? "build" : "scan", ...args];
|
|
6427
7076
|
}
|
|
6428
7077
|
const moved = MOVED_COMMANDS[first];
|