@vibgrate/cli 2026.711.2 → 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 +55 -2
- package/README.md +2 -2
- package/dist/baseline-YWGSHCKW.js +6 -0
- package/dist/{baseline-SRVG4ZAD.js.map → baseline-YWGSHCKW.js.map} +1 -1
- package/dist/{chunk-75ZJYYJE.js → chunk-L42NVMD6.js} +268 -111
- package/dist/chunk-L42NVMD6.js.map +1 -0
- package/dist/{chunk-NNU2PW2H.js → chunk-M62BGJMK.js} +274 -56
- package/dist/chunk-M62BGJMK.js.map +1 -0
- package/dist/{chunk-X5YT263H.js → chunk-VFO5UDAT.js} +21 -2
- package/dist/chunk-VFO5UDAT.js.map +1 -0
- package/dist/{chunk-4IHO6VUL.js → chunk-WZNIDXZP.js} +628 -4
- package/dist/chunk-WZNIDXZP.js.map +1 -0
- package/dist/cli.js +1426 -169
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +29 -6
- package/dist/index.js +3 -3
- package/dist/parse-worker.d.ts +1 -1
- package/dist/parse-worker.js +2 -1
- package/dist/parse-worker.js.map +1 -1
- package/dist/{types-CXnp1uGj.d.ts → types-BgNa-FZQ.d.ts} +8 -0
- package/package.json +2 -2
- package/dist/baseline-SRVG4ZAD.js +0 -6
- package/dist/chunk-4IHO6VUL.js.map +0 -1
- package/dist/chunk-75ZJYYJE.js.map +0 -1
- package/dist/chunk-NNU2PW2H.js.map +0 -1
- package/dist/chunk-X5YT263H.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
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-
|
|
4
|
-
import { loadAdvancedScanHook, baselineCommand } from './chunk-
|
|
5
|
-
import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, 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
|
-
import { fileURLToPath } from 'url';
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
13
13
|
import { Command, CommanderError, Option } from 'commander';
|
|
14
14
|
import { execSync, spawn, spawnSync, execFileSync, execFile } from 'child_process';
|
|
15
15
|
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
|
}
|
|
@@ -245,15 +245,15 @@ async function gitCommitInfo(root) {
|
|
|
245
245
|
return { sha, shortSha: sha.slice(0, 7), branch, dirty: dirty === true ? true : dirty === false ? false : void 0 };
|
|
246
246
|
}
|
|
247
247
|
function gitBranch(root) {
|
|
248
|
-
return new Promise((
|
|
248
|
+
return new Promise((resolve17) => {
|
|
249
249
|
execFile(
|
|
250
250
|
"git",
|
|
251
251
|
["rev-parse", "--abbrev-ref", "HEAD"],
|
|
252
252
|
{ cwd: root, timeout: 5e3, windowsHide: true, encoding: "utf8" },
|
|
253
253
|
(err, stdout) => {
|
|
254
|
-
if (err) return
|
|
254
|
+
if (err) return resolve17(void 0);
|
|
255
255
|
const name = String(stdout).trim();
|
|
256
|
-
|
|
256
|
+
resolve17(name && name !== "HEAD" ? name : void 0);
|
|
257
257
|
}
|
|
258
258
|
);
|
|
259
259
|
});
|
|
@@ -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
|
|
@@ -1569,19 +1569,1153 @@ async function serveHttp(graphPath, host, port, opts, freshness) {
|
|
|
1569
1569
|
info(c.red(`vg serve: request error: ${err.message}`));
|
|
1570
1570
|
}
|
|
1571
1571
|
});
|
|
1572
|
-
await new Promise((
|
|
1572
|
+
await new Promise((resolve17) => httpServer.listen(port, host, resolve17));
|
|
1573
1573
|
info(c.dim(`vg \xB7 MCP server on http://${host}:${port}/mcp (read-only, local, ${freshness})`));
|
|
1574
1574
|
await new Promise(() => {
|
|
1575
1575
|
});
|
|
1576
1576
|
}
|
|
1577
1577
|
|
|
1578
|
+
// src/lsp/protocol.ts
|
|
1579
|
+
var ErrorCodes = {
|
|
1580
|
+
MethodNotFound: -32601,
|
|
1581
|
+
InternalError: -32603
|
|
1582
|
+
};
|
|
1583
|
+
var MessageReader = class {
|
|
1584
|
+
buf = Buffer.alloc(0);
|
|
1585
|
+
/** Feed a chunk; returns every complete message now available. */
|
|
1586
|
+
push(chunk) {
|
|
1587
|
+
this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
|
|
1588
|
+
const out2 = [];
|
|
1589
|
+
for (; ; ) {
|
|
1590
|
+
const headerEnd = this.buf.indexOf("\r\n\r\n");
|
|
1591
|
+
if (headerEnd === -1) break;
|
|
1592
|
+
const header = this.buf.subarray(0, headerEnd).toString("ascii");
|
|
1593
|
+
const match = /content-length:\s*(\d+)/i.exec(header);
|
|
1594
|
+
if (!match) {
|
|
1595
|
+
this.buf = this.buf.subarray(headerEnd + 4);
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
const length = Number(match[1]);
|
|
1599
|
+
const bodyStart = headerEnd + 4;
|
|
1600
|
+
if (this.buf.length < bodyStart + length) break;
|
|
1601
|
+
const body = this.buf.subarray(bodyStart, bodyStart + length).toString("utf8");
|
|
1602
|
+
this.buf = this.buf.subarray(bodyStart + length);
|
|
1603
|
+
try {
|
|
1604
|
+
out2.push(JSON.parse(body));
|
|
1605
|
+
} catch {
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return out2;
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
var MessageWriter = class {
|
|
1612
|
+
constructor(out2) {
|
|
1613
|
+
this.out = out2;
|
|
1614
|
+
}
|
|
1615
|
+
out;
|
|
1616
|
+
write(msg) {
|
|
1617
|
+
const body = Buffer.from(JSON.stringify(msg), "utf8");
|
|
1618
|
+
this.out.write(`Content-Length: ${body.length}\r
|
|
1619
|
+
\r
|
|
1620
|
+
`);
|
|
1621
|
+
this.out.write(body);
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
var Connection = class {
|
|
1625
|
+
reader = new MessageReader();
|
|
1626
|
+
writer;
|
|
1627
|
+
requests = /* @__PURE__ */ new Map();
|
|
1628
|
+
notifications = /* @__PURE__ */ new Map();
|
|
1629
|
+
constructor(input = process.stdin, output = process.stdout) {
|
|
1630
|
+
this.writer = new MessageWriter(output);
|
|
1631
|
+
input.on("data", (chunk) => {
|
|
1632
|
+
for (const msg of this.reader.push(chunk)) void this.dispatch(msg);
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
onRequest(method, handler) {
|
|
1636
|
+
this.requests.set(method, handler);
|
|
1637
|
+
}
|
|
1638
|
+
onNotification(method, handler) {
|
|
1639
|
+
this.notifications.set(method, handler);
|
|
1640
|
+
}
|
|
1641
|
+
/** Push a server→client notification (diagnostics, `vibgrate/score`, …). */
|
|
1642
|
+
notify(method, params) {
|
|
1643
|
+
this.writer.write({ jsonrpc: "2.0", method, params });
|
|
1644
|
+
}
|
|
1645
|
+
async dispatch(msg) {
|
|
1646
|
+
if (!msg.method) return;
|
|
1647
|
+
const isRequest = msg.id !== void 0 && msg.id !== null;
|
|
1648
|
+
if (!isRequest) {
|
|
1649
|
+
const handler2 = this.notifications.get(msg.method);
|
|
1650
|
+
if (handler2) {
|
|
1651
|
+
try {
|
|
1652
|
+
await handler2(msg.method, msg.params);
|
|
1653
|
+
} catch {
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
const handler = this.requests.get(msg.method);
|
|
1659
|
+
if (!handler) {
|
|
1660
|
+
this.writer.write({
|
|
1661
|
+
jsonrpc: "2.0",
|
|
1662
|
+
id: msg.id,
|
|
1663
|
+
error: { code: ErrorCodes.MethodNotFound, message: `Unhandled method: ${msg.method}` }
|
|
1664
|
+
});
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
try {
|
|
1668
|
+
const result = await handler(msg.method, msg.params);
|
|
1669
|
+
this.writer.write({ jsonrpc: "2.0", id: msg.id, result: result ?? null });
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
this.writer.write({
|
|
1672
|
+
jsonrpc: "2.0",
|
|
1673
|
+
id: msg.id,
|
|
1674
|
+
error: {
|
|
1675
|
+
code: ErrorCodes.InternalError,
|
|
1676
|
+
message: err instanceof Error ? err.message : "Internal error"
|
|
1677
|
+
}
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
// src/lsp/manifest-positions.ts
|
|
1684
|
+
function manifestKind(filePath) {
|
|
1685
|
+
const base = (filePath.split(/[\\/]/).pop() ?? "").toLowerCase();
|
|
1686
|
+
if (base === "package.json") return "package.json";
|
|
1687
|
+
if (base === "go.mod") return "go.mod";
|
|
1688
|
+
if (base === "requirements.txt" || /^requirements.*\.txt$/.test(base)) return "requirements.txt";
|
|
1689
|
+
if (base === "pyproject.toml") return "pyproject.toml";
|
|
1690
|
+
if (base === "cargo.toml") return "Cargo.toml";
|
|
1691
|
+
if (base === "pom.xml") return "pom.xml";
|
|
1692
|
+
if (base === "build.gradle" || base === "build.gradle.kts") return "build.gradle";
|
|
1693
|
+
if (base === "gemfile") return "Gemfile";
|
|
1694
|
+
if (base === "composer.json") return "composer.json";
|
|
1695
|
+
if (base === "pubspec.yaml") return "pubspec.yaml";
|
|
1696
|
+
if (base === "mix.exs") return "mix.exs";
|
|
1697
|
+
if (base.endsWith(".csproj") || base.endsWith(".fsproj") || base.endsWith(".vbproj")) return "csproj";
|
|
1698
|
+
if (base === "dockerfile" || base.startsWith("dockerfile.")) return "Dockerfile";
|
|
1699
|
+
return "unknown";
|
|
1700
|
+
}
|
|
1701
|
+
function isManifest(filePath) {
|
|
1702
|
+
return manifestKind(filePath) !== "unknown";
|
|
1703
|
+
}
|
|
1704
|
+
function esc(s) {
|
|
1705
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1706
|
+
}
|
|
1707
|
+
function findPackageLine(text, pkg, kind) {
|
|
1708
|
+
const lines = text.split(/\r?\n/);
|
|
1709
|
+
const p = esc(pkg);
|
|
1710
|
+
const patterns = (() => {
|
|
1711
|
+
switch (kind) {
|
|
1712
|
+
case "package.json":
|
|
1713
|
+
case "composer.json":
|
|
1714
|
+
return [new RegExp(`^\\s*"${p}"\\s*:`)];
|
|
1715
|
+
case "go.mod":
|
|
1716
|
+
return [new RegExp(`^\\s*(?:require\\s+)?${p}\\s+v?\\d`)];
|
|
1717
|
+
case "requirements.txt":
|
|
1718
|
+
return [new RegExp(`^\\s*${p}\\s*(?:\\[[^\\]]*\\])?\\s*(?:[=<>!~]|$)`, "i")];
|
|
1719
|
+
case "pyproject.toml":
|
|
1720
|
+
case "Cargo.toml":
|
|
1721
|
+
return [
|
|
1722
|
+
new RegExp(`^\\s*${p}\\s*=`),
|
|
1723
|
+
new RegExp(`^\\s*"${p}\\s*(?:[=<>!~\\[]|")`)
|
|
1724
|
+
];
|
|
1725
|
+
case "pom.xml":
|
|
1726
|
+
return [new RegExp(`<artifactId>\\s*${p}\\s*</artifactId>`)];
|
|
1727
|
+
case "build.gradle":
|
|
1728
|
+
return [new RegExp(`["']${p}(?:["':])`)];
|
|
1729
|
+
case "Gemfile":
|
|
1730
|
+
return [new RegExp(`^\\s*gem\\s+["']${p}["']`)];
|
|
1731
|
+
case "pubspec.yaml":
|
|
1732
|
+
return [new RegExp(`^\\s*${p}\\s*:`)];
|
|
1733
|
+
case "mix.exs":
|
|
1734
|
+
return [new RegExp(`\\{\\s*:${p}\\s*,`)];
|
|
1735
|
+
case "csproj":
|
|
1736
|
+
return [new RegExp(`Include\\s*=\\s*"${p}"`, "i")];
|
|
1737
|
+
case "Dockerfile":
|
|
1738
|
+
return [new RegExp(`^\\s*FROM\\s+.*${p}`, "i")];
|
|
1739
|
+
default:
|
|
1740
|
+
return [];
|
|
1741
|
+
}
|
|
1742
|
+
})();
|
|
1743
|
+
for (const re of patterns) {
|
|
1744
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1745
|
+
if (re.test(lines[i])) return i;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
return -1;
|
|
1749
|
+
}
|
|
1750
|
+
function findRuntimeLine(text, kind) {
|
|
1751
|
+
const lines = text.split(/\r?\n/);
|
|
1752
|
+
const patterns = (() => {
|
|
1753
|
+
switch (kind) {
|
|
1754
|
+
case "package.json":
|
|
1755
|
+
return [/^\s*"node"\s*:/, /^\s*"engines"\s*:/];
|
|
1756
|
+
case "go.mod":
|
|
1757
|
+
return [/^\s*go\s+\d/];
|
|
1758
|
+
case "pyproject.toml":
|
|
1759
|
+
return [/^\s*(?:requires-python|python)\s*=/];
|
|
1760
|
+
case "pom.xml":
|
|
1761
|
+
return [/<(?:java\.version|maven\.compiler\.(?:source|target|release))>/];
|
|
1762
|
+
case "build.gradle":
|
|
1763
|
+
return [/(?:sourceCompatibility|targetCompatibility|JavaVersion)/];
|
|
1764
|
+
case "csproj":
|
|
1765
|
+
return [/<TargetFrameworks?>/i];
|
|
1766
|
+
case "Dockerfile":
|
|
1767
|
+
return [/^\s*FROM\s+/i];
|
|
1768
|
+
default:
|
|
1769
|
+
return [];
|
|
1770
|
+
}
|
|
1771
|
+
})();
|
|
1772
|
+
for (const re of patterns) {
|
|
1773
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1774
|
+
if (re.test(lines[i])) return i;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return -1;
|
|
1778
|
+
}
|
|
1779
|
+
function endOfLine(text, line) {
|
|
1780
|
+
const lines = text.split(/\r?\n/);
|
|
1781
|
+
return (lines[line] ?? "").length;
|
|
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
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// src/lsp/server.ts
|
|
2097
|
+
function bandForDependency(dep) {
|
|
2098
|
+
if (dep.drift === "major-behind") {
|
|
2099
|
+
return (dep.majorsBehind ?? 1) >= 2 ? "high" : "moderate";
|
|
2100
|
+
}
|
|
2101
|
+
if (dep.drift === "minor-behind") return "moderate";
|
|
2102
|
+
return "low";
|
|
2103
|
+
}
|
|
2104
|
+
function severityFor(level) {
|
|
2105
|
+
if (level === "note") return 3;
|
|
2106
|
+
return 2;
|
|
2107
|
+
}
|
|
2108
|
+
var DIAGNOSTIC_RULES = /* @__PURE__ */ new Set([
|
|
2109
|
+
"vibgrate/runtime-eol",
|
|
2110
|
+
"vibgrate/runtime-lag",
|
|
2111
|
+
"vibgrate/unmaintained",
|
|
2112
|
+
"vibgrate/abandoned",
|
|
2113
|
+
"vibgrate/license-change",
|
|
2114
|
+
"vibgrate/eol"
|
|
2115
|
+
]);
|
|
2116
|
+
var VibgrateLanguageServer = class {
|
|
2117
|
+
constructor(opts, output = process.stdout) {
|
|
2118
|
+
this.opts = opts;
|
|
2119
|
+
this.conn = new Connection(process.stdin, output);
|
|
2120
|
+
this.register();
|
|
2121
|
+
}
|
|
2122
|
+
opts;
|
|
2123
|
+
conn;
|
|
2124
|
+
docs = /* @__PURE__ */ new Map();
|
|
2125
|
+
// uri → text
|
|
2126
|
+
artifact = null;
|
|
2127
|
+
scanning = false;
|
|
2128
|
+
queued = false;
|
|
2129
|
+
debounce = null;
|
|
2130
|
+
shuttingDown = false;
|
|
2131
|
+
graph = null;
|
|
2132
|
+
graphBuilding = false;
|
|
2133
|
+
register() {
|
|
2134
|
+
this.conn.onRequest("initialize", () => ({
|
|
2135
|
+
capabilities: {
|
|
2136
|
+
// 1 = Full sync. We only actually act on save, but full sync keeps our
|
|
2137
|
+
// buffer honest so hovers land on the text the user is looking at.
|
|
2138
|
+
textDocumentSync: 1,
|
|
2139
|
+
hoverProvider: true,
|
|
2140
|
+
codeLensProvider: { resolveProvider: false },
|
|
2141
|
+
executeCommandProvider: {
|
|
2142
|
+
commands: ["vibgrate.rescan", "vibgrate.explain", "vibgrate.routeFix"]
|
|
2143
|
+
}
|
|
2144
|
+
},
|
|
2145
|
+
serverInfo: { name: "Vibgrate", version: VERSION }
|
|
2146
|
+
}));
|
|
2147
|
+
this.conn.onNotification("initialized", () => {
|
|
2148
|
+
this.scheduleScan(0);
|
|
2149
|
+
void this.ensureGraph();
|
|
2150
|
+
});
|
|
2151
|
+
this.conn.onNotification("textDocument/didOpen", (_m, params) => {
|
|
2152
|
+
const p = params;
|
|
2153
|
+
const uri = p.textDocument?.uri;
|
|
2154
|
+
if (!uri || !isManifest(uriToPath(uri))) return;
|
|
2155
|
+
this.docs.set(uri, p.textDocument?.text ?? "");
|
|
2156
|
+
if (this.artifact) this.publishForDoc(uri);
|
|
2157
|
+
else this.scheduleScan(0);
|
|
2158
|
+
});
|
|
2159
|
+
this.conn.onNotification("textDocument/didChange", (_m, params) => {
|
|
2160
|
+
const p = params;
|
|
2161
|
+
const uri = p.textDocument?.uri;
|
|
2162
|
+
if (!uri || !this.docs.has(uri)) return;
|
|
2163
|
+
const full = p.contentChanges?.[p.contentChanges.length - 1]?.text;
|
|
2164
|
+
if (typeof full === "string") this.docs.set(uri, full);
|
|
2165
|
+
if (this.artifact) this.publishForDoc(uri);
|
|
2166
|
+
});
|
|
2167
|
+
this.conn.onNotification("textDocument/didSave", (_m, params) => {
|
|
2168
|
+
const p = params;
|
|
2169
|
+
const uri = p.textDocument?.uri;
|
|
2170
|
+
if (!uri || !this.docs.has(uri)) return;
|
|
2171
|
+
this.scheduleScan(400);
|
|
2172
|
+
});
|
|
2173
|
+
this.conn.onNotification("textDocument/didClose", (_m, params) => {
|
|
2174
|
+
const p = params;
|
|
2175
|
+
if (p.textDocument?.uri) this.docs.delete(p.textDocument.uri);
|
|
2176
|
+
});
|
|
2177
|
+
this.conn.onRequest("textDocument/hover", (_m, params) => this.onHover(params));
|
|
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));
|
|
2182
|
+
this.conn.onRequest("workspace/executeCommand", (_m, params) => {
|
|
2183
|
+
const p = params;
|
|
2184
|
+
if (p.command === "vibgrate.rescan") {
|
|
2185
|
+
this.scheduleScan(0);
|
|
2186
|
+
return { ok: true };
|
|
2187
|
+
}
|
|
2188
|
+
return null;
|
|
2189
|
+
});
|
|
2190
|
+
this.conn.onRequest("shutdown", () => {
|
|
2191
|
+
this.shuttingDown = true;
|
|
2192
|
+
if (this.debounce) clearTimeout(this.debounce);
|
|
2193
|
+
return null;
|
|
2194
|
+
});
|
|
2195
|
+
this.conn.onNotification("exit", () => {
|
|
2196
|
+
process.exit(this.shuttingDown ? 0 : 1);
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
// ── Scanning ─────────────────────────────────────────────────────────────
|
|
2200
|
+
scheduleScan(delayMs) {
|
|
2201
|
+
if (this.debounce) clearTimeout(this.debounce);
|
|
2202
|
+
this.debounce = setTimeout(() => void this.scan(), delayMs);
|
|
2203
|
+
}
|
|
2204
|
+
async scan() {
|
|
2205
|
+
if (this.scanning) {
|
|
2206
|
+
this.queued = true;
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
this.scanning = true;
|
|
2210
|
+
try {
|
|
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
|
+
}
|
|
2232
|
+
this.publishScore();
|
|
2233
|
+
for (const uri of this.docs.keys()) this.publishForDoc(uri);
|
|
2234
|
+
} catch (err) {
|
|
2235
|
+
this.conn.notify("window/logMessage", {
|
|
2236
|
+
type: 3,
|
|
2237
|
+
message: `Vibgrate scan failed: ${err instanceof Error ? err.message : String(err)}`
|
|
2238
|
+
});
|
|
2239
|
+
this.conn.notify("vibgrate/status", { state: "error" });
|
|
2240
|
+
} finally {
|
|
2241
|
+
this.scanning = false;
|
|
2242
|
+
if (this.queued) {
|
|
2243
|
+
this.queued = false;
|
|
2244
|
+
this.scheduleScan(0);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
// ── Publishing ───────────────────────────────────────────────────────────
|
|
2249
|
+
publishScore() {
|
|
2250
|
+
const a = this.artifact;
|
|
2251
|
+
if (!a) return;
|
|
2252
|
+
let behind = 0;
|
|
2253
|
+
let unmaintained = 0;
|
|
2254
|
+
let total = 0;
|
|
2255
|
+
for (const proj of a.projects ?? []) {
|
|
2256
|
+
for (const dep of proj.dependencies ?? []) {
|
|
2257
|
+
total++;
|
|
2258
|
+
if (dep.drift === "major-behind" || dep.drift === "minor-behind") behind++;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
const eol = (a.findings ?? []).filter(
|
|
2262
|
+
(f) => f.ruleId === "vibgrate/runtime-eol" || f.ruleId === "vibgrate/eol"
|
|
2263
|
+
).length;
|
|
2264
|
+
unmaintained = (a.findings ?? []).filter(
|
|
2265
|
+
(f) => f.ruleId === "vibgrate/unmaintained" || f.ruleId === "vibgrate/abandoned"
|
|
2266
|
+
).length;
|
|
2267
|
+
const band = a.drift.riskLevel ?? "low";
|
|
2268
|
+
const payload = {
|
|
2269
|
+
score: a.drift.score,
|
|
2270
|
+
band,
|
|
2271
|
+
// v3 §2.4: `estimated` means "no timestamps at all" — it is NOT an
|
|
2272
|
+
// offline marker. An air-gapped scan against a dated snapshot is Verified.
|
|
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"),
|
|
2276
|
+
methodology: a.drift.methodologyVersion ?? "unknown",
|
|
2277
|
+
scale: "0 best, 100 worst",
|
|
2278
|
+
counts: { behind, eol, unmaintained, total },
|
|
2279
|
+
rootPath: a.rootPath,
|
|
2280
|
+
scannedAt: a.timestamp,
|
|
2281
|
+
...buildBreakdown(a.drift, a.projects ?? []) ? { breakdown: buildBreakdown(a.drift, a.projects ?? []) } : {}
|
|
2282
|
+
};
|
|
2283
|
+
this.conn.notify("vibgrate/score", payload);
|
|
2284
|
+
}
|
|
2285
|
+
/** Decorations + (optionally) diagnostics for one open manifest. */
|
|
2286
|
+
publishForDoc(uri) {
|
|
2287
|
+
const a = this.artifact;
|
|
2288
|
+
const text = this.docs.get(uri);
|
|
2289
|
+
if (!a || text === void 0) return;
|
|
2290
|
+
const filePath = uriToPath(uri);
|
|
2291
|
+
const kind = manifestKind(filePath);
|
|
2292
|
+
const project = projectForFile(this.opts.root, a, filePath);
|
|
2293
|
+
const items = [];
|
|
2294
|
+
for (const dep of project?.dependencies ?? []) {
|
|
2295
|
+
if (dep.drift === "current" || dep.drift === "unknown") continue;
|
|
2296
|
+
const line = findPackageLine(text, dep.package, kind);
|
|
2297
|
+
if (line === -1) continue;
|
|
2298
|
+
items.push({
|
|
2299
|
+
line,
|
|
2300
|
+
endCol: endOfLine(text, line),
|
|
2301
|
+
text: inlineLabel(dep),
|
|
2302
|
+
band: bandForDependency(dep),
|
|
2303
|
+
package: dep.package
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
this.conn.notify("vibgrate/inline", { uri, items });
|
|
2307
|
+
if (!this.opts.diagnostics) {
|
|
2308
|
+
this.conn.notify("textDocument/publishDiagnostics", { uri, diagnostics: [] });
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
const diagnostics = (a.findings ?? []).filter((f) => DIAGNOSTIC_RULES.has(f.ruleId)).filter((f) => project && sameProject(f.location, project.path)).map((f) => {
|
|
2312
|
+
const line = Math.max(0, findRuntimeLine(text, kind));
|
|
2313
|
+
return {
|
|
2314
|
+
range: {
|
|
2315
|
+
start: { line, character: 0 },
|
|
2316
|
+
end: { line, character: endOfLine(text, line) }
|
|
2317
|
+
},
|
|
2318
|
+
severity: severityFor(f.level),
|
|
2319
|
+
// never 1 — see file header
|
|
2320
|
+
source: "vibgrate",
|
|
2321
|
+
code: f.ruleId,
|
|
2322
|
+
message: f.message
|
|
2323
|
+
};
|
|
2324
|
+
});
|
|
2325
|
+
this.conn.notify("textDocument/publishDiagnostics", { uri, diagnostics });
|
|
2326
|
+
}
|
|
2327
|
+
// ── Requests ─────────────────────────────────────────────────────────────
|
|
2328
|
+
onHover(params) {
|
|
2329
|
+
const p = params;
|
|
2330
|
+
const uri = p.textDocument?.uri;
|
|
2331
|
+
const line = p.position?.line;
|
|
2332
|
+
const a = this.artifact;
|
|
2333
|
+
const text = uri ? this.docs.get(uri) : void 0;
|
|
2334
|
+
if (!uri || line === void 0 || !a || text === void 0) return null;
|
|
2335
|
+
const filePath = uriToPath(uri);
|
|
2336
|
+
const kind = manifestKind(filePath);
|
|
2337
|
+
const project = projectForFile(this.opts.root, a, filePath);
|
|
2338
|
+
const dep = (project?.dependencies ?? []).find(
|
|
2339
|
+
(d) => findPackageLine(text, d.package, kind) === line
|
|
2340
|
+
);
|
|
2341
|
+
if (!dep) return null;
|
|
2342
|
+
const contribution = a.drift.dependencyDrift?.top.find((t) => t.package === dep.package)?.drift ?? null;
|
|
2343
|
+
return {
|
|
2344
|
+
contents: { kind: "markdown", value: hoverMarkdown(dep, contribution) }
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
onCodeLens(params) {
|
|
2348
|
+
const p = params;
|
|
2349
|
+
const uri = p.textDocument?.uri;
|
|
2350
|
+
const a = this.artifact;
|
|
2351
|
+
if (!uri || !a || !this.docs.has(uri)) return [];
|
|
2352
|
+
const behind = (a.projects ?? []).reduce(
|
|
2353
|
+
(n, proj) => n + (proj.dependencies ?? []).filter(
|
|
2354
|
+
(d) => d.drift === "major-behind" || d.drift === "minor-behind"
|
|
2355
|
+
).length,
|
|
2356
|
+
0
|
|
2357
|
+
);
|
|
2358
|
+
const eol = (a.findings ?? []).filter(
|
|
2359
|
+
(f) => f.ruleId === "vibgrate/runtime-eol" || f.ruleId === "vibgrate/eol"
|
|
2360
|
+
).length;
|
|
2361
|
+
const estimated = (a.drift.mode ?? (hasReleaseDates(a) ? "verified" : "estimated")) === "estimated";
|
|
2362
|
+
const mode = estimated ? "~" : "";
|
|
2363
|
+
const title = `Vibgrate \xB7 drift ${mode}${a.drift.score} (${a.drift.riskLevel}) \xB7 ${behind} behind \xB7 ${eol} EOL`;
|
|
2364
|
+
return [
|
|
2365
|
+
{
|
|
2366
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
|
2367
|
+
command: { title, command: "vibgrate.openPanel", arguments: [] }
|
|
2368
|
+
}
|
|
2369
|
+
];
|
|
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
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
function uriToPath(uri) {
|
|
2445
|
+
try {
|
|
2446
|
+
return fileURLToPath(uri);
|
|
2447
|
+
} catch {
|
|
2448
|
+
return uri;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function projectForFile(root, a, filePath) {
|
|
2452
|
+
const dir = path.dirname(path.resolve(filePath));
|
|
2453
|
+
let best;
|
|
2454
|
+
let bestLen = -1;
|
|
2455
|
+
for (const proj of a.projects ?? []) {
|
|
2456
|
+
const projDir = path.resolve(root, proj.path);
|
|
2457
|
+
if ((dir === projDir || dir.startsWith(projDir + path.sep)) && projDir.length > bestLen) {
|
|
2458
|
+
best = proj;
|
|
2459
|
+
bestLen = projDir.length;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
return best;
|
|
2463
|
+
}
|
|
2464
|
+
function sameProject(location, projectPath) {
|
|
2465
|
+
return path.normalize(location) === path.normalize(projectPath);
|
|
2466
|
+
}
|
|
2467
|
+
function hasReleaseDates(a) {
|
|
2468
|
+
for (const proj of a.projects ?? []) {
|
|
2469
|
+
for (const dep of proj.dependencies ?? []) {
|
|
2470
|
+
if (dep.ageDays !== null && dep.ageDays !== void 0) return true;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
return false;
|
|
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
|
+
}
|
|
2568
|
+
function inlineLabel(dep) {
|
|
2569
|
+
const bits = [];
|
|
2570
|
+
if (dep.drift === "major-behind") {
|
|
2571
|
+
const n = dep.majorsBehind ?? 1;
|
|
2572
|
+
bits.push(`${n} major behind`);
|
|
2573
|
+
} else if (dep.drift === "minor-behind") {
|
|
2574
|
+
bits.push("minor behind");
|
|
2575
|
+
}
|
|
2576
|
+
if (dep.latestStable) bits.push(dep.latestStable);
|
|
2577
|
+
if (dep.ageDays != null && dep.ageDays >= 1) {
|
|
2578
|
+
bits.push(`${Math.round(dep.ageDays).toLocaleString("en-US")}d stale`);
|
|
2579
|
+
}
|
|
2580
|
+
return bits.join(" \xB7 ");
|
|
2581
|
+
}
|
|
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) {
|
|
2603
|
+
const lines = [];
|
|
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}**`);
|
|
2607
|
+
lines.push("");
|
|
2608
|
+
const yours = dep.resolvedVersion ?? dep.currentSpec;
|
|
2609
|
+
const drifted = dep.drift === "major-behind" || dep.drift === "minor-behind";
|
|
2610
|
+
if (dep.latestStable && dep.latestStable !== yours) {
|
|
2611
|
+
lines.push("**Currency**");
|
|
2612
|
+
lines.push(`| | |`);
|
|
2613
|
+
lines.push(`|---|---|`);
|
|
2614
|
+
lines.push(`| yours | \`${yours}\` |`);
|
|
2615
|
+
lines.push(`| latest | \`${dep.latestStable}\` |`);
|
|
2616
|
+
const journey = versionJourney(yours, dep.latestStable);
|
|
2617
|
+
if (journey) lines.push(`| journey | ${journey} |`);
|
|
2618
|
+
if (dep.majorsBehind) lines.push(`| majors behind | ${dep.majorsBehind} |`);
|
|
2619
|
+
if (dep.ageDays != null && dep.ageDays >= 1) {
|
|
2620
|
+
lines.push(`| age | ${Math.round(dep.ageDays).toLocaleString("en-US")} days |`);
|
|
2621
|
+
}
|
|
2622
|
+
if (dep.libyears != null && dep.libyears >= 0.01) {
|
|
2623
|
+
lines.push(`| libyears | ${dep.libyears.toFixed(2)} |`);
|
|
2624
|
+
}
|
|
2625
|
+
const license = dep.license?.spdxId ?? dep.license?.raw;
|
|
2626
|
+
if (license) lines.push(`| license | ${license} |`);
|
|
2627
|
+
} else {
|
|
2628
|
+
lines.push(`\`${yours}\` \u2014 current.`);
|
|
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
|
+
}
|
|
2648
|
+
lines.push("");
|
|
2649
|
+
lines.push("---");
|
|
2650
|
+
lines.push("$(shield) Resolved locally from your lockfile. No source uploaded.");
|
|
2651
|
+
return lines.join("\n");
|
|
2652
|
+
}
|
|
2653
|
+
function startLanguageServer(opts) {
|
|
2654
|
+
if (!fs8.existsSync(opts.root)) {
|
|
2655
|
+
process.stderr.write(`vg lsp: root does not exist: ${opts.root}
|
|
2656
|
+
`);
|
|
2657
|
+
process.exit(1);
|
|
2658
|
+
}
|
|
2659
|
+
const realWrite = process.stdout.write.bind(process.stdout);
|
|
2660
|
+
const protocolOut = {
|
|
2661
|
+
write(chunk) {
|
|
2662
|
+
return realWrite(chunk);
|
|
2663
|
+
}
|
|
2664
|
+
};
|
|
2665
|
+
process.stdout.write = ((chunk, ...rest) => process.stderr.write(chunk, ...rest));
|
|
2666
|
+
process.stderr.write(
|
|
2667
|
+
`vg lsp ${VERSION} \u2014 serving ${pathToFileURL(opts.root).href}${opts.offline ? " (offline)" : ""}
|
|
2668
|
+
`
|
|
2669
|
+
);
|
|
2670
|
+
new VibgrateLanguageServer(opts, protocolOut);
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
// src/commands/lsp.ts
|
|
2674
|
+
function registerLsp(program) {
|
|
2675
|
+
const cmd = program.command("lsp").description("start the Vibgrate language server (stdio) \u2014 the engine behind the IDE extensions").option(
|
|
2676
|
+
"--diagnostics",
|
|
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",
|
|
2678
|
+
false
|
|
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) {
|
|
2680
|
+
const global = readGlobal(this);
|
|
2681
|
+
const root = rootOf(global);
|
|
2682
|
+
startLanguageServer({
|
|
2683
|
+
root,
|
|
2684
|
+
// `--local` is the established air-gap flag across the CLI; honour it
|
|
2685
|
+
// here too, so an offline editor session stays offline.
|
|
2686
|
+
offline: global.local === true,
|
|
2687
|
+
diagnostics: opts.diagnostics === true,
|
|
2688
|
+
graph: opts.graph !== false,
|
|
2689
|
+
semantic: opts.semantic !== false
|
|
2690
|
+
});
|
|
2691
|
+
await new Promise(() => {
|
|
2692
|
+
});
|
|
2693
|
+
});
|
|
2694
|
+
applyGlobalOptions(cmd);
|
|
2695
|
+
}
|
|
2696
|
+
|
|
1578
2697
|
// src/commands/install.ts
|
|
1579
2698
|
var SMALL_REPO_FILES = 150;
|
|
1580
2699
|
function registerInstall(program) {
|
|
1581
|
-
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) {
|
|
1582
2701
|
const global = readGlobal(this);
|
|
1583
2702
|
const root = rootOf(global);
|
|
2703
|
+
const detected = opts.detect ? detectAssistants(root) : [];
|
|
1584
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
|
+
}
|
|
1585
2719
|
if (global.json) {
|
|
1586
2720
|
json(ASSISTANTS.map((a) => ({ id: a.id, label: a.label, mcp: !!a.mcp, skill: !!a.skill, nudge: !!a.nudge })));
|
|
1587
2721
|
} else {
|
|
@@ -1593,8 +2727,15 @@ function registerInstall(program) {
|
|
|
1593
2727
|
}
|
|
1594
2728
|
return;
|
|
1595
2729
|
}
|
|
1596
|
-
const targets = opts.all ? ASSISTANTS : tools.map(
|
|
1597
|
-
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
|
+
}
|
|
1598
2739
|
const graph = loadGraph(root, global.graph);
|
|
1599
2740
|
const fileCount = graph ? graph.nodes.filter((n) => n.kind === "file").length : 0;
|
|
1600
2741
|
const smallRepo = graph !== null && fileCount > 0 && fileCount < SMALL_REPO_FILES;
|
|
@@ -1617,7 +2758,7 @@ function registerInstall(program) {
|
|
|
1617
2758
|
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) {
|
|
1618
2759
|
const global = readGlobal(this);
|
|
1619
2760
|
const root = rootOf(global);
|
|
1620
|
-
const results = tools.map(
|
|
2761
|
+
const results = tools.map(resolve6).map((a) => ({ id: a.id, removed: uninstallAssistant(a, root, !!opts.purge) }));
|
|
1621
2762
|
if (global.json) {
|
|
1622
2763
|
json({ root, results });
|
|
1623
2764
|
return;
|
|
@@ -1628,7 +2769,7 @@ function registerInstall(program) {
|
|
|
1628
2769
|
});
|
|
1629
2770
|
applyGlobalOptions(uninstall);
|
|
1630
2771
|
}
|
|
1631
|
-
function
|
|
2772
|
+
function resolve6(id) {
|
|
1632
2773
|
const a = assistantById(id);
|
|
1633
2774
|
if (!a) {
|
|
1634
2775
|
const near = ASSISTANTS.filter(
|
|
@@ -1649,7 +2790,7 @@ function registerShare(program) {
|
|
|
1649
2790
|
const global = readGlobal(this);
|
|
1650
2791
|
const root = rootOf(global);
|
|
1651
2792
|
const gitDir = path.join(root, ".git");
|
|
1652
|
-
if (!
|
|
2793
|
+
if (!fs8.existsSync(gitDir)) {
|
|
1653
2794
|
throw new CliError("not a git repository \u2014 run `vg share` inside a git repo", ExitCode.USAGE_ERROR);
|
|
1654
2795
|
}
|
|
1655
2796
|
if (opts.undo) {
|
|
@@ -1671,22 +2812,22 @@ function registerShare(program) {
|
|
|
1671
2812
|
}
|
|
1672
2813
|
function writeVibgrateGitignore(root, reports) {
|
|
1673
2814
|
const dir = path.join(root, ".vibgrate");
|
|
1674
|
-
|
|
2815
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
1675
2816
|
const lines = ["# Managed by `vg share` \u2014 keep graph.json committed, ignore the rest", "cache/", "facts.jsonl"];
|
|
1676
2817
|
if (!reports) lines.push("graph.html", "GRAPH_REPORT.md");
|
|
1677
|
-
|
|
2818
|
+
fs8.writeFileSync(path.join(dir, ".gitignore"), `${lines.join("\n")}
|
|
1678
2819
|
`);
|
|
1679
2820
|
}
|
|
1680
2821
|
function writeGitAttributes(root) {
|
|
1681
2822
|
const file = path.join(root, ".gitattributes");
|
|
1682
2823
|
const line = ".vibgrate/graph.json merge=vg";
|
|
1683
|
-
let content =
|
|
2824
|
+
let content = fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : "";
|
|
1684
2825
|
if (!content.split("\n").some((l) => l.trim() === line)) {
|
|
1685
2826
|
content = content.length ? `${content.replace(/\s*$/, "")}
|
|
1686
2827
|
${line}
|
|
1687
2828
|
` : `${line}
|
|
1688
2829
|
`;
|
|
1689
|
-
|
|
2830
|
+
fs8.writeFileSync(file, content);
|
|
1690
2831
|
}
|
|
1691
2832
|
}
|
|
1692
2833
|
function configureMergeDriver(root) {
|
|
@@ -1695,7 +2836,7 @@ function configureMergeDriver(root) {
|
|
|
1695
2836
|
}
|
|
1696
2837
|
function installPreCommitHook(root) {
|
|
1697
2838
|
const hooksDir = hookDir(root);
|
|
1698
|
-
|
|
2839
|
+
fs8.mkdirSync(hooksDir, { recursive: true });
|
|
1699
2840
|
const file = path.join(hooksDir, "pre-commit");
|
|
1700
2841
|
const block = [
|
|
1701
2842
|
HOOK_BEGIN,
|
|
@@ -1705,7 +2846,7 @@ function installPreCommitHook(root) {
|
|
|
1705
2846
|
"fi",
|
|
1706
2847
|
HOOK_END
|
|
1707
2848
|
].join("\n");
|
|
1708
|
-
let content =
|
|
2849
|
+
let content = fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : "#!/bin/sh\n";
|
|
1709
2850
|
const re = new RegExp(`${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}`);
|
|
1710
2851
|
if (re.test(content)) content = content.replace(re, block);
|
|
1711
2852
|
else content = `${content.replace(/\s*$/, "")}
|
|
@@ -1714,21 +2855,21 @@ ${block}
|
|
|
1714
2855
|
`;
|
|
1715
2856
|
if (!content.startsWith("#!")) content = `#!/bin/sh
|
|
1716
2857
|
${content}`;
|
|
1717
|
-
|
|
1718
|
-
|
|
2858
|
+
fs8.writeFileSync(file, content);
|
|
2859
|
+
fs8.chmodSync(file, 493);
|
|
1719
2860
|
}
|
|
1720
2861
|
function undo(root) {
|
|
1721
2862
|
const hookFile = path.join(hookDir(root), "pre-commit");
|
|
1722
|
-
if (
|
|
2863
|
+
if (fs8.existsSync(hookFile)) {
|
|
1723
2864
|
const re = new RegExp(`\\n*${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}\\n*`);
|
|
1724
|
-
const next2 =
|
|
1725
|
-
if (next2.trim() === "#!/bin/sh")
|
|
1726
|
-
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);
|
|
1727
2868
|
}
|
|
1728
2869
|
const attr = path.join(root, ".gitattributes");
|
|
1729
|
-
if (
|
|
1730
|
-
const next2 =
|
|
1731
|
-
|
|
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);
|
|
1732
2873
|
}
|
|
1733
2874
|
try {
|
|
1734
2875
|
git(root, ["config", "--remove-section", "merge.vg"]);
|
|
@@ -1861,7 +3002,7 @@ function grepBaselineTokens(term, files) {
|
|
|
1861
3002
|
if (f.size > 512 * 1024) continue;
|
|
1862
3003
|
let content = "";
|
|
1863
3004
|
try {
|
|
1864
|
-
content =
|
|
3005
|
+
content = fs8.readFileSync(f.abs, "utf8");
|
|
1865
3006
|
} catch {
|
|
1866
3007
|
continue;
|
|
1867
3008
|
}
|
|
@@ -1871,7 +3012,7 @@ function grepBaselineTokens(term, files) {
|
|
|
1871
3012
|
}
|
|
1872
3013
|
function safeSize(abs) {
|
|
1873
3014
|
try {
|
|
1874
|
-
return
|
|
3015
|
+
return fs8.statSync(abs).size;
|
|
1875
3016
|
} catch {
|
|
1876
3017
|
return 0;
|
|
1877
3018
|
}
|
|
@@ -2193,9 +3334,9 @@ function normalizeStandards(raw) {
|
|
|
2193
3334
|
const name = asString(r.name);
|
|
2194
3335
|
if (!name) continue;
|
|
2195
3336
|
const ecosystem = asString(r.ecosystem)?.toLowerCase();
|
|
2196
|
-
const
|
|
2197
|
-
if (seen.has(
|
|
2198
|
-
seen.add(
|
|
3337
|
+
const key2 = `${ecosystem ?? "*"}\0${name.toLowerCase()}`;
|
|
3338
|
+
if (seen.has(key2)) continue;
|
|
3339
|
+
seen.add(key2);
|
|
2199
3340
|
banned.push({ name, ...ecosystem ? { ecosystem } : {}, ...asString(r.use) ? { use: asString(r.use) } : {}, ...asString(r.reason) ? { reason: asString(r.reason) } : {} });
|
|
2200
3341
|
}
|
|
2201
3342
|
return { banned };
|
|
@@ -2205,7 +3346,7 @@ function loadStandards(root) {
|
|
|
2205
3346
|
const p = path__default.join(root, rel3);
|
|
2206
3347
|
let text;
|
|
2207
3348
|
try {
|
|
2208
|
-
text =
|
|
3349
|
+
text = fs8__default.readFileSync(p, "utf8");
|
|
2209
3350
|
} catch {
|
|
2210
3351
|
continue;
|
|
2211
3352
|
}
|
|
@@ -2373,7 +3514,7 @@ function printBreakdown(usage) {
|
|
|
2373
3514
|
info(c.dim(` avg success across commands: ${usage.avgSuccessPct}%`));
|
|
2374
3515
|
}
|
|
2375
3516
|
function printSplit(usage) {
|
|
2376
|
-
const label = (
|
|
3517
|
+
const label = (key2) => key2 === "mcp" ? "MCP tools" : key2 === "cli" ? "vg CLI" : key2;
|
|
2377
3518
|
if (usage.sources.length) {
|
|
2378
3519
|
info("");
|
|
2379
3520
|
info(c.bold(" by source") + c.dim(" (how the call arrived)"));
|
|
@@ -2443,7 +3584,7 @@ async function addCmd(root, rest, opts, asJson) {
|
|
|
2443
3584
|
}
|
|
2444
3585
|
function readPackageVersion(root) {
|
|
2445
3586
|
try {
|
|
2446
|
-
const pkg = JSON.parse(
|
|
3587
|
+
const pkg = JSON.parse(fs8__default.readFileSync(path__default.join(root, "package.json"), "utf8"));
|
|
2447
3588
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
2448
3589
|
} catch {
|
|
2449
3590
|
return void 0;
|
|
@@ -2451,7 +3592,7 @@ function readPackageVersion(root) {
|
|
|
2451
3592
|
}
|
|
2452
3593
|
function readOptionalFile(p) {
|
|
2453
3594
|
try {
|
|
2454
|
-
const t =
|
|
3595
|
+
const t = fs8__default.readFileSync(p, "utf8");
|
|
2455
3596
|
return t.trim() ? t : void 0;
|
|
2456
3597
|
} catch {
|
|
2457
3598
|
return void 0;
|
|
@@ -2597,8 +3738,8 @@ function registerExport(program) {
|
|
|
2597
3738
|
out(content.trimEnd());
|
|
2598
3739
|
return;
|
|
2599
3740
|
}
|
|
2600
|
-
|
|
2601
|
-
|
|
3741
|
+
fs8.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
3742
|
+
fs8.writeFileSync(file, content);
|
|
2602
3743
|
info(`${c.green("\u2714")} wrote ${file} ${c.dim(`(${format})`)}`);
|
|
2603
3744
|
});
|
|
2604
3745
|
applyGlobalOptions(cmd);
|
|
@@ -2621,24 +3762,24 @@ function registerBundle(program) {
|
|
|
2621
3762
|
} catch {
|
|
2622
3763
|
throw new CliError("no grammars found to bundle (run a build first)", ExitCode.ERROR);
|
|
2623
3764
|
}
|
|
2624
|
-
|
|
3765
|
+
fs8.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
|
|
2625
3766
|
let grammarCount = 0;
|
|
2626
3767
|
for (const { fileName, absPath } of grammarFiles) {
|
|
2627
|
-
|
|
3768
|
+
fs8.copyFileSync(absPath, path.join(outDir, "grammars", fileName));
|
|
2628
3769
|
grammarCount++;
|
|
2629
3770
|
}
|
|
2630
3771
|
const included = [`grammars/ (${grammarCount} .wasm)`];
|
|
2631
3772
|
const graphSrc = global.graph ?? defaultGraphPath(root);
|
|
2632
|
-
if (
|
|
2633
|
-
|
|
3773
|
+
if (fs8.existsSync(graphSrc)) {
|
|
3774
|
+
fs8.copyFileSync(graphSrc, path.join(outDir, "graph.json"));
|
|
2634
3775
|
included.push("graph.json");
|
|
2635
3776
|
}
|
|
2636
3777
|
const cat = catalogPath(root);
|
|
2637
|
-
if (
|
|
2638
|
-
|
|
3778
|
+
if (fs8.existsSync(cat)) {
|
|
3779
|
+
fs8.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
|
|
2639
3780
|
included.push("vibgrate.lib.json");
|
|
2640
|
-
if (
|
|
2641
|
-
|
|
3781
|
+
if (fs8.existsSync(libDir(root))) {
|
|
3782
|
+
fs8.cpSync(libDir(root), path.join(outDir, "lib"), { recursive: true });
|
|
2642
3783
|
included.push("lib/");
|
|
2643
3784
|
}
|
|
2644
3785
|
}
|
|
@@ -2650,7 +3791,7 @@ function registerBundle(program) {
|
|
|
2650
3791
|
included,
|
|
2651
3792
|
usage: "Run `vg build --grammars <this dir>/grammars` to build fully offline."
|
|
2652
3793
|
};
|
|
2653
|
-
|
|
3794
|
+
fs8.writeFileSync(path.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
|
|
2654
3795
|
`);
|
|
2655
3796
|
if (global.json) {
|
|
2656
3797
|
json({ out: path.relative(root, outDir), ...manifest });
|
|
@@ -2675,7 +3816,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
2675
3816
|
console.log(chalk8.green("\u2714") + ` Created ${chalk8.bold("vibgrate.config.ts")}`);
|
|
2676
3817
|
}
|
|
2677
3818
|
if (opts.baseline) {
|
|
2678
|
-
const { runBaseline } = await import('./baseline-
|
|
3819
|
+
const { runBaseline } = await import('./baseline-YWGSHCKW.js');
|
|
2679
3820
|
await runBaseline(rootDir);
|
|
2680
3821
|
}
|
|
2681
3822
|
console.log("");
|
|
@@ -2785,9 +3926,9 @@ async function fileReferencesIdentifier(rootDir, relFile, identifier, readFile3)
|
|
|
2785
3926
|
}
|
|
2786
3927
|
async function defaultReadFile(absPath) {
|
|
2787
3928
|
try {
|
|
2788
|
-
const stat2 = await
|
|
3929
|
+
const stat2 = await fs14.stat(absPath);
|
|
2789
3930
|
if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) return null;
|
|
2790
|
-
return await
|
|
3931
|
+
return await fs14.readFile(absPath, "utf8");
|
|
2791
3932
|
} catch {
|
|
2792
3933
|
return null;
|
|
2793
3934
|
}
|
|
@@ -2816,11 +3957,11 @@ async function analyzeReachability(input) {
|
|
|
2816
3957
|
const importedModules = [];
|
|
2817
3958
|
const seenPackages = /* @__PURE__ */ new Set();
|
|
2818
3959
|
for (const dep of input.dependencies) {
|
|
2819
|
-
const
|
|
2820
|
-
if (seenPackages.has(
|
|
2821
|
-
seenPackages.add(
|
|
3960
|
+
const key2 = `${dep.ecosystem}|${dep.package}`;
|
|
3961
|
+
if (seenPackages.has(key2)) continue;
|
|
3962
|
+
seenPackages.add(key2);
|
|
2822
3963
|
const evidence = collectPackageImports(input.graph, dep.ecosystem, dep.package);
|
|
2823
|
-
evidenceByPackage.set(
|
|
3964
|
+
evidenceByPackage.set(key2, evidence);
|
|
2824
3965
|
if (evidence.supported && importedModules.length < MAX_IMPORTED_MODULE_PACKAGES) {
|
|
2825
3966
|
importedModules.push({
|
|
2826
3967
|
ecosystem: dep.ecosystem,
|
|
@@ -2835,8 +3976,8 @@ async function analyzeReachability(input) {
|
|
|
2835
3976
|
);
|
|
2836
3977
|
const findings = [];
|
|
2837
3978
|
for (const entry of input.manifest) {
|
|
2838
|
-
const
|
|
2839
|
-
const evidence = evidenceByPackage.get(
|
|
3979
|
+
const key2 = `${entry.ecosystem}|${entry.package}`;
|
|
3980
|
+
const evidence = evidenceByPackage.get(key2) ?? collectPackageImports(input.graph, entry.ecosystem, entry.package);
|
|
2840
3981
|
const baseConfidence = ECOSYSTEM_MATCH_CONFIDENCE[entry.ecosystem] ?? 0;
|
|
2841
3982
|
if (!evidence.supported) {
|
|
2842
3983
|
findings.push({
|
|
@@ -2934,9 +4075,9 @@ function collectPreflightDependencies(projects, projectTypeToEcosystem) {
|
|
|
2934
4075
|
for (const dep of project.dependencies) {
|
|
2935
4076
|
const version = dep.resolvedVersion || normalizeVersionSpec(dep.currentSpec);
|
|
2936
4077
|
if (!version) continue;
|
|
2937
|
-
const
|
|
2938
|
-
if (!deduped.has(
|
|
2939
|
-
deduped.set(
|
|
4078
|
+
const key2 = `${ecosystem}:${dep.package}:${version}`;
|
|
4079
|
+
if (!deduped.has(key2)) {
|
|
4080
|
+
deduped.set(key2, { ecosystem, package: dep.package, version });
|
|
2940
4081
|
}
|
|
2941
4082
|
}
|
|
2942
4083
|
}
|
|
@@ -3058,7 +4199,9 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
3058
4199
|
if (opts.strict) process.exit(1);
|
|
3059
4200
|
return;
|
|
3060
4201
|
}
|
|
3061
|
-
const
|
|
4202
|
+
const config = await loadConfig(rootDir);
|
|
4203
|
+
const databaseSchemaCaps = config.scanners !== false ? config.scanners?.databaseSchema : void 0;
|
|
4204
|
+
const { body, contentEncoding } = await prepareCompressedUpload(artifact, { databaseSchemaCaps });
|
|
3062
4205
|
const timestamp = String(Date.now());
|
|
3063
4206
|
let host = parsed.host;
|
|
3064
4207
|
if (opts.region) {
|
|
@@ -3083,7 +4226,11 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
3083
4226
|
body,
|
|
3084
4227
|
contentEncoding,
|
|
3085
4228
|
timestamp,
|
|
3086
|
-
force: opts.force
|
|
4229
|
+
force: opts.force,
|
|
4230
|
+
// Set only by an automated caller running the scan on the workspace's
|
|
4231
|
+
// behalf (e.g. a Vibgrate-hosted remediation run) — never a customer flag.
|
|
4232
|
+
runId: process.env.VIBGRATE_SCAN_RUN_ID,
|
|
4233
|
+
runToken: process.env.VIBGRATE_SCAN_RUN_TOKEN
|
|
3087
4234
|
});
|
|
3088
4235
|
host = uploadedHost;
|
|
3089
4236
|
if (!response.ok) {
|
|
@@ -3158,7 +4305,7 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
|
|
|
3158
4305
|
'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.',
|
|
3159
4306
|
collectExcludes,
|
|
3160
4307
|
[]
|
|
3161
|
-
).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
|
|
4308
|
+
).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 DriftScore 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").option("--quiet", "Suppress promotional output (the free-plan tracking panel and the AI Context install prompt); scan results are unaffected").action(async (targetPath, opts) => {
|
|
3162
4309
|
const rootDir = path.resolve(targetPath);
|
|
3163
4310
|
if (!await pathExists$1(rootDir)) {
|
|
3164
4311
|
console.error(chalk8.red(`Path does not exist: ${rootDir}`));
|
|
@@ -3332,7 +4479,7 @@ Failing: findings detected at warn level or above.`));
|
|
|
3332
4479
|
}
|
|
3333
4480
|
if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
|
|
3334
4481
|
console.error(chalk8.red(`
|
|
3335
|
-
Failing fitness function:
|
|
4482
|
+
Failing fitness function: DriftScore ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
|
|
3336
4483
|
process.exit(2);
|
|
3337
4484
|
}
|
|
3338
4485
|
if (scanOpts.driftWorseningPercent !== void 0) {
|
|
@@ -3405,7 +4552,7 @@ var WATCHED_FILES = /* @__PURE__ */ new Set([
|
|
|
3405
4552
|
"packages.lock.json"
|
|
3406
4553
|
]);
|
|
3407
4554
|
var WATCHED_EXT = /\.(cs|fs)proj$/i;
|
|
3408
|
-
var
|
|
4555
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3409
4556
|
"node_modules",
|
|
3410
4557
|
".git",
|
|
3411
4558
|
"dist",
|
|
@@ -3426,7 +4573,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
3426
4573
|
function scanStaleness(rootDir, artifactPath) {
|
|
3427
4574
|
let ref;
|
|
3428
4575
|
try {
|
|
3429
|
-
ref =
|
|
4576
|
+
ref = fs8.statSync(artifactPath).mtimeMs;
|
|
3430
4577
|
} catch {
|
|
3431
4578
|
return { stale: false };
|
|
3432
4579
|
}
|
|
@@ -3438,7 +4585,7 @@ function scanStaleness(rootDir, artifactPath) {
|
|
|
3438
4585
|
if (depth > 8 || scanned > MAX_ENTRIES) return;
|
|
3439
4586
|
let entries;
|
|
3440
4587
|
try {
|
|
3441
|
-
entries =
|
|
4588
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
3442
4589
|
} catch {
|
|
3443
4590
|
return;
|
|
3444
4591
|
}
|
|
@@ -3446,14 +4593,14 @@ function scanStaleness(rootDir, artifactPath) {
|
|
|
3446
4593
|
scanned++;
|
|
3447
4594
|
if (scanned > MAX_ENTRIES) break;
|
|
3448
4595
|
if (e.isDirectory()) {
|
|
3449
|
-
if (!
|
|
4596
|
+
if (!SKIP_DIRS2.has(e.name) && !e.name.startsWith(".")) walk2(path.join(dir, e.name), depth + 1);
|
|
3450
4597
|
continue;
|
|
3451
4598
|
}
|
|
3452
4599
|
if (!WATCHED_FILES.has(e.name) && !WATCHED_EXT.test(e.name)) continue;
|
|
3453
4600
|
const abs = path.join(dir, e.name);
|
|
3454
4601
|
let mtimeMs;
|
|
3455
4602
|
try {
|
|
3456
|
-
mtimeMs =
|
|
4603
|
+
mtimeMs = fs8.statSync(abs).mtimeMs;
|
|
3457
4604
|
} catch {
|
|
3458
4605
|
continue;
|
|
3459
4606
|
}
|
|
@@ -3520,7 +4667,7 @@ function extractContracts(content, pkg, ecosystem) {
|
|
|
3520
4667
|
}
|
|
3521
4668
|
|
|
3522
4669
|
// src/reporting/planning/usage.ts
|
|
3523
|
-
var
|
|
4670
|
+
var SKIP_DIRS3 = /* @__PURE__ */ new Set([
|
|
3524
4671
|
"node_modules",
|
|
3525
4672
|
".git",
|
|
3526
4673
|
"dist",
|
|
@@ -3587,7 +4734,7 @@ function analyzeTree(root, packages) {
|
|
|
3587
4734
|
if (depth > MAX_DEPTH || filesSeen >= MAX_FILES) return;
|
|
3588
4735
|
let entries;
|
|
3589
4736
|
try {
|
|
3590
|
-
entries =
|
|
4737
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
3591
4738
|
} catch {
|
|
3592
4739
|
return;
|
|
3593
4740
|
}
|
|
@@ -3595,7 +4742,7 @@ function analyzeTree(root, packages) {
|
|
|
3595
4742
|
for (const entry of entries) {
|
|
3596
4743
|
if (filesSeen >= MAX_FILES) return;
|
|
3597
4744
|
if (entry.isDirectory()) {
|
|
3598
|
-
if (
|
|
4745
|
+
if (SKIP_DIRS3.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
3599
4746
|
walk2(path.join(dir, entry.name), depth + 1);
|
|
3600
4747
|
continue;
|
|
3601
4748
|
}
|
|
@@ -3605,8 +4752,8 @@ function analyzeTree(root, packages) {
|
|
|
3605
4752
|
const full = path.join(dir, entry.name);
|
|
3606
4753
|
let content;
|
|
3607
4754
|
try {
|
|
3608
|
-
if (
|
|
3609
|
-
content =
|
|
4755
|
+
if (fs8.statSync(full).size > MAX_FILE_BYTES2) continue;
|
|
4756
|
+
content = fs8.readFileSync(full, "utf8");
|
|
3610
4757
|
} catch {
|
|
3611
4758
|
continue;
|
|
3612
4759
|
}
|
|
@@ -3702,9 +4849,6 @@ function planBlock(plan, recommended) {
|
|
|
3702
4849
|
if (plan.upgrades.length > shown.length) {
|
|
3703
4850
|
lines.push(chalk8.dim(` \u2026 and ${plan.upgrades.length - shown.length} more`));
|
|
3704
4851
|
}
|
|
3705
|
-
if (plan.upgrades.length === 0) {
|
|
3706
|
-
lines.push(chalk8.dim(" (no upgrades in this plan)"));
|
|
3707
|
-
}
|
|
3708
4852
|
return lines.join("\n");
|
|
3709
4853
|
}
|
|
3710
4854
|
function dataNote(report) {
|
|
@@ -3731,9 +4875,15 @@ function renderText(report) {
|
|
|
3731
4875
|
out2.push(chalk8.red(`\u26A0 ${report.exploitability.kevPackages} package(s) carry a KNOWN-EXPLOITED (KEV) advisory${epss} \u2014 prioritise these.`));
|
|
3732
4876
|
}
|
|
3733
4877
|
out2.push("");
|
|
3734
|
-
|
|
3735
|
-
|
|
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."));
|
|
3736
4881
|
out2.push("");
|
|
4882
|
+
} else {
|
|
4883
|
+
for (const plan of visiblePlans) {
|
|
4884
|
+
out2.push(planBlock(plan, plan.tier === report.recommended));
|
|
4885
|
+
out2.push("");
|
|
4886
|
+
}
|
|
3737
4887
|
}
|
|
3738
4888
|
out2.push(chalk8.bold("Recommendation"));
|
|
3739
4889
|
out2.push(` ${chalk8.green(report.plans.find((p) => p.tier === report.recommended)?.label ?? report.recommended)} \u2014 ${report.rationale}`);
|
|
@@ -3756,7 +4906,12 @@ function renderMarkdown(report) {
|
|
|
3756
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."}_`);
|
|
3757
4907
|
}
|
|
3758
4908
|
out2.push("");
|
|
3759
|
-
|
|
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) {
|
|
3760
4915
|
const rec = plan.tier === report.recommended ? " \u2705 **recommended**" : "";
|
|
3761
4916
|
out2.push(`## ${plan.label}${rec}`);
|
|
3762
4917
|
out2.push("");
|
|
@@ -3769,14 +4924,12 @@ function renderMarkdown(report) {
|
|
|
3769
4924
|
out2.push(`- \u26A0 Introduces advisories in target versions: ${deltaSummary(plan.introduces)}`);
|
|
3770
4925
|
}
|
|
3771
4926
|
out2.push("");
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
out2.push(`| \`${u.package}\` | ${u.from ?? "?"} | ${u.to ?? "?"} | ${u.kind} | ${u.reason} |`);
|
|
3777
|
-
}
|
|
3778
|
-
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} |`);
|
|
3779
4931
|
}
|
|
4932
|
+
out2.push("");
|
|
3780
4933
|
}
|
|
3781
4934
|
out2.push("## Recommendation");
|
|
3782
4935
|
out2.push("");
|
|
@@ -3789,6 +4942,29 @@ function renderMarkdown(report) {
|
|
|
3789
4942
|
return out2.join("\n");
|
|
3790
4943
|
}
|
|
3791
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
|
+
|
|
3792
4968
|
// src/reporting/planning/expected-drift.ts
|
|
3793
4969
|
function estimateDriftScore(artifact, upgraded) {
|
|
3794
4970
|
const projects = JSON.parse(JSON.stringify(artifact.projects ?? []));
|
|
@@ -3797,6 +4973,11 @@ function estimateDriftScore(artifact, upgraded) {
|
|
|
3797
4973
|
for (const dep of p.dependencies ?? []) {
|
|
3798
4974
|
if (!upgraded.has(dep.package)) continue;
|
|
3799
4975
|
const mb3 = dep.majorsBehind ?? 0;
|
|
4976
|
+
dep.majorsBehind = 0;
|
|
4977
|
+
dep.drift = "current";
|
|
4978
|
+
if (dep.libyears != null) dep.libyears = 0;
|
|
4979
|
+
if (dep.ageDays != null) dep.ageDays = 0;
|
|
4980
|
+
if (dep.latestStable) dep.resolvedVersion = dep.latestStable;
|
|
3800
4981
|
if (mb3 >= 2 && buckets.twoPlusBehind > 0) {
|
|
3801
4982
|
buckets.twoPlusBehind--;
|
|
3802
4983
|
buckets.current++;
|
|
@@ -3940,22 +5121,56 @@ async function fetchLatestVersion() {
|
|
|
3940
5121
|
}
|
|
3941
5122
|
async function writeCache(data) {
|
|
3942
5123
|
try {
|
|
3943
|
-
await
|
|
3944
|
-
await
|
|
5124
|
+
await fs14.mkdir(CACHE_DIR, { recursive: true });
|
|
5125
|
+
await fs14.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
3945
5126
|
} catch {
|
|
3946
5127
|
}
|
|
3947
5128
|
}
|
|
3948
5129
|
|
|
3949
5130
|
// src/reporting/commands/update.ts
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
if (execPath
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
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;
|
|
3958
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;
|
|
3959
5174
|
}
|
|
3960
5175
|
return null;
|
|
3961
5176
|
}
|
|
@@ -4005,24 +5220,25 @@ function getInstallCommand(pm, pkg, version, isDev, opts = {}) {
|
|
|
4005
5220
|
return isDev ? `npm install --save-dev ${spec}` : `npm install ${spec}`;
|
|
4006
5221
|
}
|
|
4007
5222
|
}
|
|
4008
|
-
async function
|
|
5223
|
+
async function getLocalDependencyState(cwd, pkg = PACKAGE_NAME) {
|
|
4009
5224
|
try {
|
|
4010
5225
|
const pkgPath = path.join(cwd, "package.json");
|
|
4011
5226
|
const raw = await (await import('fs/promises')).readFile(pkgPath, "utf-8");
|
|
4012
|
-
const
|
|
4013
|
-
|
|
5227
|
+
const parsed = JSON.parse(raw);
|
|
5228
|
+
if (parsed.devDependencies?.[pkg]) return "dev";
|
|
5229
|
+
if (parsed.dependencies?.[pkg]) return "prod";
|
|
4014
5230
|
} catch {
|
|
4015
|
-
return true;
|
|
4016
5231
|
}
|
|
5232
|
+
return null;
|
|
4017
5233
|
}
|
|
4018
5234
|
async function confirmWorkspaceRoot(pm) {
|
|
4019
5235
|
if (!(process.stdin.isTTY && process.stdout.isTTY)) return false;
|
|
4020
5236
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4021
|
-
return new Promise((
|
|
5237
|
+
return new Promise((resolve17) => {
|
|
4022
5238
|
rl.question(chalk8.yellow(`Update the ${pm} workspace root with -w? [y/N]: `), (ans) => {
|
|
4023
5239
|
rl.close();
|
|
4024
5240
|
const t = ans.trim().toLowerCase();
|
|
4025
|
-
|
|
5241
|
+
resolve17(t === "y" || t === "yes");
|
|
4026
5242
|
});
|
|
4027
5243
|
});
|
|
4028
5244
|
}
|
|
@@ -4046,15 +5262,29 @@ var updateCommand = new Command("update").description("Update vibgrate to the la
|
|
|
4046
5262
|
return;
|
|
4047
5263
|
}
|
|
4048
5264
|
const cwd = process.cwd();
|
|
4049
|
-
const
|
|
4050
|
-
|
|
4051
|
-
|
|
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);
|
|
4052
5282
|
let cmd;
|
|
4053
5283
|
if (isGlobal) {
|
|
4054
|
-
cmd = getGlobalUpdateCommand(pm,
|
|
5284
|
+
cmd = getGlobalUpdateCommand(pm, PACKAGE_NAME, latest);
|
|
4055
5285
|
console.log(chalk8.dim(`Updating global installation with ${pm}: ${cmd}`));
|
|
4056
5286
|
} else {
|
|
4057
|
-
const isDev =
|
|
5287
|
+
const isDev = localState !== "prod";
|
|
4058
5288
|
let workspaceRoot = false;
|
|
4059
5289
|
if (pm === "pnpm" && await detectWorkspaceRoot(cwd)) {
|
|
4060
5290
|
console.log(
|
|
@@ -4062,19 +5292,19 @@ var updateCommand = new Command("update").description("Update vibgrate to the la
|
|
|
4062
5292
|
);
|
|
4063
5293
|
const proceed = opts.yes || opts.workspaceRoot || await confirmWorkspaceRoot(pm);
|
|
4064
5294
|
if (!proceed) {
|
|
4065
|
-
const rootCmd = getInstallCommand(pm,
|
|
5295
|
+
const rootCmd = getInstallCommand(pm, PACKAGE_NAME, latest, isDev, { workspaceRoot: true });
|
|
4066
5296
|
console.log(chalk8.dim(`Skipped. To update the workspace root, run: ${rootCmd}`));
|
|
4067
5297
|
console.log(chalk8.dim('or re-run "vg update --yes" (or --workspace-root) to let vg do it.'));
|
|
4068
5298
|
return;
|
|
4069
5299
|
}
|
|
4070
5300
|
workspaceRoot = true;
|
|
4071
5301
|
}
|
|
4072
|
-
cmd = getInstallCommand(pm,
|
|
5302
|
+
cmd = getInstallCommand(pm, PACKAGE_NAME, latest, isDev, { workspaceRoot });
|
|
4073
5303
|
console.log(chalk8.dim(`Using ${pm}: ${cmd}`));
|
|
4074
5304
|
}
|
|
4075
5305
|
try {
|
|
4076
5306
|
execSync(cmd, { cwd, stdio: "inherit" });
|
|
4077
|
-
console.log(chalk8.green("\u2714") + ` Updated to
|
|
5307
|
+
console.log(chalk8.green("\u2714") + ` Updated to ${PACKAGE_NAME}@${latest}`);
|
|
4078
5308
|
} catch {
|
|
4079
5309
|
console.error(chalk8.red(`Update failed. Run manually: ${cmd}`));
|
|
4080
5310
|
process.exit(1);
|
|
@@ -4098,11 +5328,11 @@ function collectCandidates(artifact) {
|
|
|
4098
5328
|
for (const project of artifact.projects ?? []) {
|
|
4099
5329
|
const ecosystem = ecosystemId(project.type);
|
|
4100
5330
|
for (const dep of project.dependencies ?? []) {
|
|
4101
|
-
const
|
|
4102
|
-
if (seen.has(
|
|
5331
|
+
const key2 = `${ecosystem}\0${dep.package}`;
|
|
5332
|
+
if (seen.has(key2)) continue;
|
|
4103
5333
|
if (!dep.latestStable || dep.latestStable === dep.resolvedVersion) continue;
|
|
4104
5334
|
if (dep.drift === "current" || dep.drift === "unknown") continue;
|
|
4105
|
-
seen.add(
|
|
5335
|
+
seen.add(key2);
|
|
4106
5336
|
out2.push({
|
|
4107
5337
|
package: dep.package,
|
|
4108
5338
|
ecosystem,
|
|
@@ -4150,7 +5380,7 @@ async function repositoryIdentity(rootDir, override) {
|
|
|
4150
5380
|
return { name: override?.trim() || void 0 };
|
|
4151
5381
|
}
|
|
4152
5382
|
}
|
|
4153
|
-
var fixCommand = new Command("fix").description("
|
|
5383
|
+
var fixCommand = new Command("fix").description("Get ranked, risk-tiered upgrade plans from the hosted planner and optionally apply them (prompts before changing files; --yes applies non-interactively, --dry-run previews, --no-apply only prints). Requires login/DSN.").argument("[path]", "Path to analyse", ".").option("--in <file>", "Scan artifact to read", ".vibgrate/scan_result.json").option("--format <format>", "Output format (text|json|md)", "text").option("--dsn <dsn>", 'DSN token (or use VIBGRATE_DSN env / "vg login")').option("--region <region>", "Override data residency region (us, eu)").option("--repository-name <name>", "Override the repository name recorded for this plan").option("--plan <tier>", "Apply a specific plan non-interactively (safe|balanced|aggressive)").option("--yes", "Apply the recommended plan without prompting").option("--dry-run", "Show what would change without applying").option("--no-apply", "Only print the plans; never modify the project").option("--fail-on-vulns <severity>", "Exit non-zero if the recommended plan leaves an advisory at/above this severity unresolved (low|moderate|high|critical)").action(async (targetPath, opts) => {
|
|
4154
5384
|
const rootDir = path.resolve(targetPath);
|
|
4155
5385
|
if (!await pathExists(rootDir)) {
|
|
4156
5386
|
console.error(chalk8.red(`Path does not exist: ${rootDir}`));
|
|
@@ -4240,6 +5470,9 @@ var fixCommand = new Command("fix").description("Analyse drift and get ranked, r
|
|
|
4240
5470
|
if (response.requestId) console.error(chalk8.dim(` (ref ${response.requestId})`));
|
|
4241
5471
|
process.exit(1);
|
|
4242
5472
|
}
|
|
5473
|
+
const { plans: dedupedPlans, canonicalTier } = dedupePlans(response.plans);
|
|
5474
|
+
response.plans = dedupedPlans;
|
|
5475
|
+
response.recommended = canonicalTier.get(response.recommended) ?? response.recommended;
|
|
4243
5476
|
const currentDrift = artifact.drift?.score;
|
|
4244
5477
|
if (typeof currentDrift === "number") {
|
|
4245
5478
|
response.currentDriftScore = currentDrift;
|
|
@@ -4252,7 +5485,7 @@ var fixCommand = new Command("fix").description("Analyse drift and get ranked, r
|
|
|
4252
5485
|
}
|
|
4253
5486
|
emit(response, opts.format);
|
|
4254
5487
|
if (opts.format === "text" && opts.apply !== false) {
|
|
4255
|
-
await runApplyFlow(rootDir, artifact, response, opts);
|
|
5488
|
+
await runApplyFlow(rootDir, artifact, response, opts, canonicalTier);
|
|
4256
5489
|
}
|
|
4257
5490
|
if (failOn) {
|
|
4258
5491
|
const recommended = response.plans.find((p) => p.tier === response.recommended);
|
|
@@ -4261,7 +5494,8 @@ var fixCommand = new Command("fix").description("Analyse drift and get ranked, r
|
|
|
4261
5494
|
for (const sev of Object.keys(SEVERITY_RANK)) {
|
|
4262
5495
|
if (SEVERITY_RANK[sev] < threshold) continue;
|
|
4263
5496
|
const open = response.unresolved.bySeverity[sev] ?? 0;
|
|
4264
|
-
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);
|
|
4265
5499
|
stillOpen += open + Math.max(0, notFixedByRecommended);
|
|
4266
5500
|
}
|
|
4267
5501
|
if (stillOpen > 0) {
|
|
@@ -4318,8 +5552,8 @@ async function buildTargetResolver(rootDir, artifact) {
|
|
|
4318
5552
|
const ecosystem = ecosystemId(project.type);
|
|
4319
5553
|
const dir = relProjectDir(rootDir, project.path);
|
|
4320
5554
|
for (const dep of project.dependencies ?? []) {
|
|
4321
|
-
const
|
|
4322
|
-
(owners.get(
|
|
5555
|
+
const key2 = `${ecosystem}\0${dep.package}`;
|
|
5556
|
+
(owners.get(key2) ?? owners.set(key2, /* @__PURE__ */ new Set()).get(key2)).add(dir);
|
|
4323
5557
|
}
|
|
4324
5558
|
}
|
|
4325
5559
|
const rootFallback = [{ dir: ".", isWorkspaceRoot: rootIsWorkspace }];
|
|
@@ -4349,18 +5583,18 @@ function promptPlanSelection(response) {
|
|
|
4349
5583
|
const defaultIdx = plans.findIndex((p) => p.tier === response.recommended);
|
|
4350
5584
|
const def = defaultIdx >= 0 ? defaultIdx + 1 : 1;
|
|
4351
5585
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4352
|
-
return new Promise((
|
|
5586
|
+
return new Promise((resolve17) => {
|
|
4353
5587
|
rl.question(`Plan [1-${plans.length}, default ${def}, q to cancel]: `, (ans) => {
|
|
4354
5588
|
rl.close();
|
|
4355
5589
|
const t = ans.trim().toLowerCase();
|
|
4356
|
-
if (t === "q" || t === "quit") return
|
|
5590
|
+
if (t === "q" || t === "quit") return resolve17(null);
|
|
4357
5591
|
const n = t === "" ? def : Number.parseInt(t, 10);
|
|
4358
|
-
if (!Number.isInteger(n) || n < 1 || n > plans.length) return
|
|
4359
|
-
|
|
5592
|
+
if (!Number.isInteger(n) || n < 1 || n > plans.length) return resolve17(null);
|
|
5593
|
+
resolve17(plans[n - 1].tier);
|
|
4360
5594
|
});
|
|
4361
5595
|
});
|
|
4362
5596
|
}
|
|
4363
|
-
async function runApplyFlow(rootDir, artifact, response, opts) {
|
|
5597
|
+
async function runApplyFlow(rootDir, artifact, response, opts, canonicalTier) {
|
|
4364
5598
|
const nonEmpty = response.plans.filter((p) => p.upgrades.length > 0);
|
|
4365
5599
|
if (nonEmpty.length === 0) {
|
|
4366
5600
|
console.log(chalk8.green("\n\u2714 Nothing to upgrade \u2014 every tracked dependency is current."));
|
|
@@ -4368,7 +5602,8 @@ async function runApplyFlow(rootDir, artifact, response, opts) {
|
|
|
4368
5602
|
}
|
|
4369
5603
|
let chosen;
|
|
4370
5604
|
if (opts.plan) {
|
|
4371
|
-
|
|
5605
|
+
const tier = canonicalTier?.get(opts.plan) ?? opts.plan;
|
|
5606
|
+
chosen = response.plans.find((p) => p.tier === tier);
|
|
4372
5607
|
if (!chosen) {
|
|
4373
5608
|
console.error(chalk8.red(`Unknown plan '${opts.plan}'. Use safe, balanced, or aggressive.`));
|
|
4374
5609
|
process.exit(1);
|
|
@@ -4727,6 +5962,26 @@ function formatExtended(ext) {
|
|
|
4727
5962
|
}
|
|
4728
5963
|
lines.push("");
|
|
4729
5964
|
}
|
|
5965
|
+
if (ext.databaseSchema) {
|
|
5966
|
+
const ds = ext.databaseSchema;
|
|
5967
|
+
lines.push(chalk8.bold.underline(" Database Schema"));
|
|
5968
|
+
const dsHead = [];
|
|
5969
|
+
if (ds.providers.length > 0) dsHead.push(ds.providers.join(", "));
|
|
5970
|
+
dsHead.push(`${ds.models.length} model${ds.models.length !== 1 ? "s" : ""}`);
|
|
5971
|
+
dsHead.push(`${ds.enums.length} enum${ds.enums.length !== 1 ? "s" : ""}`);
|
|
5972
|
+
lines.push(` ${dsHead.join(" \xB7 ")}`);
|
|
5973
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
5974
|
+
for (const m of ds.models) bySource.set(m.source, (bySource.get(m.source) ?? 0) + 1);
|
|
5975
|
+
if (bySource.size > 1) {
|
|
5976
|
+
const sourceSummary = [...bySource.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([s, n]) => `${s}: ${n}`).join(", ");
|
|
5977
|
+
lines.push(` Sources: ${sourceSummary}`);
|
|
5978
|
+
}
|
|
5979
|
+
if (ds.models.length > 0) {
|
|
5980
|
+
const preview = ds.models.slice(0, 5).map((m) => m.name).join(", ");
|
|
5981
|
+
lines.push(` Models: ${chalk8.white(preview)}${ds.models.length > 5 ? chalk8.dim(` (+${ds.models.length - 5} more)`) : ""}`);
|
|
5982
|
+
}
|
|
5983
|
+
lines.push("");
|
|
5984
|
+
}
|
|
4730
5985
|
if (ext.dependencyGraph) {
|
|
4731
5986
|
const dg = ext.dependencyGraph;
|
|
4732
5987
|
if (dg.lockfileType) {
|
|
@@ -5168,7 +6423,7 @@ function openUrl(url) {
|
|
|
5168
6423
|
}
|
|
5169
6424
|
|
|
5170
6425
|
// src/reporting/commands/login.ts
|
|
5171
|
-
var delay = (ms) => new Promise((
|
|
6426
|
+
var delay = (ms) => new Promise((resolve17) => setTimeout(resolve17, ms));
|
|
5172
6427
|
function isInside(dir, file) {
|
|
5173
6428
|
const rel3 = path.relative(dir, file);
|
|
5174
6429
|
return !!rel3 && !rel3.startsWith("..") && !path.isAbsolute(rel3);
|
|
@@ -5593,21 +6848,21 @@ function formatDeltaText(base, current) {
|
|
|
5593
6848
|
const added = [];
|
|
5594
6849
|
const removed = [];
|
|
5595
6850
|
const changed = [];
|
|
5596
|
-
for (const [
|
|
5597
|
-
if (!baseMap.has(
|
|
5598
|
-
added.push(`${
|
|
6851
|
+
for (const [key2, dep] of currentMap.entries()) {
|
|
6852
|
+
if (!baseMap.has(key2)) {
|
|
6853
|
+
added.push(`${key2} @ ${dep.resolvedVersion ?? dep.currentSpec}`);
|
|
5599
6854
|
continue;
|
|
5600
6855
|
}
|
|
5601
|
-
const prev = baseMap.get(
|
|
6856
|
+
const prev = baseMap.get(key2);
|
|
5602
6857
|
const prevVersion = prev.resolvedVersion ?? prev.currentSpec;
|
|
5603
6858
|
const nowVersion = dep.resolvedVersion ?? dep.currentSpec;
|
|
5604
6859
|
if (prevVersion !== nowVersion || prev.majorsBehind !== dep.majorsBehind) {
|
|
5605
|
-
changed.push(`${
|
|
6860
|
+
changed.push(`${key2} ${prevVersion} -> ${nowVersion} (majorsBehind ${prev.majorsBehind ?? "unknown"} -> ${dep.majorsBehind ?? "unknown"})`);
|
|
5606
6861
|
}
|
|
5607
6862
|
}
|
|
5608
|
-
for (const [
|
|
5609
|
-
if (!currentMap.has(
|
|
5610
|
-
removed.push(`${
|
|
6863
|
+
for (const [key2, dep] of baseMap.entries()) {
|
|
6864
|
+
if (!currentMap.has(key2)) {
|
|
6865
|
+
removed.push(`${key2} @ ${dep.resolvedVersion ?? dep.currentSpec}`);
|
|
5611
6866
|
}
|
|
5612
6867
|
}
|
|
5613
6868
|
const lines = [
|
|
@@ -5615,7 +6870,7 @@ function formatDeltaText(base, current) {
|
|
|
5615
6870
|
"===================",
|
|
5616
6871
|
`Baseline: ${base.timestamp}`,
|
|
5617
6872
|
`Current: ${current.timestamp}`,
|
|
5618
|
-
`
|
|
6873
|
+
`DriftScore delta: ${(current.drift.score - base.drift.score).toFixed(2)} points`,
|
|
5619
6874
|
"",
|
|
5620
6875
|
`Added dependencies (${added.length})`,
|
|
5621
6876
|
...added.map((d) => ` + ${d}`),
|
|
@@ -5693,6 +6948,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
5693
6948
|
"areas",
|
|
5694
6949
|
"oddities",
|
|
5695
6950
|
"serve",
|
|
6951
|
+
"lsp",
|
|
5696
6952
|
"install",
|
|
5697
6953
|
"uninstall",
|
|
5698
6954
|
"share",
|
|
@@ -5733,6 +6989,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
5733
6989
|
registerImpact(program);
|
|
5734
6990
|
registerUnknowns(program);
|
|
5735
6991
|
registerServe(program);
|
|
6992
|
+
registerLsp(program);
|
|
5736
6993
|
registerInstall(program);
|
|
5737
6994
|
registerShare(program);
|
|
5738
6995
|
registerBenchmark(program);
|
|
@@ -5814,7 +7071,7 @@ function dispatch(argv, cwd) {
|
|
|
5814
7071
|
}
|
|
5815
7072
|
const looksLikeQuestion = /\s/.test(first) || first.endsWith("?");
|
|
5816
7073
|
if (looksLikeQuestion) return ["ask", ...args];
|
|
5817
|
-
if (
|
|
7074
|
+
if (fs8.existsSync(path.resolve(cwd, first))) {
|
|
5818
7075
|
return [hasBuildOnlyFlag(args) ? "build" : "scan", ...args];
|
|
5819
7076
|
}
|
|
5820
7077
|
const moved = MOVED_COMMANDS[first];
|