@vibgrate/cli 2026.715.1 → 2026.718.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +1 -1
- package/README.md +26 -0
- package/dist/baseline-DKPQWOQ6.js +6 -0
- package/dist/{baseline-BTLZWSNS.js.map → baseline-DKPQWOQ6.js.map} +1 -1
- package/dist/{chunk-6DKWB2OI.js → chunk-BFXDF6ZE.js} +3 -3
- package/dist/{chunk-6DKWB2OI.js.map → chunk-BFXDF6ZE.js.map} +1 -1
- package/dist/{chunk-D4BPWARI.js → chunk-H5M7CXE6.js} +108 -35
- package/dist/chunk-H5M7CXE6.js.map +1 -0
- package/dist/{chunk-C5AVF7PT.js → chunk-RME7BFMU.js} +44 -12
- package/dist/chunk-RME7BFMU.js.map +1 -0
- package/dist/cli.js +1005 -176
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/dist/baseline-BTLZWSNS.js +0 -6
- package/dist/chunk-C5AVF7PT.js.map +0 -1
- package/dist/chunk-D4BPWARI.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, ingestHostForRegionId, dsnCommand, pushCommand, CliError, ExitCode, NPX_INVOCATION, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, recordCliCall, CLI_TOOL_ALIASES, countPending, resolveOne, indexFor, countTokens, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, usageError, loadGraph, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, readUsage, exportGraph, catalogPath, libDir, stableStringify, buildGraph, writeArtifacts, writeSnapshot, UsageError, ResourceLimitError, epistemicBreakdown, cacheDir, probeFreshness, discover, vibgrateDir, modelCacheInfo, clearModelCache, GraphSource, createServer, assistantById, serializeGraph, resolveLimits, loadCatalog, driftFor, addLibrary, parseDsn as parseDsn$1, publishPrivateLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface, assessDocQuality, symbolsFromApi, fetchHostedDocsCached, selectForBudget, formatForExt, verifyDeterminism, renderHtml, isModelReady } from './chunk-
|
|
3
|
-
import { resolvedGrammarFiles, grammarSetVersion, canonicalize } from './chunk-VFO5UDAT.js';
|
|
4
|
-
import { loadAdvancedScanHook, baselineCommand } from './chunk-
|
|
5
|
-
import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, loadConfig, prepareCompressedUpload, computeDriftScore, titleBox, projectTypeToVulnEcosystem, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, PROJECT_TYPE_TO_OSV_ECOSYSTEM, fetchRiskySymbols, brandProgressBar, resolveHead, workingTreeDirty } from './chunk-
|
|
2
|
+
import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, ingestHostForRegionId, dsnCommand, pushCommand, CliError, ExitCode, NPX_INVOCATION, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, recordCliCall, CLI_TOOL_ALIASES, countPending, resolveOne, indexFor, countTokens, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, detectAssistants, usageError, loadGraph, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, readUsage, exportGraph, catalogPath, libDir, stableStringify, buildGraph, writeArtifacts, writeSnapshot, UsageError, ResourceLimitError, epistemicBreakdown, cacheDir, probeFreshness, discover, vibgrateDir, modelCacheInfo, clearModelCache, GraphSource, createServer, assistantById, serializeGraph, resolveLimits, loadCatalog, driftFor, addLibrary, parseDsn as parseDsn$1, publishPrivateLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface, assessDocQuality, symbolsFromApi, fetchHostedDocsCached, selectForBudget, formatForExt, hostedBase, verifyDeterminism, renderHtml, isModelReady, readStoredCredentials, projectCredentialsPath, homeCredentialsPath, SKIP_DIRS, SKIP_FILES } from './chunk-H5M7CXE6.js';
|
|
3
|
+
import { resolvedGrammarFiles, grammarSetVersion, hashString, canonicalize, hashBytes } from './chunk-VFO5UDAT.js';
|
|
4
|
+
import { loadAdvancedScanHook, baselineCommand } from './chunk-BFXDF6ZE.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-RME7BFMU.js';
|
|
6
6
|
import './chunk-RXP66R2E.js';
|
|
7
7
|
import { pathExists as pathExists$1, parseExcludePatterns } from './chunk-GI6W53LM.js';
|
|
8
|
-
import * as
|
|
9
|
-
import
|
|
8
|
+
import * as fs8 from 'fs';
|
|
9
|
+
import fs8__default, { realpathSync } from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import path__default from 'path';
|
|
12
12
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
@@ -16,7 +16,7 @@ import * as crypto from 'crypto';
|
|
|
16
16
|
import { randomUUID } from 'crypto';
|
|
17
17
|
import chalk8 from 'chalk';
|
|
18
18
|
import * as os from 'os';
|
|
19
|
-
import * as
|
|
19
|
+
import * as fs15 from 'fs/promises';
|
|
20
20
|
import * as readline from 'readline';
|
|
21
21
|
import semver from 'semver';
|
|
22
22
|
|
|
@@ -178,14 +178,14 @@ async function signGraphAttestation(root, graph, opts) {
|
|
|
178
178
|
const explicitKey = opts.key ?? process.env.VG_ATTEST_KEY;
|
|
179
179
|
const keyPath = explicitKey ? path.resolve(explicitKey) : path.join(root, ".vibgrate", DEFAULT_KEY);
|
|
180
180
|
let keyGeneratedAt;
|
|
181
|
-
if (!
|
|
181
|
+
if (!fs8.existsSync(keyPath)) {
|
|
182
182
|
if (explicitKey) {
|
|
183
183
|
throw new CliError(`signing key not found: ${explicitKey}`, ExitCode.USAGE_ERROR);
|
|
184
184
|
}
|
|
185
185
|
const kp = generateKeypair();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
fs8.mkdirSync(path.dirname(keyPath), { recursive: true });
|
|
187
|
+
fs8.writeFileSync(keyPath, kp.privatePem, { mode: 384 });
|
|
188
|
+
fs8.writeFileSync(`${keyPath}.pub`, kp.publicPem);
|
|
189
189
|
keyGeneratedAt = keyPath;
|
|
190
190
|
notices.push(
|
|
191
191
|
`minted a new Ed25519 signing key at ${rel(root, keyPath)} (keyid ${kp.keyid}) \u2014 keep it, add it to .gitignore, and reuse it to re-sign reproducibly`
|
|
@@ -193,7 +193,7 @@ async function signGraphAttestation(root, graph, opts) {
|
|
|
193
193
|
}
|
|
194
194
|
let privateKey;
|
|
195
195
|
try {
|
|
196
|
-
privateKey = crypto.createPrivateKey(
|
|
196
|
+
privateKey = crypto.createPrivateKey(fs8.readFileSync(keyPath, "utf8"));
|
|
197
197
|
} catch {
|
|
198
198
|
throw new CliError(`could not read an Ed25519 private key from ${rel(root, keyPath)}`, ExitCode.USAGE_ERROR);
|
|
199
199
|
}
|
|
@@ -207,8 +207,8 @@ async function signGraphAttestation(root, graph, opts) {
|
|
|
207
207
|
const statement = buildStatement(graph, { commit });
|
|
208
208
|
const envelope = signStatement(statement, privateKey);
|
|
209
209
|
const outPath = opts.attestation ? path.resolve(opts.attestation) : path.join(root, ".vibgrate", DEFAULT_ATTESTATION);
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
fs8.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
211
|
+
fs8.writeFileSync(outPath, serializeEnvelope(envelope));
|
|
212
212
|
return {
|
|
213
213
|
summary: {
|
|
214
214
|
keyid: envelope.signatures[0]?.keyid ?? "",
|
|
@@ -223,7 +223,7 @@ async function signGraphAttestation(root, graph, opts) {
|
|
|
223
223
|
}
|
|
224
224
|
function verifyGraphAttestation(root, opts) {
|
|
225
225
|
const bundlePath = opts.attestation ? path.resolve(opts.attestation) : path.join(root, ".vibgrate", DEFAULT_ATTESTATION);
|
|
226
|
-
if (!
|
|
226
|
+
if (!fs8.existsSync(bundlePath)) {
|
|
227
227
|
if (opts.attestation || opts.pub) {
|
|
228
228
|
throw new CliError(
|
|
229
229
|
`no attestation at ${rel(root, bundlePath)} \u2014 sign one with \`vg build --attest\``,
|
|
@@ -232,8 +232,8 @@ function verifyGraphAttestation(root, opts) {
|
|
|
232
232
|
}
|
|
233
233
|
return { bundlePath, missing: true };
|
|
234
234
|
}
|
|
235
|
-
const envelope = parseEnvelope(
|
|
236
|
-
const publicKeyPem = opts.pub ?
|
|
235
|
+
const envelope = parseEnvelope(fs8.readFileSync(bundlePath, "utf8"));
|
|
236
|
+
const publicKeyPem = opts.pub ? fs8.readFileSync(path.resolve(opts.pub), "utf8") : void 0;
|
|
237
237
|
const graph = loadGraph(root) ?? void 0;
|
|
238
238
|
return { bundlePath, missing: false, result: verifyEnvelope(envelope, { publicKeyPem, graph }) };
|
|
239
239
|
}
|
|
@@ -584,8 +584,8 @@ function writeExport(graph, target) {
|
|
|
584
584
|
ExitCode.USAGE_ERROR
|
|
585
585
|
);
|
|
586
586
|
}
|
|
587
|
-
|
|
588
|
-
|
|
587
|
+
fs8.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
|
|
588
|
+
fs8.writeFileSync(target, content);
|
|
589
589
|
}
|
|
590
590
|
function collect(value, prev) {
|
|
591
591
|
return [...prev, value];
|
|
@@ -604,7 +604,7 @@ async function runStatus(global) {
|
|
|
604
604
|
const root = path.resolve(global.cwd ?? ".");
|
|
605
605
|
const graphPath = global.graph ?? defaultGraphPath(root);
|
|
606
606
|
const graph = loadGraph(root, graphPath);
|
|
607
|
-
const hasCache =
|
|
607
|
+
const hasCache = fs8.existsSync(path.join(cacheDir(root), "parse-cache.json"));
|
|
608
608
|
let stale = null;
|
|
609
609
|
let staleExact = false;
|
|
610
610
|
if (graph) {
|
|
@@ -1287,16 +1287,40 @@ var OFFSET_FILE = "stats-share.json";
|
|
|
1287
1287
|
var INSTALL_ID_DIR = path.join(os.homedir(), ".vibgrate");
|
|
1288
1288
|
var INSTALL_ID_FILE = path.join(INSTALL_ID_DIR, "install-id");
|
|
1289
1289
|
var SEND_TIMEOUT_MS = 4e3;
|
|
1290
|
-
function
|
|
1290
|
+
function telemetryOptOut(env = process.env) {
|
|
1291
|
+
const dnt = (env.DO_NOT_TRACK ?? "").trim().toLowerCase();
|
|
1292
|
+
if (dnt !== "" && dnt !== "0" && dnt !== "false") return "DO_NOT_TRACK";
|
|
1293
|
+
const vt = (env.VIBGRATE_TELEMETRY ?? "").trim().toLowerCase();
|
|
1294
|
+
if (vt === "0" || vt === "false" || vt === "off") return "VIBGRATE_TELEMETRY";
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
var CI_ENV_VARS = [
|
|
1298
|
+
"CI",
|
|
1299
|
+
"GITHUB_ACTIONS",
|
|
1300
|
+
"GITLAB_CI",
|
|
1301
|
+
"CIRCLECI",
|
|
1302
|
+
"TRAVIS",
|
|
1303
|
+
"BUILDKITE",
|
|
1304
|
+
"JENKINS_URL",
|
|
1305
|
+
"TF_BUILD"
|
|
1306
|
+
];
|
|
1307
|
+
function isCI(env = process.env) {
|
|
1308
|
+
return CI_ENV_VARS.some((v) => {
|
|
1309
|
+
const val = (env[v] ?? "").trim().toLowerCase();
|
|
1310
|
+
return val !== "" && val !== "0" && val !== "false";
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
function installId(env = process.env) {
|
|
1314
|
+
if (telemetryOptOut(env) !== null || isCI(env)) return randomUuid();
|
|
1291
1315
|
try {
|
|
1292
|
-
const existing =
|
|
1316
|
+
const existing = fs8.readFileSync(INSTALL_ID_FILE, "utf8").trim();
|
|
1293
1317
|
if (isUuid(existing)) return existing;
|
|
1294
1318
|
} catch {
|
|
1295
1319
|
}
|
|
1296
1320
|
const id = randomUuid();
|
|
1297
1321
|
try {
|
|
1298
|
-
|
|
1299
|
-
|
|
1322
|
+
fs8.mkdirSync(INSTALL_ID_DIR, { recursive: true });
|
|
1323
|
+
fs8.writeFileSync(INSTALL_ID_FILE, id + "\n", { mode: 384 });
|
|
1300
1324
|
} catch {
|
|
1301
1325
|
}
|
|
1302
1326
|
return id;
|
|
@@ -1320,7 +1344,7 @@ function ledgerFilePath(root) {
|
|
|
1320
1344
|
}
|
|
1321
1345
|
function readOffset(root) {
|
|
1322
1346
|
try {
|
|
1323
|
-
const raw = JSON.parse(
|
|
1347
|
+
const raw = JSON.parse(fs8.readFileSync(offsetPath(root), "utf8"));
|
|
1324
1348
|
return typeof raw.offset === "number" && raw.offset >= 0 ? raw.offset : 0;
|
|
1325
1349
|
} catch {
|
|
1326
1350
|
return 0;
|
|
@@ -1328,8 +1352,8 @@ function readOffset(root) {
|
|
|
1328
1352
|
}
|
|
1329
1353
|
function writeOffset(root, offset) {
|
|
1330
1354
|
try {
|
|
1331
|
-
|
|
1332
|
-
|
|
1355
|
+
fs8.mkdirSync(cacheDir(root), { recursive: true });
|
|
1356
|
+
fs8.writeFileSync(offsetPath(root), JSON.stringify({ offset }) + "\n");
|
|
1333
1357
|
} catch {
|
|
1334
1358
|
}
|
|
1335
1359
|
}
|
|
@@ -1337,7 +1361,7 @@ function buildBatch(root, fromOffset, id = installId()) {
|
|
|
1337
1361
|
const file = ledgerFilePath(root);
|
|
1338
1362
|
let size = 0;
|
|
1339
1363
|
try {
|
|
1340
|
-
size =
|
|
1364
|
+
size = fs8.statSync(file).size;
|
|
1341
1365
|
} catch {
|
|
1342
1366
|
return { batch: null, newOffset: 0 };
|
|
1343
1367
|
}
|
|
@@ -1345,13 +1369,13 @@ function buildBatch(root, fromOffset, id = installId()) {
|
|
|
1345
1369
|
if (start >= size) return { batch: null, newOffset: size };
|
|
1346
1370
|
let text = "";
|
|
1347
1371
|
try {
|
|
1348
|
-
const fd =
|
|
1372
|
+
const fd = fs8.openSync(file, "r");
|
|
1349
1373
|
try {
|
|
1350
1374
|
const buf = Buffer.alloc(size - start);
|
|
1351
|
-
|
|
1375
|
+
fs8.readSync(fd, buf, 0, buf.length, start);
|
|
1352
1376
|
text = buf.toString("utf8");
|
|
1353
1377
|
} finally {
|
|
1354
|
-
|
|
1378
|
+
fs8.closeSync(fd);
|
|
1355
1379
|
}
|
|
1356
1380
|
} catch {
|
|
1357
1381
|
return { batch: null, newOffset: fromOffset };
|
|
@@ -1374,8 +1398,8 @@ function buildBatch(root, fromOffset, id = installId()) {
|
|
|
1374
1398
|
const source = e.source ?? "mcp";
|
|
1375
1399
|
const client = e.client ?? "unknown";
|
|
1376
1400
|
const outcome = e.outcome ?? "complete";
|
|
1377
|
-
const
|
|
1378
|
-
const row = rows.get(
|
|
1401
|
+
const key2 = `${source}|${client}|${e.tool}|${outcome}`;
|
|
1402
|
+
const row = rows.get(key2) ?? {
|
|
1379
1403
|
source,
|
|
1380
1404
|
client,
|
|
1381
1405
|
tool: e.tool,
|
|
@@ -1387,7 +1411,7 @@ function buildBatch(root, fromOffset, id = installId()) {
|
|
|
1387
1411
|
row.calls++;
|
|
1388
1412
|
row.vgTokens += e.vgTokens ?? 0;
|
|
1389
1413
|
row.baselineTokens += e.baselineTokens ?? 0;
|
|
1390
|
-
rows.set(
|
|
1414
|
+
rows.set(key2, row);
|
|
1391
1415
|
total++;
|
|
1392
1416
|
if (typeof e.ts === "number") {
|
|
1393
1417
|
if (e.ts < minTs) minTs = e.ts;
|
|
@@ -1463,6 +1487,27 @@ var StatsSharer = class {
|
|
|
1463
1487
|
}
|
|
1464
1488
|
};
|
|
1465
1489
|
|
|
1490
|
+
// src/util/origin.ts
|
|
1491
|
+
function originAllowed(origin, allowlist) {
|
|
1492
|
+
if (origin === void 0 || origin === "") return true;
|
|
1493
|
+
const allowed = (allowlist ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1494
|
+
if (allowed.includes("*")) return true;
|
|
1495
|
+
if (allowed.includes(origin)) return true;
|
|
1496
|
+
return isLoopbackOrigin(origin);
|
|
1497
|
+
}
|
|
1498
|
+
function isLoopbackOrigin(origin) {
|
|
1499
|
+
let url;
|
|
1500
|
+
try {
|
|
1501
|
+
url = new URL(origin);
|
|
1502
|
+
} catch {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
1506
|
+
const host = url.hostname;
|
|
1507
|
+
if (host === "localhost" || host === "[::1]" || host === "::1") return true;
|
|
1508
|
+
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1466
1511
|
// src/commands/serve.ts
|
|
1467
1512
|
var SHARE_FLUSH_INTERVAL_MS = 5 * 60 * 1e3;
|
|
1468
1513
|
function registerServe(program) {
|
|
@@ -1472,9 +1517,14 @@ function registerServe(program) {
|
|
|
1472
1517
|
const graphPath = global.graph ?? defaultGraphPath(root);
|
|
1473
1518
|
const refresh = opts.refresh !== false && !global.graph;
|
|
1474
1519
|
const local = global.local === true;
|
|
1475
|
-
const
|
|
1520
|
+
const optOut = telemetryOptOut();
|
|
1521
|
+
const shareStats = opts.shareStats === true && !local && optOut === null;
|
|
1476
1522
|
const serveOpts = {
|
|
1477
|
-
|
|
1523
|
+
// A *requested* --share-stats always implies local recording, even when
|
|
1524
|
+
// the upload itself is suppressed (--local / env opt-out) — the local
|
|
1525
|
+
// ledger never leaves the machine, and the disclosure messages below
|
|
1526
|
+
// promise "recording locally".
|
|
1527
|
+
savings: opts.savings === true || opts.shareStats === true,
|
|
1478
1528
|
shareStats,
|
|
1479
1529
|
local,
|
|
1480
1530
|
dedup: opts.dedup === true,
|
|
@@ -1483,6 +1533,8 @@ function registerServe(program) {
|
|
|
1483
1533
|
await ensureServableGraph(root, graphPath, global, refresh);
|
|
1484
1534
|
if (opts.shareStats === true && local) {
|
|
1485
1535
|
info(c.dim("vg \xB7 --share-stats ignored under --local (air-gapped): recording locally, not uploading."));
|
|
1536
|
+
} else if (opts.shareStats === true && optOut !== null) {
|
|
1537
|
+
info(c.dim(`vg \xB7 --share-stats disabled by ${optOut}: recording locally, not uploading.`));
|
|
1486
1538
|
}
|
|
1487
1539
|
if (shareStats) startSharing(root);
|
|
1488
1540
|
const freshness = refresh ? "auto-refresh" : "as built";
|
|
@@ -1497,7 +1549,7 @@ function registerServe(program) {
|
|
|
1497
1549
|
}
|
|
1498
1550
|
async function ensureServableGraph(root, graphPath, global, refresh, opts = {}) {
|
|
1499
1551
|
if (refresh) {
|
|
1500
|
-
if (!
|
|
1552
|
+
if (!fs8.existsSync(graphPath)) {
|
|
1501
1553
|
info(c.dim("vg \xB7 no map found \u2014 building it before serving\u2026"));
|
|
1502
1554
|
await runBuild(
|
|
1503
1555
|
[],
|
|
@@ -1514,7 +1566,7 @@ async function ensureServableGraph(root, graphPath, global, refresh, opts = {})
|
|
|
1514
1566
|
}
|
|
1515
1567
|
}
|
|
1516
1568
|
}
|
|
1517
|
-
if (!
|
|
1569
|
+
if (!fs8.existsSync(graphPath)) {
|
|
1518
1570
|
throw new CliError(
|
|
1519
1571
|
`no map found at ${graphPath} \u2014 run \`vg\` to build one first`,
|
|
1520
1572
|
ExitCode.NOT_FOUND
|
|
@@ -1555,6 +1607,13 @@ async function serveHttp(graphPath, host, port, opts, freshness) {
|
|
|
1555
1607
|
res.writeHead(404).end("not found");
|
|
1556
1608
|
return;
|
|
1557
1609
|
}
|
|
1610
|
+
const origin = req.headers.origin;
|
|
1611
|
+
if (!originAllowed(origin, process.env.VIBGRATE_ALLOWED_ORIGINS)) {
|
|
1612
|
+
res.writeHead(403, { "Content-Type": "text/plain" }).end(
|
|
1613
|
+
"forbidden origin \u2014 vg serve only accepts browser requests from loopback origins. Set VIBGRATE_ALLOWED_ORIGINS to allow others."
|
|
1614
|
+
);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1558
1617
|
try {
|
|
1559
1618
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
1560
1619
|
const server = createServer(source, opts);
|
|
@@ -1780,6 +1839,318 @@ function endOfLine(text, line) {
|
|
|
1780
1839
|
const lines = text.split(/\r?\n/);
|
|
1781
1840
|
return (lines[line] ?? "").length;
|
|
1782
1841
|
}
|
|
1842
|
+
var CACHE_VERSION = "vg-lsp-scan-cache/2";
|
|
1843
|
+
function cachePath(root) {
|
|
1844
|
+
return path.join(cacheDir(root), "lsp-scan.json");
|
|
1845
|
+
}
|
|
1846
|
+
function manifestHash(root) {
|
|
1847
|
+
const files = [];
|
|
1848
|
+
const walk2 = (dir) => {
|
|
1849
|
+
let entries;
|
|
1850
|
+
try {
|
|
1851
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
1852
|
+
} catch {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
for (const entry of entries) {
|
|
1856
|
+
const abs = path.join(dir, entry.name);
|
|
1857
|
+
if (entry.isDirectory()) {
|
|
1858
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
1859
|
+
walk2(abs);
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
if (!entry.isFile()) continue;
|
|
1863
|
+
const base = entry.name.toLowerCase();
|
|
1864
|
+
if (!isManifest(abs) && !SKIP_FILES.has(base)) continue;
|
|
1865
|
+
try {
|
|
1866
|
+
const rel3 = path.relative(root, abs).split(path.sep).join("/");
|
|
1867
|
+
files.push({ rel: rel3, hash: hashBytes(fs8.readFileSync(abs)) });
|
|
1868
|
+
} catch {
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
};
|
|
1872
|
+
walk2(root);
|
|
1873
|
+
files.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
1874
|
+
return hashString(canonicalize(files));
|
|
1875
|
+
}
|
|
1876
|
+
function loadScanCache(root, key2) {
|
|
1877
|
+
try {
|
|
1878
|
+
const raw = JSON.parse(fs8.readFileSync(cachePath(root), "utf8"));
|
|
1879
|
+
if (raw.version === CACHE_VERSION && raw.manifestHash === key2.manifestHash && raw.toolVersion === key2.toolVersion && raw.offline === key2.offline && raw.artifact) {
|
|
1880
|
+
return { manifestHash: raw.manifestHash, artifact: raw.artifact };
|
|
1881
|
+
}
|
|
1882
|
+
} catch {
|
|
1883
|
+
}
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
function writeScanCache(root, key2, artifact) {
|
|
1887
|
+
try {
|
|
1888
|
+
fs8.mkdirSync(cacheDir(root), { recursive: true });
|
|
1889
|
+
fs8.writeFileSync(
|
|
1890
|
+
cachePath(root),
|
|
1891
|
+
stableStringify(
|
|
1892
|
+
{ version: CACHE_VERSION, manifestHash: key2.manifestHash, toolVersion: key2.toolVersion, offline: key2.offline, artifact },
|
|
1893
|
+
0
|
|
1894
|
+
)
|
|
1895
|
+
);
|
|
1896
|
+
} catch {
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// src/lsp/graph-query.ts
|
|
1901
|
+
async function runGraphQuery(graph, params, ctx) {
|
|
1902
|
+
switch (params.mode) {
|
|
1903
|
+
case "ask":
|
|
1904
|
+
return runAsk(graph, params, ctx);
|
|
1905
|
+
case "areas":
|
|
1906
|
+
return runAreas(graph, params);
|
|
1907
|
+
case "hubs":
|
|
1908
|
+
return runHubs(graph, params);
|
|
1909
|
+
case "impact":
|
|
1910
|
+
return runImpact(graph, params);
|
|
1911
|
+
case "path":
|
|
1912
|
+
return runPath(graph, params);
|
|
1913
|
+
case "show":
|
|
1914
|
+
return runShow(graph, params);
|
|
1915
|
+
case "tree":
|
|
1916
|
+
return runTree(graph, params);
|
|
1917
|
+
default:
|
|
1918
|
+
return { ok: false, mode: params.mode, error: "bad-request", message: `unknown mode "${String(params.mode)}"` };
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
async function runAsk(graph, params, ctx) {
|
|
1922
|
+
const question = (params.question ?? "").trim();
|
|
1923
|
+
if (!question) return { ok: false, mode: "ask", error: "bad-request", message: "question is required" };
|
|
1924
|
+
const budget = params.budget ?? 2e3;
|
|
1925
|
+
const wantSemantic = !!params.semantic && !ctx.offline && ctx.semantic !== false;
|
|
1926
|
+
let result;
|
|
1927
|
+
let mode = "lexical";
|
|
1928
|
+
let note;
|
|
1929
|
+
if (wantSemantic) {
|
|
1930
|
+
let reason;
|
|
1931
|
+
const embedder = await loadEmbedder({ onUnavailable: (r) => reason = r });
|
|
1932
|
+
if (embedder) {
|
|
1933
|
+
const vectors = await getNodeEmbeddings(graph, embedder, ctx.root);
|
|
1934
|
+
result = await queryGraphSemantic(graph, question, { budget, embedder, nodeVectors: vectors });
|
|
1935
|
+
mode = `semantic (${embedder.id})`;
|
|
1936
|
+
} else {
|
|
1937
|
+
result = queryGraph(graph, question, { budget });
|
|
1938
|
+
note = reason ? unavailableMessage(reason) : "semantic unavailable; used lexical";
|
|
1939
|
+
}
|
|
1940
|
+
} else {
|
|
1941
|
+
result = queryGraph(graph, question, { budget });
|
|
1942
|
+
if (params.semantic && ctx.semantic === false) note = "semantic search is turned off \u2014 used lexical";
|
|
1943
|
+
else if (params.semantic && ctx.offline) note = "semantic skipped \u2014 Vibgrate is running offline";
|
|
1944
|
+
}
|
|
1945
|
+
return {
|
|
1946
|
+
ok: true,
|
|
1947
|
+
mode: "ask",
|
|
1948
|
+
data: {
|
|
1949
|
+
question: result.question,
|
|
1950
|
+
mode,
|
|
1951
|
+
note,
|
|
1952
|
+
tokensEstimate: result.tokensEstimate,
|
|
1953
|
+
matches: result.matches.map((m) => ({
|
|
1954
|
+
name: m.node.qualifiedName,
|
|
1955
|
+
kind: m.node.kind,
|
|
1956
|
+
file: m.node.file,
|
|
1957
|
+
line: m.node.span.start,
|
|
1958
|
+
score: m.score,
|
|
1959
|
+
why: m.why
|
|
1960
|
+
})),
|
|
1961
|
+
context: result.context
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function runAreas(graph, params) {
|
|
1966
|
+
const limit = params.limit ?? 30;
|
|
1967
|
+
const list = [...graph.areas].sort((a, b) => b.size - a.size || a.id - b.id).slice(0, limit);
|
|
1968
|
+
return { ok: true, mode: "areas", data: list };
|
|
1969
|
+
}
|
|
1970
|
+
function runHubs(graph, params) {
|
|
1971
|
+
const limit = params.limit ?? 20;
|
|
1972
|
+
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);
|
|
1973
|
+
return { ok: true, mode: "hubs", data: list };
|
|
1974
|
+
}
|
|
1975
|
+
function nodeSummary2(n) {
|
|
1976
|
+
return {
|
|
1977
|
+
id: n.id,
|
|
1978
|
+
name: n.qualifiedName,
|
|
1979
|
+
kind: n.kind,
|
|
1980
|
+
file: n.file,
|
|
1981
|
+
line: n.span.start,
|
|
1982
|
+
importance: n.importance,
|
|
1983
|
+
isHub: n.isHub,
|
|
1984
|
+
area: n.area
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
function resolveOrError(graph, name, mode) {
|
|
1988
|
+
const { node, candidates } = resolveOne(graph, name);
|
|
1989
|
+
if (node) return { node };
|
|
1990
|
+
if (candidates.length === 0) {
|
|
1991
|
+
return { ok: false, mode, error: "not-found", message: `no node matches "${name}"` };
|
|
1992
|
+
}
|
|
1993
|
+
return {
|
|
1994
|
+
ok: false,
|
|
1995
|
+
mode,
|
|
1996
|
+
error: "ambiguous",
|
|
1997
|
+
message: `"${name}" is ambiguous`,
|
|
1998
|
+
candidates: candidates.slice(0, 10).map((n) => ({ name: n.qualifiedName, kind: n.kind, file: n.file, line: n.span.start }))
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
function runImpact(graph, params) {
|
|
2002
|
+
const name = (params.name ?? "").trim();
|
|
2003
|
+
if (!name) return { ok: false, mode: "impact", error: "bad-request", message: "name is required" };
|
|
2004
|
+
const resolved = resolveOrError(graph, name, "impact");
|
|
2005
|
+
if (!("node" in resolved)) return resolved;
|
|
2006
|
+
const result = impactOf(graph, resolved.node.id, { depth: params.depth ?? 4 });
|
|
2007
|
+
return { ok: true, mode: "impact", data: result };
|
|
2008
|
+
}
|
|
2009
|
+
function runPath(graph, params) {
|
|
2010
|
+
const a = (params.a ?? "").trim();
|
|
2011
|
+
const b = (params.b ?? "").trim();
|
|
2012
|
+
if (!a || !b) return { ok: false, mode: "path", error: "bad-request", message: "a and b are required" };
|
|
2013
|
+
const ra = resolveOrError(graph, a, "path");
|
|
2014
|
+
if (!("node" in ra)) return ra;
|
|
2015
|
+
const rb = resolveOrError(graph, b, "path");
|
|
2016
|
+
if (!("node" in rb)) return rb;
|
|
2017
|
+
const result = shortestPath(graph, ra.node.id, rb.node.id);
|
|
2018
|
+
if (!result) {
|
|
2019
|
+
return {
|
|
2020
|
+
ok: false,
|
|
2021
|
+
mode: "path",
|
|
2022
|
+
error: "not-found",
|
|
2023
|
+
message: `no path between ${ra.node.qualifiedName} and ${rb.node.qualifiedName}`
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
2027
|
+
const names = result.ids.map((id) => byId.get(id)?.qualifiedName ?? id);
|
|
2028
|
+
return {
|
|
2029
|
+
ok: true,
|
|
2030
|
+
mode: "path",
|
|
2031
|
+
data: { from: ra.node.qualifiedName, to: rb.node.qualifiedName, hops: names.length - 1, direction: result.direction, path: names }
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
function runShow(graph, params) {
|
|
2035
|
+
const name = (params.name ?? "").trim();
|
|
2036
|
+
if (!name) return { ok: false, mode: "show", error: "bad-request", message: "name is required" };
|
|
2037
|
+
const resolved = resolveOrError(graph, name, "show");
|
|
2038
|
+
if (!("node" in resolved)) return resolved;
|
|
2039
|
+
const node = resolved.node;
|
|
2040
|
+
const index = indexFor(graph);
|
|
2041
|
+
const callees = dedupeNodes(index.callees(node.id).map((x) => x.node));
|
|
2042
|
+
const callers = dedupeNodes(index.callers(node.id).map((x) => x.node));
|
|
2043
|
+
const extendsEdges = index.out(node.id, "extends").concat(index.out(node.id, "implements"));
|
|
2044
|
+
const supertypes = extendsEdges.map((e) => index.node(e.dst)?.qualifiedName).filter(Boolean);
|
|
2045
|
+
const area = graph.areas.find((a) => a.id === node.area);
|
|
2046
|
+
return {
|
|
2047
|
+
ok: true,
|
|
2048
|
+
mode: "show",
|
|
2049
|
+
data: {
|
|
2050
|
+
id: node.id,
|
|
2051
|
+
name: node.qualifiedName,
|
|
2052
|
+
kind: node.kind,
|
|
2053
|
+
file: node.file,
|
|
2054
|
+
line: node.span.start,
|
|
2055
|
+
signature: node.signature ?? null,
|
|
2056
|
+
importance: node.importance,
|
|
2057
|
+
centrality: node.centrality,
|
|
2058
|
+
isHub: node.isHub,
|
|
2059
|
+
area: node.area,
|
|
2060
|
+
areaLabel: area?.label ?? null,
|
|
2061
|
+
tested: node.tested,
|
|
2062
|
+
calls: callees.map((n) => n.qualifiedName),
|
|
2063
|
+
calledBy: callers.map((n) => n.qualifiedName),
|
|
2064
|
+
extends: supertypes
|
|
2065
|
+
}
|
|
2066
|
+
};
|
|
2067
|
+
}
|
|
2068
|
+
function dedupeNodes(nodes) {
|
|
2069
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2070
|
+
const out2 = [];
|
|
2071
|
+
for (const n of nodes) {
|
|
2072
|
+
if (seen.has(n.id)) continue;
|
|
2073
|
+
seen.add(n.id);
|
|
2074
|
+
out2.push(n);
|
|
2075
|
+
}
|
|
2076
|
+
return out2;
|
|
2077
|
+
}
|
|
2078
|
+
function runTree(graph, params) {
|
|
2079
|
+
const name = (params.name ?? "").trim();
|
|
2080
|
+
if (!name) return { ok: false, mode: "tree", error: "bad-request", message: "name is required" };
|
|
2081
|
+
const resolved = resolveOrError(graph, name, "tree");
|
|
2082
|
+
if (!("node" in resolved)) return resolved;
|
|
2083
|
+
const index = indexFor(graph);
|
|
2084
|
+
const direction = params.callers ? "callers" : "callees";
|
|
2085
|
+
const maxDepth = Math.max(1, params.depth ?? 3);
|
|
2086
|
+
const data = toJsonTree(index, resolved.node, direction, maxDepth, /* @__PURE__ */ new Set());
|
|
2087
|
+
return { ok: true, mode: "tree", data };
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// src/lsp/enrich.ts
|
|
2091
|
+
var OSV_QUERY_URL = "https://api.osv.dev/v1/query";
|
|
2092
|
+
var cache = /* @__PURE__ */ new Map();
|
|
2093
|
+
function key(ecosystem, name, version) {
|
|
2094
|
+
return `${ecosystem}:${name}@${version}`;
|
|
2095
|
+
}
|
|
2096
|
+
function severityOf(v) {
|
|
2097
|
+
const dbSpecific = v.database_specific;
|
|
2098
|
+
const label = typeof dbSpecific?.severity === "string" ? dbSpecific.severity.toLowerCase() : "";
|
|
2099
|
+
if (label.includes("critical")) return "critical";
|
|
2100
|
+
if (label.includes("high")) return "high";
|
|
2101
|
+
if (label.includes("moderate") || label.includes("medium")) return "moderate";
|
|
2102
|
+
if (label.includes("low")) return "low";
|
|
2103
|
+
const sev = Array.isArray(v.severity) ? v.severity : [];
|
|
2104
|
+
const cvss = sev.find((s) => typeof s.score === "string" && String(s.type).startsWith("CVSS"));
|
|
2105
|
+
if (cvss) {
|
|
2106
|
+
const m = /\/AV:.*$/.test(String(cvss.score)) ? null : Number(cvss.score);
|
|
2107
|
+
if (m != null && !Number.isNaN(m)) {
|
|
2108
|
+
if (m >= 9) return "critical";
|
|
2109
|
+
if (m >= 7) return "high";
|
|
2110
|
+
if (m >= 4) return "moderate";
|
|
2111
|
+
if (m > 0) return "low";
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
return "unknown";
|
|
2115
|
+
}
|
|
2116
|
+
function toSummary(raw) {
|
|
2117
|
+
const v = raw ?? {};
|
|
2118
|
+
const aliases = Array.isArray(v.aliases) ? v.aliases.map(String) : [];
|
|
2119
|
+
const details = typeof v.details === "string" ? v.details : "";
|
|
2120
|
+
const summary = typeof v.summary === "string" && v.summary || (details ? details.split("\n")[0].slice(0, 140) : "") || "Advisory";
|
|
2121
|
+
return {
|
|
2122
|
+
id: String(v.id ?? "unknown"),
|
|
2123
|
+
cves: aliases.filter((a) => a.startsWith("CVE-")),
|
|
2124
|
+
summary,
|
|
2125
|
+
severity: severityOf(v)
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
async function enrichVulns(ecosystem, name, version, opts = { offline: false }) {
|
|
2129
|
+
if (!ecosystem || !version) return { vulns: [], cveCount: 0, source: "error" };
|
|
2130
|
+
if (opts.offline) return { vulns: [], cveCount: 0, source: "offline" };
|
|
2131
|
+
const k = key(ecosystem, name, version);
|
|
2132
|
+
const cached = cache.get(k);
|
|
2133
|
+
if (cached) return { ...cached, source: "cache" };
|
|
2134
|
+
try {
|
|
2135
|
+
const controller = new AbortController();
|
|
2136
|
+
const timer = setTimeout(() => controller.abort(), 8e3);
|
|
2137
|
+
const res = await fetch(OSV_QUERY_URL, {
|
|
2138
|
+
method: "POST",
|
|
2139
|
+
headers: { "content-type": "application/json" },
|
|
2140
|
+
body: JSON.stringify({ version, package: { name, ecosystem } }),
|
|
2141
|
+
signal: controller.signal
|
|
2142
|
+
}).finally(() => clearTimeout(timer));
|
|
2143
|
+
if (!res.ok) return { vulns: [], cveCount: 0, source: "error" };
|
|
2144
|
+
const data = await res.json();
|
|
2145
|
+
const vulns = (data.vulns ?? []).map(toSummary);
|
|
2146
|
+
const cveCount = new Set(vulns.flatMap((v) => v.cves)).size;
|
|
2147
|
+
const result = { vulns, cveCount, source: "osv" };
|
|
2148
|
+
cache.set(k, result);
|
|
2149
|
+
return result;
|
|
2150
|
+
} catch {
|
|
2151
|
+
return { vulns: [], cveCount: 0, source: "error" };
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
1783
2154
|
|
|
1784
2155
|
// src/lsp/server.ts
|
|
1785
2156
|
function bandForDependency(dep) {
|
|
@@ -1816,6 +2187,8 @@ var VibgrateLanguageServer = class {
|
|
|
1816
2187
|
queued = false;
|
|
1817
2188
|
debounce = null;
|
|
1818
2189
|
shuttingDown = false;
|
|
2190
|
+
graph = null;
|
|
2191
|
+
graphBuilding = false;
|
|
1819
2192
|
register() {
|
|
1820
2193
|
this.conn.onRequest("initialize", () => ({
|
|
1821
2194
|
capabilities: {
|
|
@@ -1830,7 +2203,10 @@ var VibgrateLanguageServer = class {
|
|
|
1830
2203
|
},
|
|
1831
2204
|
serverInfo: { name: "Vibgrate", version: VERSION }
|
|
1832
2205
|
}));
|
|
1833
|
-
this.conn.onNotification("initialized", () =>
|
|
2206
|
+
this.conn.onNotification("initialized", () => {
|
|
2207
|
+
this.scheduleScan(0);
|
|
2208
|
+
void this.ensureGraph();
|
|
2209
|
+
});
|
|
1834
2210
|
this.conn.onNotification("textDocument/didOpen", (_m, params) => {
|
|
1835
2211
|
const p = params;
|
|
1836
2212
|
const uri = p.textDocument?.uri;
|
|
@@ -1859,6 +2235,9 @@ var VibgrateLanguageServer = class {
|
|
|
1859
2235
|
});
|
|
1860
2236
|
this.conn.onRequest("textDocument/hover", (_m, params) => this.onHover(params));
|
|
1861
2237
|
this.conn.onRequest("textDocument/codeLens", (_m, params) => this.onCodeLens(params));
|
|
2238
|
+
this.conn.onRequest("vibgrate/graph/query", (_m, params) => this.onGraphQuery(params));
|
|
2239
|
+
this.conn.onRequest("vibgrate/score/forFile", (_m, params) => this.onScoreForFile(params));
|
|
2240
|
+
this.conn.onRequest("vibgrate/enrich", (_m, params) => this.onEnrich(params));
|
|
1862
2241
|
this.conn.onRequest("workspace/executeCommand", (_m, params) => {
|
|
1863
2242
|
const p = params;
|
|
1864
2243
|
if (p.command === "vibgrate.rescan") {
|
|
@@ -1888,20 +2267,27 @@ var VibgrateLanguageServer = class {
|
|
|
1888
2267
|
}
|
|
1889
2268
|
this.scanning = true;
|
|
1890
2269
|
try {
|
|
1891
|
-
const
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
2270
|
+
const cacheKey = { manifestHash: manifestHash(this.opts.root), toolVersion: VERSION, offline: this.opts.offline };
|
|
2271
|
+
const cached = loadScanCache(this.opts.root, cacheKey);
|
|
2272
|
+
if (cached) {
|
|
2273
|
+
this.artifact = cached.artifact;
|
|
2274
|
+
} else {
|
|
2275
|
+
const scanOpts = {
|
|
2276
|
+
format: "json",
|
|
2277
|
+
concurrency: 8,
|
|
2278
|
+
offline: this.opts.offline,
|
|
2279
|
+
noLocalArtifacts: true,
|
|
2280
|
+
// an editor scan must not litter the repo
|
|
2281
|
+
noGraph: true,
|
|
2282
|
+
// the code map is not needed to score drift; skip the cost
|
|
2283
|
+
quiet: true,
|
|
2284
|
+
// no spinners, no artifact dump — an editor is not a terminal
|
|
2285
|
+
vibgrateVersion: VERSION
|
|
2286
|
+
};
|
|
2287
|
+
const advanced = await loadAdvancedScanHook();
|
|
2288
|
+
this.artifact = await runCoreScan(this.opts.root, scanOpts, advanced);
|
|
2289
|
+
writeScanCache(this.opts.root, cacheKey, this.artifact);
|
|
2290
|
+
}
|
|
1905
2291
|
this.publishScore();
|
|
1906
2292
|
for (const uri of this.docs.keys()) this.publishForDoc(uri);
|
|
1907
2293
|
} catch (err) {
|
|
@@ -1943,12 +2329,15 @@ var VibgrateLanguageServer = class {
|
|
|
1943
2329
|
band,
|
|
1944
2330
|
// v3 §2.4: `estimated` means "no timestamps at all" — it is NOT an
|
|
1945
2331
|
// offline marker. An air-gapped scan against a dated snapshot is Verified.
|
|
1946
|
-
|
|
2332
|
+
// The score now carries the authoritative provenance (driftscore-3.0
|
|
2333
|
+
// envelope); fall back to the artifact heuristic for pre-v3 engines.
|
|
2334
|
+
mode: a.drift.mode ?? (hasReleaseDates(a) ? "verified" : "estimated"),
|
|
1947
2335
|
methodology: a.drift.methodologyVersion ?? "unknown",
|
|
1948
2336
|
scale: "0 best, 100 worst",
|
|
1949
2337
|
counts: { behind, eol, unmaintained, total },
|
|
1950
2338
|
rootPath: a.rootPath,
|
|
1951
|
-
scannedAt: a.timestamp
|
|
2339
|
+
scannedAt: a.timestamp,
|
|
2340
|
+
...buildBreakdown(a.drift, a.projects ?? []) ? { breakdown: buildBreakdown(a.drift, a.projects ?? []) } : {}
|
|
1952
2341
|
};
|
|
1953
2342
|
this.conn.notify("vibgrate/score", payload);
|
|
1954
2343
|
}
|
|
@@ -2009,8 +2398,9 @@ var VibgrateLanguageServer = class {
|
|
|
2009
2398
|
(d) => findPackageLine(text, d.package, kind) === line
|
|
2010
2399
|
);
|
|
2011
2400
|
if (!dep) return null;
|
|
2401
|
+
const contribution = a.drift.dependencyDrift?.top.find((t) => t.package === dep.package)?.drift ?? null;
|
|
2012
2402
|
return {
|
|
2013
|
-
contents: { kind: "markdown", value: hoverMarkdown(dep) }
|
|
2403
|
+
contents: { kind: "markdown", value: hoverMarkdown(dep, contribution) }
|
|
2014
2404
|
};
|
|
2015
2405
|
}
|
|
2016
2406
|
onCodeLens(params) {
|
|
@@ -2027,7 +2417,8 @@ var VibgrateLanguageServer = class {
|
|
|
2027
2417
|
const eol = (a.findings ?? []).filter(
|
|
2028
2418
|
(f) => f.ruleId === "vibgrate/runtime-eol" || f.ruleId === "vibgrate/eol"
|
|
2029
2419
|
).length;
|
|
2030
|
-
const
|
|
2420
|
+
const estimated = (a.drift.mode ?? (hasReleaseDates(a) ? "verified" : "estimated")) === "estimated";
|
|
2421
|
+
const mode = estimated ? "~" : "";
|
|
2031
2422
|
const title = `Vibgrate \xB7 drift ${mode}${a.drift.score} (${a.drift.riskLevel}) \xB7 ${behind} behind \xB7 ${eol} EOL`;
|
|
2032
2423
|
return [
|
|
2033
2424
|
{
|
|
@@ -2036,6 +2427,78 @@ var VibgrateLanguageServer = class {
|
|
|
2036
2427
|
}
|
|
2037
2428
|
];
|
|
2038
2429
|
}
|
|
2430
|
+
// ── Graph ────────────────────────────────────────────────────────────────
|
|
2431
|
+
/**
|
|
2432
|
+
* Load the committed map if one exists, else build it once (this is the
|
|
2433
|
+
* "first activation sets up the graph" step). Fire-and-forget from
|
|
2434
|
+
* `initialized` so it never blocks the drift score; a query that arrives
|
|
2435
|
+
* mid-build awaits the same in-flight promise via the `graphBuilding` guard.
|
|
2436
|
+
*/
|
|
2437
|
+
async ensureGraph() {
|
|
2438
|
+
if (!this.opts.graph) {
|
|
2439
|
+
this.conn.notify("vibgrate/graph/status", { state: "disabled" });
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
if (this.graph || this.graphBuilding) return;
|
|
2443
|
+
this.graphBuilding = true;
|
|
2444
|
+
try {
|
|
2445
|
+
const existing = loadGraph(this.opts.root);
|
|
2446
|
+
if (existing) {
|
|
2447
|
+
this.graph = existing;
|
|
2448
|
+
this.conn.notify("vibgrate/graph/status", { state: "ready" });
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
this.conn.notify("vibgrate/graph/status", { state: "building" });
|
|
2452
|
+
const result = await buildGraph({ root: this.opts.root });
|
|
2453
|
+
writeArtifacts(result.graph, { root: this.opts.root, html: false, report: false });
|
|
2454
|
+
writeSnapshot(this.opts.root, result.graph.provenance.corpusHash, result.fileStats, {});
|
|
2455
|
+
this.graph = result.graph;
|
|
2456
|
+
this.conn.notify("vibgrate/graph/status", { state: "ready" });
|
|
2457
|
+
} catch (err) {
|
|
2458
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2459
|
+
this.conn.notify("window/logMessage", { type: 3, message: `Vibgrate graph build failed: ${message}` });
|
|
2460
|
+
this.conn.notify("vibgrate/graph/status", { state: "error", message });
|
|
2461
|
+
} finally {
|
|
2462
|
+
this.graphBuilding = false;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
/** The graph, refreshed if the working tree drifted since the last build (same probe `vg ask` uses). */
|
|
2466
|
+
async graphForQuery() {
|
|
2467
|
+
await this.ensureGraph();
|
|
2468
|
+
if (!this.graph) return null;
|
|
2469
|
+
const refreshed = await refreshIfStale(this.opts.root);
|
|
2470
|
+
if (refreshed.status === "refreshed" && refreshed.wrote) {
|
|
2471
|
+
const reloaded = loadGraph(this.opts.root);
|
|
2472
|
+
if (reloaded) this.graph = reloaded;
|
|
2473
|
+
}
|
|
2474
|
+
return this.graph;
|
|
2475
|
+
}
|
|
2476
|
+
async onGraphQuery(params) {
|
|
2477
|
+
const p = params;
|
|
2478
|
+
if (!this.opts.graph) {
|
|
2479
|
+
return { ok: false, mode: p.mode, error: "disabled", message: "the local Vibgrate Graph is turned off" };
|
|
2480
|
+
}
|
|
2481
|
+
const graph = await this.graphForQuery();
|
|
2482
|
+
if (!graph) {
|
|
2483
|
+
return { ok: false, mode: p.mode, error: "not-found", message: "no code map yet \u2014 it is still building" };
|
|
2484
|
+
}
|
|
2485
|
+
return runGraphQuery(graph, p, { root: this.opts.root, offline: this.opts.offline, semantic: this.opts.semantic });
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* `vibgrate/enrich` — lazy vulnerability enrichment for one expanded package,
|
|
2489
|
+
* straight from OSV.dev (never our API). Honours the server's offline flag.
|
|
2490
|
+
*/
|
|
2491
|
+
onEnrich(params) {
|
|
2492
|
+
const p = params ?? {};
|
|
2493
|
+
return enrichVulns(p.ecosystem ?? null, p.package ?? "", p.version ?? null, { offline: this.opts.offline });
|
|
2494
|
+
}
|
|
2495
|
+
onScoreForFile(params) {
|
|
2496
|
+
const p = params;
|
|
2497
|
+
const a = this.artifact;
|
|
2498
|
+
if (!a || !p.uri) return null;
|
|
2499
|
+
const project = projectForFile(this.opts.root, a, uriToPath(p.uri));
|
|
2500
|
+
return project ? scoreForProject(a, project) : null;
|
|
2501
|
+
}
|
|
2039
2502
|
};
|
|
2040
2503
|
function uriToPath(uri) {
|
|
2041
2504
|
try {
|
|
@@ -2068,6 +2531,99 @@ function hasReleaseDates(a) {
|
|
|
2068
2531
|
}
|
|
2069
2532
|
return false;
|
|
2070
2533
|
}
|
|
2534
|
+
function scoreForProject(a, proj) {
|
|
2535
|
+
if (!proj.drift) return null;
|
|
2536
|
+
let behind = 0;
|
|
2537
|
+
let total = 0;
|
|
2538
|
+
for (const dep of proj.dependencies ?? []) {
|
|
2539
|
+
total++;
|
|
2540
|
+
if (dep.drift === "major-behind" || dep.drift === "minor-behind") behind++;
|
|
2541
|
+
}
|
|
2542
|
+
const projFindings = (a.findings ?? []).filter((f) => sameProject(f.location, proj.path));
|
|
2543
|
+
const eol = projFindings.filter((f) => f.ruleId === "vibgrate/runtime-eol" || f.ruleId === "vibgrate/eol").length;
|
|
2544
|
+
const unmaintained = projFindings.filter(
|
|
2545
|
+
(f) => f.ruleId === "vibgrate/unmaintained" || f.ruleId === "vibgrate/abandoned"
|
|
2546
|
+
).length;
|
|
2547
|
+
const hasDates = (proj.dependencies ?? []).some((d) => d.ageDays !== null && d.ageDays !== void 0);
|
|
2548
|
+
return {
|
|
2549
|
+
score: proj.drift.score,
|
|
2550
|
+
band: proj.drift.riskLevel ?? "low",
|
|
2551
|
+
mode: proj.drift.mode ?? (hasDates ? "verified" : "estimated"),
|
|
2552
|
+
methodology: proj.drift.methodologyVersion ?? a.drift.methodologyVersion ?? "unknown",
|
|
2553
|
+
scale: "0 best, 100 worst",
|
|
2554
|
+
counts: { behind, eol, unmaintained, total },
|
|
2555
|
+
rootPath: proj.path,
|
|
2556
|
+
scannedAt: a.timestamp,
|
|
2557
|
+
...buildBreakdown(proj.drift, [proj]) ? { breakdown: buildBreakdown(proj.drift, [proj]) } : {}
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
function bandForScore(n) {
|
|
2561
|
+
if (n <= 30) return "low";
|
|
2562
|
+
if (n <= 60) return "moderate";
|
|
2563
|
+
return "high";
|
|
2564
|
+
}
|
|
2565
|
+
function buildBreakdown(drift, projects) {
|
|
2566
|
+
const top = drift.dependencyDrift?.top;
|
|
2567
|
+
if (!top || top.length === 0) return void 0;
|
|
2568
|
+
const byPkg = /* @__PURE__ */ new Map();
|
|
2569
|
+
for (const p of projects) {
|
|
2570
|
+
const ecosystem = osvEcosystem(p.type);
|
|
2571
|
+
for (const d of p.dependencies ?? []) if (!byPkg.has(d.package)) byPkg.set(d.package, { row: d, ecosystem });
|
|
2572
|
+
}
|
|
2573
|
+
const items = top.filter((t) => t.drift > 0).map((t) => {
|
|
2574
|
+
const found = byPkg.get(t.package);
|
|
2575
|
+
const row = found?.row;
|
|
2576
|
+
return {
|
|
2577
|
+
package: t.package,
|
|
2578
|
+
section: row?.section ?? "dependencies",
|
|
2579
|
+
ecosystem: found?.ecosystem ?? null,
|
|
2580
|
+
drift: t.drift,
|
|
2581
|
+
band: bandForScore(t.drift),
|
|
2582
|
+
currentSpec: row?.currentSpec ?? "",
|
|
2583
|
+
resolvedVersion: row?.resolvedVersion ?? null,
|
|
2584
|
+
latestStable: row?.latestStable ?? null,
|
|
2585
|
+
majorsBehind: row?.majorsBehind ?? null,
|
|
2586
|
+
ageDays: row?.ageDays ?? null,
|
|
2587
|
+
mode: t.mode,
|
|
2588
|
+
unsupported: t.unsupported,
|
|
2589
|
+
abandoned: t.flags.includes("abandoned-floor"),
|
|
2590
|
+
flags: t.flags
|
|
2591
|
+
};
|
|
2592
|
+
});
|
|
2593
|
+
return items.length > 0 ? items : void 0;
|
|
2594
|
+
}
|
|
2595
|
+
function osvEcosystem(type) {
|
|
2596
|
+
switch (type) {
|
|
2597
|
+
case "node":
|
|
2598
|
+
case "typescript":
|
|
2599
|
+
return "npm";
|
|
2600
|
+
case "python":
|
|
2601
|
+
return "PyPI";
|
|
2602
|
+
case "java":
|
|
2603
|
+
case "kotlin":
|
|
2604
|
+
case "scala":
|
|
2605
|
+
case "groovy":
|
|
2606
|
+
return "Maven";
|
|
2607
|
+
case "go":
|
|
2608
|
+
return "Go";
|
|
2609
|
+
case "rust":
|
|
2610
|
+
return "crates.io";
|
|
2611
|
+
case "ruby":
|
|
2612
|
+
return "RubyGems";
|
|
2613
|
+
case "php":
|
|
2614
|
+
return "Packagist";
|
|
2615
|
+
case "dotnet":
|
|
2616
|
+
return "NuGet";
|
|
2617
|
+
case "dart":
|
|
2618
|
+
return "Pub";
|
|
2619
|
+
case "swift":
|
|
2620
|
+
return "SwiftURL";
|
|
2621
|
+
case "elixir":
|
|
2622
|
+
return "Hex";
|
|
2623
|
+
default:
|
|
2624
|
+
return null;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2071
2627
|
function inlineLabel(dep) {
|
|
2072
2628
|
const bits = [];
|
|
2073
2629
|
if (dep.drift === "major-behind") {
|
|
@@ -2082,16 +2638,42 @@ function inlineLabel(dep) {
|
|
|
2082
2638
|
}
|
|
2083
2639
|
return bits.join(" \xB7 ");
|
|
2084
2640
|
}
|
|
2085
|
-
function
|
|
2641
|
+
function commandUri(command, args) {
|
|
2642
|
+
return `command:${command}?${encodeURIComponent(JSON.stringify([args]))}`;
|
|
2643
|
+
}
|
|
2644
|
+
function driftBadge(dep) {
|
|
2645
|
+
if (dep.drift === "major-behind") {
|
|
2646
|
+
const n = dep.majorsBehind ?? 1;
|
|
2647
|
+
return `${n} major${n === 1 ? "" : "s"} behind`;
|
|
2648
|
+
}
|
|
2649
|
+
if (dep.drift === "minor-behind") return "minor behind";
|
|
2650
|
+
return null;
|
|
2651
|
+
}
|
|
2652
|
+
function versionJourney(yours, latest) {
|
|
2653
|
+
const from = Number(/^(\d+)/.exec(yours)?.[1]);
|
|
2654
|
+
const to = Number(/^(\d+)/.exec(latest)?.[1]);
|
|
2655
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || to <= from) return null;
|
|
2656
|
+
const majors = [];
|
|
2657
|
+
for (let m = from; m <= to; m++) majors.push(m);
|
|
2658
|
+
const shown = majors.length > 5 ? [majors[0], majors[1], -1, majors[majors.length - 2], majors[majors.length - 1]] : majors;
|
|
2659
|
+
return shown.map((m) => m === -1 ? "\u2026" : `${m}.x`).join(" \u2192 ");
|
|
2660
|
+
}
|
|
2661
|
+
function hoverMarkdown(dep, contribution) {
|
|
2086
2662
|
const lines = [];
|
|
2087
|
-
|
|
2663
|
+
const badge = driftBadge(dep);
|
|
2664
|
+
const contributionText = contribution != null ? ` \xB7 contributes +${Math.round(contribution)} to drift` : "";
|
|
2665
|
+
lines.push(badge ? `**${dep.package}** \`${badge}\`${contributionText}` : `**${dep.package}**`);
|
|
2088
2666
|
lines.push("");
|
|
2089
2667
|
const yours = dep.resolvedVersion ?? dep.currentSpec;
|
|
2668
|
+
const drifted = dep.drift === "major-behind" || dep.drift === "minor-behind";
|
|
2090
2669
|
if (dep.latestStable && dep.latestStable !== yours) {
|
|
2670
|
+
lines.push("**Currency**");
|
|
2091
2671
|
lines.push(`| | |`);
|
|
2092
2672
|
lines.push(`|---|---|`);
|
|
2093
2673
|
lines.push(`| yours | \`${yours}\` |`);
|
|
2094
2674
|
lines.push(`| latest | \`${dep.latestStable}\` |`);
|
|
2675
|
+
const journey = versionJourney(yours, dep.latestStable);
|
|
2676
|
+
if (journey) lines.push(`| journey | ${journey} |`);
|
|
2095
2677
|
if (dep.majorsBehind) lines.push(`| majors behind | ${dep.majorsBehind} |`);
|
|
2096
2678
|
if (dep.ageDays != null && dep.ageDays >= 1) {
|
|
2097
2679
|
lines.push(`| age | ${Math.round(dep.ageDays).toLocaleString("en-US")} days |`);
|
|
@@ -2104,12 +2686,31 @@ function hoverMarkdown(dep) {
|
|
|
2104
2686
|
} else {
|
|
2105
2687
|
lines.push(`\`${yours}\` \u2014 current.`);
|
|
2106
2688
|
}
|
|
2689
|
+
if (dep.unsupported || dep.abandoned) {
|
|
2690
|
+
lines.push("");
|
|
2691
|
+
lines.push("**Maintenance**");
|
|
2692
|
+
if (dep.unsupported) lines.push("$(warning) Unsupported / deprecated \u2014 floored at 70.");
|
|
2693
|
+
if (dep.abandoned) lines.push("$(warning) Abandoned \u2014 no recent releases \u2014 floored at 50.");
|
|
2694
|
+
}
|
|
2695
|
+
if (drifted) {
|
|
2696
|
+
lines.push("");
|
|
2697
|
+
lines.push("---");
|
|
2698
|
+
const routeFixUri = commandUri("vibgrate.routeFix", {
|
|
2699
|
+
package: dep.package,
|
|
2700
|
+
from: yours,
|
|
2701
|
+
to: dep.latestStable ?? yours,
|
|
2702
|
+
majorsBehind: dep.majorsBehind ?? 0
|
|
2703
|
+
});
|
|
2704
|
+
const explainUri = commandUri("vibgrate.explain", { package: dep.package });
|
|
2705
|
+
lines.push(`[Route this fix \u2192](${routeFixUri}) \xB7 [Explain this drift](${explainUri})`);
|
|
2706
|
+
}
|
|
2107
2707
|
lines.push("");
|
|
2708
|
+
lines.push("---");
|
|
2108
2709
|
lines.push("$(shield) Resolved locally from your lockfile. No source uploaded.");
|
|
2109
2710
|
return lines.join("\n");
|
|
2110
2711
|
}
|
|
2111
2712
|
function startLanguageServer(opts) {
|
|
2112
|
-
if (!
|
|
2713
|
+
if (!fs8.existsSync(opts.root)) {
|
|
2113
2714
|
process.stderr.write(`vg lsp: root does not exist: ${opts.root}
|
|
2114
2715
|
`);
|
|
2115
2716
|
process.exit(1);
|
|
@@ -2134,7 +2735,7 @@ function registerLsp(program) {
|
|
|
2134
2735
|
"--diagnostics",
|
|
2135
2736
|
"also publish Problems-panel diagnostics (EOL runtime, unmaintained, license change). Off by default: drift is not a defect, and the Problems panel is not ours to fill",
|
|
2136
2737
|
false
|
|
2137
|
-
).action(async function(opts) {
|
|
2738
|
+
).option("--no-graph", "skip the local Vibgrate Graph entirely: no background build, and graph queries report it as turned off").option("--no-semantic", "never use semantic search for graph queries (lexical only; the embedding model is not downloaded)").action(async function(opts) {
|
|
2138
2739
|
const global = readGlobal(this);
|
|
2139
2740
|
const root = rootOf(global);
|
|
2140
2741
|
startLanguageServer({
|
|
@@ -2142,7 +2743,9 @@ function registerLsp(program) {
|
|
|
2142
2743
|
// `--local` is the established air-gap flag across the CLI; honour it
|
|
2143
2744
|
// here too, so an offline editor session stays offline.
|
|
2144
2745
|
offline: global.local === true,
|
|
2145
|
-
diagnostics: opts.diagnostics === true
|
|
2746
|
+
diagnostics: opts.diagnostics === true,
|
|
2747
|
+
graph: opts.graph !== false,
|
|
2748
|
+
semantic: opts.semantic !== false
|
|
2146
2749
|
});
|
|
2147
2750
|
await new Promise(() => {
|
|
2148
2751
|
});
|
|
@@ -2153,10 +2756,25 @@ function registerLsp(program) {
|
|
|
2153
2756
|
// src/commands/install.ts
|
|
2154
2757
|
var SMALL_REPO_FILES = 150;
|
|
2155
2758
|
function registerInstall(program) {
|
|
2156
|
-
const install = program.command("install").description("add vg to your AI assistant(s): skill + MCP + advisory nudge").argument("[tools...]", `assistant ids: ${ASSISTANTS.map((a) => a.id).join(", ")}`).option("--all", "install for every supported assistant").option("--list", "show the support matrix and exit").option("--no-hook", "skip the advisory nudge").action(function(tools, opts) {
|
|
2759
|
+
const install = program.command("install").description("add vg to your AI assistant(s): skill + MCP + advisory nudge").argument("[tools...]", `assistant ids: ${ASSISTANTS.map((a) => a.id).join(", ")}`).option("--all", "install for every supported assistant").option("--detect", "detect assistants in use (repo footprint, home config, PATH) and install for those; with --list, only report what was detected").option("--list", "show the support matrix and exit").option("--no-hook", "skip the advisory nudge").action(function(tools, opts) {
|
|
2157
2760
|
const global = readGlobal(this);
|
|
2158
2761
|
const root = rootOf(global);
|
|
2762
|
+
const detected = opts.detect ? detectAssistants(root) : [];
|
|
2159
2763
|
if (opts.list) {
|
|
2764
|
+
if (opts.detect) {
|
|
2765
|
+
if (global.json) {
|
|
2766
|
+
json(detected.map((d) => ({ id: d.assistant.id, label: d.assistant.label, via: d.via, marker: d.marker })));
|
|
2767
|
+
} else if (detected.length === 0) {
|
|
2768
|
+
info(`${c.cyan("vg install")} \xB7 no AI assistants detected here`);
|
|
2769
|
+
} else {
|
|
2770
|
+
info(`${c.cyan("vg install")} \xB7 detected assistants`);
|
|
2771
|
+
const pad4 = Math.max(...detected.map((d) => d.assistant.id.length)) + 2;
|
|
2772
|
+
for (const d of detected) {
|
|
2773
|
+
info(` ${c.bold(d.assistant.id.padEnd(pad4))} ${d.assistant.label} ${c.dim(`via ${d.via}: ${d.marker}`)}`);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2160
2778
|
if (global.json) {
|
|
2161
2779
|
json(ASSISTANTS.map((a) => ({ id: a.id, label: a.label, mcp: !!a.mcp, skill: !!a.skill, nudge: !!a.nudge })));
|
|
2162
2780
|
} else {
|
|
@@ -2168,8 +2786,15 @@ function registerInstall(program) {
|
|
|
2168
2786
|
}
|
|
2169
2787
|
return;
|
|
2170
2788
|
}
|
|
2171
|
-
const targets = opts.all ? ASSISTANTS : tools.map(resolve6);
|
|
2172
|
-
if (targets.length === 0)
|
|
2789
|
+
const targets = opts.all ? ASSISTANTS : opts.detect ? detected.map((d) => d.assistant) : tools.map(resolve6);
|
|
2790
|
+
if (targets.length === 0) {
|
|
2791
|
+
if (opts.detect) {
|
|
2792
|
+
if (global.json) json({ root, detected: [], results: [] });
|
|
2793
|
+
else info(`${c.cyan("vg install")} \xB7 no AI assistants detected here \u2014 name one (e.g. \`vg install claude\`) or use --all`);
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
throw usageError("name an assistant (e.g. `vg install claude`) or use --all / --detect / --list");
|
|
2797
|
+
}
|
|
2173
2798
|
const graph = loadGraph(root, global.graph);
|
|
2174
2799
|
const fileCount = graph ? graph.nodes.filter((n) => n.kind === "file").length : 0;
|
|
2175
2800
|
const smallRepo = graph !== null && fileCount > 0 && fileCount < SMALL_REPO_FILES;
|
|
@@ -2224,7 +2849,7 @@ function registerShare(program) {
|
|
|
2224
2849
|
const global = readGlobal(this);
|
|
2225
2850
|
const root = rootOf(global);
|
|
2226
2851
|
const gitDir = path.join(root, ".git");
|
|
2227
|
-
if (!
|
|
2852
|
+
if (!fs8.existsSync(gitDir)) {
|
|
2228
2853
|
throw new CliError("not a git repository \u2014 run `vg share` inside a git repo", ExitCode.USAGE_ERROR);
|
|
2229
2854
|
}
|
|
2230
2855
|
if (opts.undo) {
|
|
@@ -2246,22 +2871,22 @@ function registerShare(program) {
|
|
|
2246
2871
|
}
|
|
2247
2872
|
function writeVibgrateGitignore(root, reports) {
|
|
2248
2873
|
const dir = path.join(root, ".vibgrate");
|
|
2249
|
-
|
|
2874
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
2250
2875
|
const lines = ["# Managed by `vg share` \u2014 keep graph.json committed, ignore the rest", "cache/", "facts.jsonl"];
|
|
2251
2876
|
if (!reports) lines.push("graph.html", "GRAPH_REPORT.md");
|
|
2252
|
-
|
|
2877
|
+
fs8.writeFileSync(path.join(dir, ".gitignore"), `${lines.join("\n")}
|
|
2253
2878
|
`);
|
|
2254
2879
|
}
|
|
2255
2880
|
function writeGitAttributes(root) {
|
|
2256
2881
|
const file = path.join(root, ".gitattributes");
|
|
2257
2882
|
const line = ".vibgrate/graph.json merge=vg";
|
|
2258
|
-
let content =
|
|
2883
|
+
let content = fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : "";
|
|
2259
2884
|
if (!content.split("\n").some((l) => l.trim() === line)) {
|
|
2260
2885
|
content = content.length ? `${content.replace(/\s*$/, "")}
|
|
2261
2886
|
${line}
|
|
2262
2887
|
` : `${line}
|
|
2263
2888
|
`;
|
|
2264
|
-
|
|
2889
|
+
fs8.writeFileSync(file, content);
|
|
2265
2890
|
}
|
|
2266
2891
|
}
|
|
2267
2892
|
function configureMergeDriver(root) {
|
|
@@ -2270,7 +2895,7 @@ function configureMergeDriver(root) {
|
|
|
2270
2895
|
}
|
|
2271
2896
|
function installPreCommitHook(root) {
|
|
2272
2897
|
const hooksDir = hookDir(root);
|
|
2273
|
-
|
|
2898
|
+
fs8.mkdirSync(hooksDir, { recursive: true });
|
|
2274
2899
|
const file = path.join(hooksDir, "pre-commit");
|
|
2275
2900
|
const block = [
|
|
2276
2901
|
HOOK_BEGIN,
|
|
@@ -2280,7 +2905,7 @@ function installPreCommitHook(root) {
|
|
|
2280
2905
|
"fi",
|
|
2281
2906
|
HOOK_END
|
|
2282
2907
|
].join("\n");
|
|
2283
|
-
let content =
|
|
2908
|
+
let content = fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : "#!/bin/sh\n";
|
|
2284
2909
|
const re = new RegExp(`${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}`);
|
|
2285
2910
|
if (re.test(content)) content = content.replace(re, block);
|
|
2286
2911
|
else content = `${content.replace(/\s*$/, "")}
|
|
@@ -2289,21 +2914,21 @@ ${block}
|
|
|
2289
2914
|
`;
|
|
2290
2915
|
if (!content.startsWith("#!")) content = `#!/bin/sh
|
|
2291
2916
|
${content}`;
|
|
2292
|
-
|
|
2293
|
-
|
|
2917
|
+
fs8.writeFileSync(file, content);
|
|
2918
|
+
fs8.chmodSync(file, 493);
|
|
2294
2919
|
}
|
|
2295
2920
|
function undo(root) {
|
|
2296
2921
|
const hookFile = path.join(hookDir(root), "pre-commit");
|
|
2297
|
-
if (
|
|
2922
|
+
if (fs8.existsSync(hookFile)) {
|
|
2298
2923
|
const re = new RegExp(`\\n*${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}\\n*`);
|
|
2299
|
-
const next2 =
|
|
2300
|
-
if (next2.trim() === "#!/bin/sh")
|
|
2301
|
-
else
|
|
2924
|
+
const next2 = fs8.readFileSync(hookFile, "utf8").replace(re, "\n");
|
|
2925
|
+
if (next2.trim() === "#!/bin/sh") fs8.rmSync(hookFile);
|
|
2926
|
+
else fs8.writeFileSync(hookFile, next2);
|
|
2302
2927
|
}
|
|
2303
2928
|
const attr = path.join(root, ".gitattributes");
|
|
2304
|
-
if (
|
|
2305
|
-
const next2 =
|
|
2306
|
-
|
|
2929
|
+
if (fs8.existsSync(attr)) {
|
|
2930
|
+
const next2 = fs8.readFileSync(attr, "utf8").split("\n").filter((l) => l.trim() !== ".vibgrate/graph.json merge=vg").join("\n");
|
|
2931
|
+
fs8.writeFileSync(attr, next2);
|
|
2307
2932
|
}
|
|
2308
2933
|
try {
|
|
2309
2934
|
git(root, ["config", "--remove-section", "merge.vg"]);
|
|
@@ -2436,7 +3061,7 @@ function grepBaselineTokens(term, files) {
|
|
|
2436
3061
|
if (f.size > 512 * 1024) continue;
|
|
2437
3062
|
let content = "";
|
|
2438
3063
|
try {
|
|
2439
|
-
content =
|
|
3064
|
+
content = fs8.readFileSync(f.abs, "utf8");
|
|
2440
3065
|
} catch {
|
|
2441
3066
|
continue;
|
|
2442
3067
|
}
|
|
@@ -2446,7 +3071,7 @@ function grepBaselineTokens(term, files) {
|
|
|
2446
3071
|
}
|
|
2447
3072
|
function safeSize(abs) {
|
|
2448
3073
|
try {
|
|
2449
|
-
return
|
|
3074
|
+
return fs8.statSync(abs).size;
|
|
2450
3075
|
} catch {
|
|
2451
3076
|
return 0;
|
|
2452
3077
|
}
|
|
@@ -2768,9 +3393,9 @@ function normalizeStandards(raw) {
|
|
|
2768
3393
|
const name = asString(r.name);
|
|
2769
3394
|
if (!name) continue;
|
|
2770
3395
|
const ecosystem = asString(r.ecosystem)?.toLowerCase();
|
|
2771
|
-
const
|
|
2772
|
-
if (seen.has(
|
|
2773
|
-
seen.add(
|
|
3396
|
+
const key2 = `${ecosystem ?? "*"}\0${name.toLowerCase()}`;
|
|
3397
|
+
if (seen.has(key2)) continue;
|
|
3398
|
+
seen.add(key2);
|
|
2774
3399
|
banned.push({ name, ...ecosystem ? { ecosystem } : {}, ...asString(r.use) ? { use: asString(r.use) } : {}, ...asString(r.reason) ? { reason: asString(r.reason) } : {} });
|
|
2775
3400
|
}
|
|
2776
3401
|
return { banned };
|
|
@@ -2780,7 +3405,7 @@ function loadStandards(root) {
|
|
|
2780
3405
|
const p = path__default.join(root, rel3);
|
|
2781
3406
|
let text;
|
|
2782
3407
|
try {
|
|
2783
|
-
text =
|
|
3408
|
+
text = fs8__default.readFileSync(p, "utf8");
|
|
2784
3409
|
} catch {
|
|
2785
3410
|
continue;
|
|
2786
3411
|
}
|
|
@@ -2948,7 +3573,7 @@ function printBreakdown(usage) {
|
|
|
2948
3573
|
info(c.dim(` avg success across commands: ${usage.avgSuccessPct}%`));
|
|
2949
3574
|
}
|
|
2950
3575
|
function printSplit(usage) {
|
|
2951
|
-
const label = (
|
|
3576
|
+
const label = (key2) => key2 === "mcp" ? "MCP tools" : key2 === "cli" ? "vg CLI" : key2;
|
|
2952
3577
|
if (usage.sources.length) {
|
|
2953
3578
|
info("");
|
|
2954
3579
|
info(c.bold(" by source") + c.dim(" (how the call arrived)"));
|
|
@@ -3018,7 +3643,7 @@ async function addCmd(root, rest, opts, asJson) {
|
|
|
3018
3643
|
}
|
|
3019
3644
|
function readPackageVersion(root) {
|
|
3020
3645
|
try {
|
|
3021
|
-
const pkg = JSON.parse(
|
|
3646
|
+
const pkg = JSON.parse(fs8__default.readFileSync(path__default.join(root, "package.json"), "utf8"));
|
|
3022
3647
|
return typeof pkg.version === "string" ? pkg.version : void 0;
|
|
3023
3648
|
} catch {
|
|
3024
3649
|
return void 0;
|
|
@@ -3026,7 +3651,7 @@ function readPackageVersion(root) {
|
|
|
3026
3651
|
}
|
|
3027
3652
|
function readOptionalFile(p) {
|
|
3028
3653
|
try {
|
|
3029
|
-
const t =
|
|
3654
|
+
const t = fs8__default.readFileSync(p, "utf8");
|
|
3030
3655
|
return t.trim() ? t : void 0;
|
|
3031
3656
|
} catch {
|
|
3032
3657
|
return void 0;
|
|
@@ -3172,8 +3797,8 @@ function registerExport(program) {
|
|
|
3172
3797
|
out(content.trimEnd());
|
|
3173
3798
|
return;
|
|
3174
3799
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
3800
|
+
fs8.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
3801
|
+
fs8.writeFileSync(file, content);
|
|
3177
3802
|
info(`${c.green("\u2714")} wrote ${file} ${c.dim(`(${format})`)}`);
|
|
3178
3803
|
});
|
|
3179
3804
|
applyGlobalOptions(cmd);
|
|
@@ -3196,24 +3821,24 @@ function registerBundle(program) {
|
|
|
3196
3821
|
} catch {
|
|
3197
3822
|
throw new CliError("no grammars found to bundle (run a build first)", ExitCode.ERROR);
|
|
3198
3823
|
}
|
|
3199
|
-
|
|
3824
|
+
fs8.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
|
|
3200
3825
|
let grammarCount = 0;
|
|
3201
3826
|
for (const { fileName, absPath } of grammarFiles) {
|
|
3202
|
-
|
|
3827
|
+
fs8.copyFileSync(absPath, path.join(outDir, "grammars", fileName));
|
|
3203
3828
|
grammarCount++;
|
|
3204
3829
|
}
|
|
3205
3830
|
const included = [`grammars/ (${grammarCount} .wasm)`];
|
|
3206
3831
|
const graphSrc = global.graph ?? defaultGraphPath(root);
|
|
3207
|
-
if (
|
|
3208
|
-
|
|
3832
|
+
if (fs8.existsSync(graphSrc)) {
|
|
3833
|
+
fs8.copyFileSync(graphSrc, path.join(outDir, "graph.json"));
|
|
3209
3834
|
included.push("graph.json");
|
|
3210
3835
|
}
|
|
3211
3836
|
const cat = catalogPath(root);
|
|
3212
|
-
if (
|
|
3213
|
-
|
|
3837
|
+
if (fs8.existsSync(cat)) {
|
|
3838
|
+
fs8.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
|
|
3214
3839
|
included.push("vibgrate.lib.json");
|
|
3215
|
-
if (
|
|
3216
|
-
|
|
3840
|
+
if (fs8.existsSync(libDir(root))) {
|
|
3841
|
+
fs8.cpSync(libDir(root), path.join(outDir, "lib"), { recursive: true });
|
|
3217
3842
|
included.push("lib/");
|
|
3218
3843
|
}
|
|
3219
3844
|
}
|
|
@@ -3225,7 +3850,7 @@ function registerBundle(program) {
|
|
|
3225
3850
|
included,
|
|
3226
3851
|
usage: "Run `vg build --grammars <this dir>/grammars` to build fully offline."
|
|
3227
3852
|
};
|
|
3228
|
-
|
|
3853
|
+
fs8.writeFileSync(path.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
|
|
3229
3854
|
`);
|
|
3230
3855
|
if (global.json) {
|
|
3231
3856
|
json({ out: path.relative(root, outDir), ...manifest });
|
|
@@ -3237,6 +3862,125 @@ function registerBundle(program) {
|
|
|
3237
3862
|
});
|
|
3238
3863
|
applyGlobalOptions(cmd);
|
|
3239
3864
|
}
|
|
3865
|
+
function registerDoctor(program) {
|
|
3866
|
+
const cmd = program.command("doctor").description("diagnose your setup: config, credentials, map freshness, hosted reachability, MCP launch").action(async function() {
|
|
3867
|
+
await runDoctor(readGlobal(this));
|
|
3868
|
+
});
|
|
3869
|
+
applyGlobalOptions(cmd);
|
|
3870
|
+
}
|
|
3871
|
+
var REACH_TIMEOUT_MS = 3e3;
|
|
3872
|
+
var CONFIG_BASENAMES = ["vibgrate.config.ts", "vibgrate.config.js", "vibgrate.config.json"];
|
|
3873
|
+
async function runDoctor(global) {
|
|
3874
|
+
const root = rootOf(global);
|
|
3875
|
+
const local = global.local === true;
|
|
3876
|
+
const graphPath = global.graph ?? defaultGraphPath(root);
|
|
3877
|
+
const configFile = CONFIG_BASENAMES.find((f) => fs8.existsSync(path.join(root, f))) ?? null;
|
|
3878
|
+
const graph = loadGraph(root, graphPath);
|
|
3879
|
+
let staleFiles = null;
|
|
3880
|
+
if (graph) {
|
|
3881
|
+
const probe = probeFreshness(root);
|
|
3882
|
+
if (probe) staleFiles = driftCount(probe.drift);
|
|
3883
|
+
}
|
|
3884
|
+
const catalog = loadCatalog(root);
|
|
3885
|
+
const libCount = Object.keys(catalog.libraries).length;
|
|
3886
|
+
const creds = diagnoseCredentials(root);
|
|
3887
|
+
const base = hostedBase();
|
|
3888
|
+
const reachable = local ? null : await probeReachable(base);
|
|
3889
|
+
const launch = detectServeLaunch();
|
|
3890
|
+
const optOut = telemetryOptOut();
|
|
3891
|
+
const d = {
|
|
3892
|
+
version: VERSION,
|
|
3893
|
+
node: process.version,
|
|
3894
|
+
platform: `${process.platform}/${process.arch}`,
|
|
3895
|
+
root,
|
|
3896
|
+
configFile,
|
|
3897
|
+
map: {
|
|
3898
|
+
path: path.relative(root, graphPath),
|
|
3899
|
+
built: graph !== null,
|
|
3900
|
+
generatedAt: graph?.generatedAt ?? null,
|
|
3901
|
+
staleFiles
|
|
3902
|
+
},
|
|
3903
|
+
libCatalog: {
|
|
3904
|
+
path: path.relative(root, catalogPath(root)),
|
|
3905
|
+
present: fs8.existsSync(catalogPath(root)),
|
|
3906
|
+
libraries: libCount
|
|
3907
|
+
},
|
|
3908
|
+
credentials: creds,
|
|
3909
|
+
hosted: { base, checked: !local, reachable },
|
|
3910
|
+
mcpLaunch: { command: launch.command, args: launch.args, note: launch.note ?? null },
|
|
3911
|
+
telemetry: { optOut, ci: isCI(), endpoint: statsEndpoint() }
|
|
3912
|
+
};
|
|
3913
|
+
if (global.json) {
|
|
3914
|
+
json(d);
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
info(`${c.cyan("vg")} doctor \xB7 v${d.version} \xB7 node ${d.node} \xB7 ${d.platform}`);
|
|
3918
|
+
info(` root ${d.root}`);
|
|
3919
|
+
info(` config ${d.configFile ? c.green(d.configFile) : c.dim("none (defaults) \u2014 `vg init` writes one")}`);
|
|
3920
|
+
if (!d.map.built) {
|
|
3921
|
+
info(` map ${c.yellow("none")} \u2014 run ${c.bold("vg")} to build ${c.dim(d.map.path)}`);
|
|
3922
|
+
} else {
|
|
3923
|
+
const fresh = d.map.staleFiles == null ? c.dim("freshness unknown (no snapshot)") : d.map.staleFiles === 0 ? c.green("up to date") : c.yellow(`${d.map.staleFiles} file(s) stale \u2014 auto-refreshes on next query`);
|
|
3924
|
+
info(` map ${c.green("built")} ${c.dim(d.map.generatedAt ?? "")} \xB7 ${fresh}`);
|
|
3925
|
+
}
|
|
3926
|
+
info(
|
|
3927
|
+
` lib ${d.libCatalog.present ? `${c.green(String(d.libCatalog.libraries))} librar${d.libCatalog.libraries === 1 ? "y" : "ies"} in ${d.libCatalog.path}` : c.dim("no catalog \u2014 `vg lib add <source>` starts one; `vg lib <name>` works without it")}`
|
|
3928
|
+
);
|
|
3929
|
+
if (d.credentials.source === "none") {
|
|
3930
|
+
info(` auth ${c.dim("anonymous \u2014 fine for everything local; `vg login` enables push/publish")}`);
|
|
3931
|
+
} else {
|
|
3932
|
+
info(
|
|
3933
|
+
` auth ${c.green(d.credentials.source)} ${c.dim(d.credentials.path ?? "")} \xB7 workspace ${d.credentials.workspaceId ?? "?"} \xB7 ${d.credentials.host ?? "?"}`
|
|
3934
|
+
);
|
|
3935
|
+
}
|
|
3936
|
+
if (!d.hosted.checked) {
|
|
3937
|
+
info(` hosted ${c.dim(`skipped under --local (${d.hosted.base})`)}`);
|
|
3938
|
+
} else {
|
|
3939
|
+
info(
|
|
3940
|
+
` hosted ${d.hosted.base} \xB7 ${d.hosted.reachable ? c.green("reachable") : c.yellow("unreachable \u2014 local answers still work")}`
|
|
3941
|
+
);
|
|
3942
|
+
}
|
|
3943
|
+
info(
|
|
3944
|
+
` mcp ${c.bold(`${d.mcpLaunch.command} ${d.mcpLaunch.args.join(" ")}`)}${d.mcpLaunch.note ? c.dim(` (${d.mcpLaunch.note})`) : ""}`
|
|
3945
|
+
);
|
|
3946
|
+
const tel = d.telemetry.optOut ? c.green(`opted out via ${d.telemetry.optOut}`) : "off by default \u2014 only `vg serve --share-stats` ever uploads";
|
|
3947
|
+
info(` telemetry ${tel}${d.telemetry.ci ? c.dim(" \xB7 CI detected") : ""}`);
|
|
3948
|
+
}
|
|
3949
|
+
function diagnoseCredentials(root) {
|
|
3950
|
+
const none = { source: "none", path: null, host: null, workspaceId: null };
|
|
3951
|
+
if (process.env.VIBGRATE_DSN) {
|
|
3952
|
+
const parsed2 = parseDsn$1(process.env.VIBGRATE_DSN);
|
|
3953
|
+
return { source: "env", path: null, host: parsed2?.host ?? null, workspaceId: parsed2?.workspaceId ?? null };
|
|
3954
|
+
}
|
|
3955
|
+
const stored = readStoredCredentials({ cwd: root });
|
|
3956
|
+
if (!stored) return none;
|
|
3957
|
+
const file = credentialsPath({ cwd: root });
|
|
3958
|
+
const source = process.env.VIBGRATE_CREDENTIALS ? "env" : file === projectCredentialsPath(root) ? "project" : "home";
|
|
3959
|
+
const parsed = parseDsn$1(stored.dsn);
|
|
3960
|
+
return {
|
|
3961
|
+
source,
|
|
3962
|
+
path: shortenHome(file),
|
|
3963
|
+
host: stored.ingestHost ?? parsed?.host ?? null,
|
|
3964
|
+
workspaceId: stored.workspaceId ?? parsed?.workspaceId ?? null
|
|
3965
|
+
};
|
|
3966
|
+
}
|
|
3967
|
+
function shortenHome(p) {
|
|
3968
|
+
const home = homeCredentialsPath().slice(0, -"/.vibgrate/credentials.json".length);
|
|
3969
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
3970
|
+
}
|
|
3971
|
+
async function probeReachable(base) {
|
|
3972
|
+
const controller = new AbortController();
|
|
3973
|
+
const timer = setTimeout(() => controller.abort(), REACH_TIMEOUT_MS);
|
|
3974
|
+
timer.unref?.();
|
|
3975
|
+
try {
|
|
3976
|
+
await fetch(base, { method: "HEAD", signal: controller.signal });
|
|
3977
|
+
return true;
|
|
3978
|
+
} catch {
|
|
3979
|
+
return false;
|
|
3980
|
+
} finally {
|
|
3981
|
+
clearTimeout(timer);
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3240
3984
|
var initCommand = new Command("init").description("Initialize vibgrate in a project").argument("[path]", "Path to initialize", ".").option("--baseline", "Create initial baseline after init").option("--yes", "Skip confirmation prompts").action(async (targetPath, opts) => {
|
|
3241
3985
|
const rootDir = path.resolve(targetPath);
|
|
3242
3986
|
const vibgrateDir2 = path.join(rootDir, ".vibgrate");
|
|
@@ -3250,7 +3994,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
|
|
|
3250
3994
|
console.log(chalk8.green("\u2714") + ` Created ${chalk8.bold("vibgrate.config.ts")}`);
|
|
3251
3995
|
}
|
|
3252
3996
|
if (opts.baseline) {
|
|
3253
|
-
const { runBaseline } = await import('./baseline-
|
|
3997
|
+
const { runBaseline } = await import('./baseline-DKPQWOQ6.js');
|
|
3254
3998
|
await runBaseline(rootDir);
|
|
3255
3999
|
}
|
|
3256
4000
|
console.log("");
|
|
@@ -3360,9 +4104,9 @@ async function fileReferencesIdentifier(rootDir, relFile, identifier, readFile3)
|
|
|
3360
4104
|
}
|
|
3361
4105
|
async function defaultReadFile(absPath) {
|
|
3362
4106
|
try {
|
|
3363
|
-
const stat2 = await
|
|
4107
|
+
const stat2 = await fs15.stat(absPath);
|
|
3364
4108
|
if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) return null;
|
|
3365
|
-
return await
|
|
4109
|
+
return await fs15.readFile(absPath, "utf8");
|
|
3366
4110
|
} catch {
|
|
3367
4111
|
return null;
|
|
3368
4112
|
}
|
|
@@ -3391,11 +4135,11 @@ async function analyzeReachability(input) {
|
|
|
3391
4135
|
const importedModules = [];
|
|
3392
4136
|
const seenPackages = /* @__PURE__ */ new Set();
|
|
3393
4137
|
for (const dep of input.dependencies) {
|
|
3394
|
-
const
|
|
3395
|
-
if (seenPackages.has(
|
|
3396
|
-
seenPackages.add(
|
|
4138
|
+
const key2 = `${dep.ecosystem}|${dep.package}`;
|
|
4139
|
+
if (seenPackages.has(key2)) continue;
|
|
4140
|
+
seenPackages.add(key2);
|
|
3397
4141
|
const evidence = collectPackageImports(input.graph, dep.ecosystem, dep.package);
|
|
3398
|
-
evidenceByPackage.set(
|
|
4142
|
+
evidenceByPackage.set(key2, evidence);
|
|
3399
4143
|
if (evidence.supported && importedModules.length < MAX_IMPORTED_MODULE_PACKAGES) {
|
|
3400
4144
|
importedModules.push({
|
|
3401
4145
|
ecosystem: dep.ecosystem,
|
|
@@ -3410,8 +4154,8 @@ async function analyzeReachability(input) {
|
|
|
3410
4154
|
);
|
|
3411
4155
|
const findings = [];
|
|
3412
4156
|
for (const entry of input.manifest) {
|
|
3413
|
-
const
|
|
3414
|
-
const evidence = evidenceByPackage.get(
|
|
4157
|
+
const key2 = `${entry.ecosystem}|${entry.package}`;
|
|
4158
|
+
const evidence = evidenceByPackage.get(key2) ?? collectPackageImports(input.graph, entry.ecosystem, entry.package);
|
|
3415
4159
|
const baseConfidence = ECOSYSTEM_MATCH_CONFIDENCE[entry.ecosystem] ?? 0;
|
|
3416
4160
|
if (!evidence.supported) {
|
|
3417
4161
|
findings.push({
|
|
@@ -3509,9 +4253,9 @@ function collectPreflightDependencies(projects, projectTypeToEcosystem) {
|
|
|
3509
4253
|
for (const dep of project.dependencies) {
|
|
3510
4254
|
const version = dep.resolvedVersion || normalizeVersionSpec(dep.currentSpec);
|
|
3511
4255
|
if (!version) continue;
|
|
3512
|
-
const
|
|
3513
|
-
if (!deduped.has(
|
|
3514
|
-
deduped.set(
|
|
4256
|
+
const key2 = `${ecosystem}:${dep.package}:${version}`;
|
|
4257
|
+
if (!deduped.has(key2)) {
|
|
4258
|
+
deduped.set(key2, { ecosystem, package: dep.package, version });
|
|
3515
4259
|
}
|
|
3516
4260
|
}
|
|
3517
4261
|
}
|
|
@@ -3986,7 +4730,7 @@ var WATCHED_FILES = /* @__PURE__ */ new Set([
|
|
|
3986
4730
|
"packages.lock.json"
|
|
3987
4731
|
]);
|
|
3988
4732
|
var WATCHED_EXT = /\.(cs|fs)proj$/i;
|
|
3989
|
-
var
|
|
4733
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3990
4734
|
"node_modules",
|
|
3991
4735
|
".git",
|
|
3992
4736
|
"dist",
|
|
@@ -4007,7 +4751,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
4007
4751
|
function scanStaleness(rootDir, artifactPath) {
|
|
4008
4752
|
let ref;
|
|
4009
4753
|
try {
|
|
4010
|
-
ref =
|
|
4754
|
+
ref = fs8.statSync(artifactPath).mtimeMs;
|
|
4011
4755
|
} catch {
|
|
4012
4756
|
return { stale: false };
|
|
4013
4757
|
}
|
|
@@ -4019,7 +4763,7 @@ function scanStaleness(rootDir, artifactPath) {
|
|
|
4019
4763
|
if (depth > 8 || scanned > MAX_ENTRIES) return;
|
|
4020
4764
|
let entries;
|
|
4021
4765
|
try {
|
|
4022
|
-
entries =
|
|
4766
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
4023
4767
|
} catch {
|
|
4024
4768
|
return;
|
|
4025
4769
|
}
|
|
@@ -4027,14 +4771,14 @@ function scanStaleness(rootDir, artifactPath) {
|
|
|
4027
4771
|
scanned++;
|
|
4028
4772
|
if (scanned > MAX_ENTRIES) break;
|
|
4029
4773
|
if (e.isDirectory()) {
|
|
4030
|
-
if (!
|
|
4774
|
+
if (!SKIP_DIRS2.has(e.name) && !e.name.startsWith(".")) walk2(path.join(dir, e.name), depth + 1);
|
|
4031
4775
|
continue;
|
|
4032
4776
|
}
|
|
4033
4777
|
if (!WATCHED_FILES.has(e.name) && !WATCHED_EXT.test(e.name)) continue;
|
|
4034
4778
|
const abs = path.join(dir, e.name);
|
|
4035
4779
|
let mtimeMs;
|
|
4036
4780
|
try {
|
|
4037
|
-
mtimeMs =
|
|
4781
|
+
mtimeMs = fs8.statSync(abs).mtimeMs;
|
|
4038
4782
|
} catch {
|
|
4039
4783
|
continue;
|
|
4040
4784
|
}
|
|
@@ -4101,7 +4845,7 @@ function extractContracts(content, pkg, ecosystem) {
|
|
|
4101
4845
|
}
|
|
4102
4846
|
|
|
4103
4847
|
// src/reporting/planning/usage.ts
|
|
4104
|
-
var
|
|
4848
|
+
var SKIP_DIRS3 = /* @__PURE__ */ new Set([
|
|
4105
4849
|
"node_modules",
|
|
4106
4850
|
".git",
|
|
4107
4851
|
"dist",
|
|
@@ -4168,7 +4912,7 @@ function analyzeTree(root, packages) {
|
|
|
4168
4912
|
if (depth > MAX_DEPTH || filesSeen >= MAX_FILES) return;
|
|
4169
4913
|
let entries;
|
|
4170
4914
|
try {
|
|
4171
|
-
entries =
|
|
4915
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
4172
4916
|
} catch {
|
|
4173
4917
|
return;
|
|
4174
4918
|
}
|
|
@@ -4176,7 +4920,7 @@ function analyzeTree(root, packages) {
|
|
|
4176
4920
|
for (const entry of entries) {
|
|
4177
4921
|
if (filesSeen >= MAX_FILES) return;
|
|
4178
4922
|
if (entry.isDirectory()) {
|
|
4179
|
-
if (
|
|
4923
|
+
if (SKIP_DIRS3.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
4180
4924
|
walk2(path.join(dir, entry.name), depth + 1);
|
|
4181
4925
|
continue;
|
|
4182
4926
|
}
|
|
@@ -4186,8 +4930,8 @@ function analyzeTree(root, packages) {
|
|
|
4186
4930
|
const full = path.join(dir, entry.name);
|
|
4187
4931
|
let content;
|
|
4188
4932
|
try {
|
|
4189
|
-
if (
|
|
4190
|
-
content =
|
|
4933
|
+
if (fs8.statSync(full).size > MAX_FILE_BYTES2) continue;
|
|
4934
|
+
content = fs8.readFileSync(full, "utf8");
|
|
4191
4935
|
} catch {
|
|
4192
4936
|
continue;
|
|
4193
4937
|
}
|
|
@@ -4283,9 +5027,6 @@ function planBlock(plan, recommended) {
|
|
|
4283
5027
|
if (plan.upgrades.length > shown.length) {
|
|
4284
5028
|
lines.push(chalk8.dim(` \u2026 and ${plan.upgrades.length - shown.length} more`));
|
|
4285
5029
|
}
|
|
4286
|
-
if (plan.upgrades.length === 0) {
|
|
4287
|
-
lines.push(chalk8.dim(" (no upgrades in this plan)"));
|
|
4288
|
-
}
|
|
4289
5030
|
return lines.join("\n");
|
|
4290
5031
|
}
|
|
4291
5032
|
function dataNote(report) {
|
|
@@ -4312,9 +5053,15 @@ function renderText(report) {
|
|
|
4312
5053
|
out2.push(chalk8.red(`\u26A0 ${report.exploitability.kevPackages} package(s) carry a KNOWN-EXPLOITED (KEV) advisory${epss} \u2014 prioritise these.`));
|
|
4313
5054
|
}
|
|
4314
5055
|
out2.push("");
|
|
4315
|
-
|
|
4316
|
-
|
|
5056
|
+
const visiblePlans = report.plans.filter((plan) => plan.upgrades.length > 0);
|
|
5057
|
+
if (visiblePlans.length === 0) {
|
|
5058
|
+
out2.push(chalk8.green("\u2714 Nothing to upgrade \u2014 every tracked dependency is current."));
|
|
4317
5059
|
out2.push("");
|
|
5060
|
+
} else {
|
|
5061
|
+
for (const plan of visiblePlans) {
|
|
5062
|
+
out2.push(planBlock(plan, plan.tier === report.recommended));
|
|
5063
|
+
out2.push("");
|
|
5064
|
+
}
|
|
4318
5065
|
}
|
|
4319
5066
|
out2.push(chalk8.bold("Recommendation"));
|
|
4320
5067
|
out2.push(` ${chalk8.green(report.plans.find((p) => p.tier === report.recommended)?.label ?? report.recommended)} \u2014 ${report.rationale}`);
|
|
@@ -4337,7 +5084,12 @@ function renderMarkdown(report) {
|
|
|
4337
5084
|
out2.push(`_${report.vulnerabilityData === "unavailable" ? "Advisory data was unavailable \u2014 vulnerability impact is not shown." : "Advisory data was partial \u2014 some ecosystems were not checked."}_`);
|
|
4338
5085
|
}
|
|
4339
5086
|
out2.push("");
|
|
4340
|
-
|
|
5087
|
+
const visiblePlans = report.plans.filter((plan) => plan.upgrades.length > 0);
|
|
5088
|
+
if (visiblePlans.length === 0) {
|
|
5089
|
+
out2.push("\u2714 Nothing to upgrade \u2014 every tracked dependency is current.");
|
|
5090
|
+
out2.push("");
|
|
5091
|
+
}
|
|
5092
|
+
for (const plan of visiblePlans) {
|
|
4341
5093
|
const rec = plan.tier === report.recommended ? " \u2705 **recommended**" : "";
|
|
4342
5094
|
out2.push(`## ${plan.label}${rec}`);
|
|
4343
5095
|
out2.push("");
|
|
@@ -4350,14 +5102,12 @@ function renderMarkdown(report) {
|
|
|
4350
5102
|
out2.push(`- \u26A0 Introduces advisories in target versions: ${deltaSummary(plan.introduces)}`);
|
|
4351
5103
|
}
|
|
4352
5104
|
out2.push("");
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
out2.push(`| \`${u.package}\` | ${u.from ?? "?"} | ${u.to ?? "?"} | ${u.kind} | ${u.reason} |`);
|
|
4358
|
-
}
|
|
4359
|
-
out2.push("");
|
|
5105
|
+
out2.push("| Package | From | To | Kind | Reason |");
|
|
5106
|
+
out2.push("|---|---|---|---|---|");
|
|
5107
|
+
for (const u of plan.upgrades) {
|
|
5108
|
+
out2.push(`| \`${u.package}\` | ${u.from ?? "?"} | ${u.to ?? "?"} | ${u.kind} | ${u.reason} |`);
|
|
4360
5109
|
}
|
|
5110
|
+
out2.push("");
|
|
4361
5111
|
}
|
|
4362
5112
|
out2.push("## Recommendation");
|
|
4363
5113
|
out2.push("");
|
|
@@ -4370,6 +5120,29 @@ function renderMarkdown(report) {
|
|
|
4370
5120
|
return out2.join("\n");
|
|
4371
5121
|
}
|
|
4372
5122
|
|
|
5123
|
+
// src/reporting/planning/dedupe.ts
|
|
5124
|
+
var TIER_ORDER = ["safe", "balanced", "aggressive"];
|
|
5125
|
+
function upgradeSetKey(plan) {
|
|
5126
|
+
return plan.upgrades.map((u) => `${u.ecosystem}\0${u.package}\0${u.from ?? ""}\0${u.to ?? ""}`).sort().join("\n");
|
|
5127
|
+
}
|
|
5128
|
+
function dedupePlans(plans) {
|
|
5129
|
+
const byTier = (plan) => {
|
|
5130
|
+
const i = TIER_ORDER.indexOf(plan.tier);
|
|
5131
|
+
return i === -1 ? TIER_ORDER.length : i;
|
|
5132
|
+
};
|
|
5133
|
+
const ordered = [...plans].sort((a, b) => byTier(a) - byTier(b));
|
|
5134
|
+
const survivorByKey = /* @__PURE__ */ new Map();
|
|
5135
|
+
const canonicalTier = /* @__PURE__ */ new Map();
|
|
5136
|
+
for (const plan of ordered) {
|
|
5137
|
+
const key2 = upgradeSetKey(plan);
|
|
5138
|
+
const survivor = survivorByKey.get(key2) ?? plan;
|
|
5139
|
+
if (!survivorByKey.has(key2)) survivorByKey.set(key2, plan);
|
|
5140
|
+
canonicalTier.set(plan.tier, survivor.tier);
|
|
5141
|
+
}
|
|
5142
|
+
const survivors = new Set(survivorByKey.values());
|
|
5143
|
+
return { plans: plans.filter((p) => survivors.has(p)), canonicalTier };
|
|
5144
|
+
}
|
|
5145
|
+
|
|
4373
5146
|
// src/reporting/planning/expected-drift.ts
|
|
4374
5147
|
function estimateDriftScore(artifact, upgraded) {
|
|
4375
5148
|
const projects = JSON.parse(JSON.stringify(artifact.projects ?? []));
|
|
@@ -4526,22 +5299,56 @@ async function fetchLatestVersion() {
|
|
|
4526
5299
|
}
|
|
4527
5300
|
async function writeCache(data) {
|
|
4528
5301
|
try {
|
|
4529
|
-
await
|
|
4530
|
-
await
|
|
5302
|
+
await fs15.mkdir(CACHE_DIR, { recursive: true });
|
|
5303
|
+
await fs15.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
|
|
4531
5304
|
} catch {
|
|
4532
5305
|
}
|
|
4533
5306
|
}
|
|
4534
5307
|
|
|
4535
5308
|
// src/reporting/commands/update.ts
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
if (execPath
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
5309
|
+
var PACKAGE_NAME = "@vibgrate/cli";
|
|
5310
|
+
function detectGlobalInstall(execPath = process.argv[1] || "", cwd = process.cwd()) {
|
|
5311
|
+
if (!execPath) return null;
|
|
5312
|
+
let resolved = execPath;
|
|
5313
|
+
try {
|
|
5314
|
+
resolved = fs8.realpathSync(execPath);
|
|
5315
|
+
} catch {
|
|
5316
|
+
}
|
|
5317
|
+
const p = resolved.replace(/\\/g, "/");
|
|
5318
|
+
if (!p.includes("/node_modules/")) return null;
|
|
5319
|
+
if (p.includes("/_npx/") || p.includes("/dlx-") || p.includes("/.bunx/")) return null;
|
|
5320
|
+
const workdir = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
5321
|
+
if (workdir && (p === workdir || p.startsWith(workdir + "/"))) return null;
|
|
5322
|
+
if (p.includes("/pnpm/")) return "pnpm";
|
|
5323
|
+
if (p.includes("/yarn/") || p.includes("/.yarn/")) return "yarn";
|
|
5324
|
+
if (p.includes("/.bun/")) return "bun";
|
|
5325
|
+
return "npm";
|
|
5326
|
+
}
|
|
5327
|
+
var defaultRunner = (cmd) => execSync(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3 });
|
|
5328
|
+
async function findGlobalInstall(pkg = PACKAGE_NAME, run = defaultRunner) {
|
|
5329
|
+
const tryRun = (cmd) => {
|
|
5330
|
+
try {
|
|
5331
|
+
const out2 = run(cmd).trim().split("\n")[0]?.trim();
|
|
5332
|
+
return out2 || null;
|
|
5333
|
+
} catch {
|
|
5334
|
+
return null;
|
|
4544
5335
|
}
|
|
5336
|
+
};
|
|
5337
|
+
const probes = [
|
|
5338
|
+
{ pm: "npm", root: () => tryRun("npm root -g") },
|
|
5339
|
+
{ pm: "pnpm", root: () => tryRun("pnpm root -g") },
|
|
5340
|
+
{
|
|
5341
|
+
pm: "yarn",
|
|
5342
|
+
root: () => {
|
|
5343
|
+
const dir = tryRun("yarn global dir");
|
|
5344
|
+
return dir ? path.join(dir, "node_modules") : null;
|
|
5345
|
+
}
|
|
5346
|
+
},
|
|
5347
|
+
{ pm: "bun", root: () => path.join(os.homedir(), ".bun", "install", "global", "node_modules") }
|
|
5348
|
+
];
|
|
5349
|
+
for (const probe of probes) {
|
|
5350
|
+
const root = probe.root();
|
|
5351
|
+
if (root && await pathExists(path.join(root, pkg))) return probe.pm;
|
|
4545
5352
|
}
|
|
4546
5353
|
return null;
|
|
4547
5354
|
}
|
|
@@ -4591,15 +5398,16 @@ function getInstallCommand(pm, pkg, version, isDev, opts = {}) {
|
|
|
4591
5398
|
return isDev ? `npm install --save-dev ${spec}` : `npm install ${spec}`;
|
|
4592
5399
|
}
|
|
4593
5400
|
}
|
|
4594
|
-
async function
|
|
5401
|
+
async function getLocalDependencyState(cwd, pkg = PACKAGE_NAME) {
|
|
4595
5402
|
try {
|
|
4596
5403
|
const pkgPath = path.join(cwd, "package.json");
|
|
4597
5404
|
const raw = await (await import('fs/promises')).readFile(pkgPath, "utf-8");
|
|
4598
|
-
const
|
|
4599
|
-
|
|
5405
|
+
const parsed = JSON.parse(raw);
|
|
5406
|
+
if (parsed.devDependencies?.[pkg]) return "dev";
|
|
5407
|
+
if (parsed.dependencies?.[pkg]) return "prod";
|
|
4600
5408
|
} catch {
|
|
4601
|
-
return true;
|
|
4602
5409
|
}
|
|
5410
|
+
return null;
|
|
4603
5411
|
}
|
|
4604
5412
|
async function confirmWorkspaceRoot(pm) {
|
|
4605
5413
|
if (!(process.stdin.isTTY && process.stdout.isTTY)) return false;
|
|
@@ -4632,15 +5440,29 @@ var updateCommand = new Command("update").description("Update vibgrate to the la
|
|
|
4632
5440
|
return;
|
|
4633
5441
|
}
|
|
4634
5442
|
const cwd = process.cwd();
|
|
4635
|
-
const
|
|
4636
|
-
|
|
4637
|
-
|
|
5443
|
+
const runtimeGlobalPm = detectGlobalInstall();
|
|
5444
|
+
let globalPm = runtimeGlobalPm;
|
|
5445
|
+
let isGlobal = Boolean(opts.global) || runtimeGlobalPm !== null;
|
|
5446
|
+
let localState = null;
|
|
5447
|
+
if (!isGlobal) {
|
|
5448
|
+
localState = await getLocalDependencyState(cwd);
|
|
5449
|
+
if (localState === null) {
|
|
5450
|
+
globalPm = await findGlobalInstall();
|
|
5451
|
+
if (globalPm) {
|
|
5452
|
+
isGlobal = true;
|
|
5453
|
+
console.log(
|
|
5454
|
+
chalk8.dim(`Found a global ${globalPm} installation of ${PACKAGE_NAME} \u2014 updating it instead of this project.`)
|
|
5455
|
+
);
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
const pm = opts.pm || globalPm || await detectPackageManager(cwd);
|
|
4638
5460
|
let cmd;
|
|
4639
5461
|
if (isGlobal) {
|
|
4640
|
-
cmd = getGlobalUpdateCommand(pm,
|
|
5462
|
+
cmd = getGlobalUpdateCommand(pm, PACKAGE_NAME, latest);
|
|
4641
5463
|
console.log(chalk8.dim(`Updating global installation with ${pm}: ${cmd}`));
|
|
4642
5464
|
} else {
|
|
4643
|
-
const isDev =
|
|
5465
|
+
const isDev = localState !== "prod";
|
|
4644
5466
|
let workspaceRoot = false;
|
|
4645
5467
|
if (pm === "pnpm" && await detectWorkspaceRoot(cwd)) {
|
|
4646
5468
|
console.log(
|
|
@@ -4648,19 +5470,19 @@ var updateCommand = new Command("update").description("Update vibgrate to the la
|
|
|
4648
5470
|
);
|
|
4649
5471
|
const proceed = opts.yes || opts.workspaceRoot || await confirmWorkspaceRoot(pm);
|
|
4650
5472
|
if (!proceed) {
|
|
4651
|
-
const rootCmd = getInstallCommand(pm,
|
|
5473
|
+
const rootCmd = getInstallCommand(pm, PACKAGE_NAME, latest, isDev, { workspaceRoot: true });
|
|
4652
5474
|
console.log(chalk8.dim(`Skipped. To update the workspace root, run: ${rootCmd}`));
|
|
4653
5475
|
console.log(chalk8.dim('or re-run "vg update --yes" (or --workspace-root) to let vg do it.'));
|
|
4654
5476
|
return;
|
|
4655
5477
|
}
|
|
4656
5478
|
workspaceRoot = true;
|
|
4657
5479
|
}
|
|
4658
|
-
cmd = getInstallCommand(pm,
|
|
5480
|
+
cmd = getInstallCommand(pm, PACKAGE_NAME, latest, isDev, { workspaceRoot });
|
|
4659
5481
|
console.log(chalk8.dim(`Using ${pm}: ${cmd}`));
|
|
4660
5482
|
}
|
|
4661
5483
|
try {
|
|
4662
5484
|
execSync(cmd, { cwd, stdio: "inherit" });
|
|
4663
|
-
console.log(chalk8.green("\u2714") + ` Updated to
|
|
5485
|
+
console.log(chalk8.green("\u2714") + ` Updated to ${PACKAGE_NAME}@${latest}`);
|
|
4664
5486
|
} catch {
|
|
4665
5487
|
console.error(chalk8.red(`Update failed. Run manually: ${cmd}`));
|
|
4666
5488
|
process.exit(1);
|
|
@@ -4684,11 +5506,11 @@ function collectCandidates(artifact) {
|
|
|
4684
5506
|
for (const project of artifact.projects ?? []) {
|
|
4685
5507
|
const ecosystem = ecosystemId(project.type);
|
|
4686
5508
|
for (const dep of project.dependencies ?? []) {
|
|
4687
|
-
const
|
|
4688
|
-
if (seen.has(
|
|
5509
|
+
const key2 = `${ecosystem}\0${dep.package}`;
|
|
5510
|
+
if (seen.has(key2)) continue;
|
|
4689
5511
|
if (!dep.latestStable || dep.latestStable === dep.resolvedVersion) continue;
|
|
4690
5512
|
if (dep.drift === "current" || dep.drift === "unknown") continue;
|
|
4691
|
-
seen.add(
|
|
5513
|
+
seen.add(key2);
|
|
4692
5514
|
out2.push({
|
|
4693
5515
|
package: dep.package,
|
|
4694
5516
|
ecosystem,
|
|
@@ -4826,6 +5648,9 @@ var fixCommand = new Command("fix").description("Get ranked, risk-tiered upgrade
|
|
|
4826
5648
|
if (response.requestId) console.error(chalk8.dim(` (ref ${response.requestId})`));
|
|
4827
5649
|
process.exit(1);
|
|
4828
5650
|
}
|
|
5651
|
+
const { plans: dedupedPlans, canonicalTier } = dedupePlans(response.plans);
|
|
5652
|
+
response.plans = dedupedPlans;
|
|
5653
|
+
response.recommended = canonicalTier.get(response.recommended) ?? response.recommended;
|
|
4829
5654
|
const currentDrift = artifact.drift?.score;
|
|
4830
5655
|
if (typeof currentDrift === "number") {
|
|
4831
5656
|
response.currentDriftScore = currentDrift;
|
|
@@ -4838,7 +5663,7 @@ var fixCommand = new Command("fix").description("Get ranked, risk-tiered upgrade
|
|
|
4838
5663
|
}
|
|
4839
5664
|
emit(response, opts.format);
|
|
4840
5665
|
if (opts.format === "text" && opts.apply !== false) {
|
|
4841
|
-
await runApplyFlow(rootDir, artifact, response, opts);
|
|
5666
|
+
await runApplyFlow(rootDir, artifact, response, opts, canonicalTier);
|
|
4842
5667
|
}
|
|
4843
5668
|
if (failOn) {
|
|
4844
5669
|
const recommended = response.plans.find((p) => p.tier === response.recommended);
|
|
@@ -4847,7 +5672,8 @@ var fixCommand = new Command("fix").description("Get ranked, risk-tiered upgrade
|
|
|
4847
5672
|
for (const sev of Object.keys(SEVERITY_RANK)) {
|
|
4848
5673
|
if (SEVERITY_RANK[sev] < threshold) continue;
|
|
4849
5674
|
const open = response.unresolved.bySeverity[sev] ?? 0;
|
|
4850
|
-
const
|
|
5675
|
+
const maxFixable = Math.max(0, ...response.plans.map((p) => p.fixes.bySeverity[sev] ?? 0));
|
|
5676
|
+
const notFixedByRecommended = maxFixable - (recommended?.fixes.bySeverity[sev] ?? 0);
|
|
4851
5677
|
stillOpen += open + Math.max(0, notFixedByRecommended);
|
|
4852
5678
|
}
|
|
4853
5679
|
if (stillOpen > 0) {
|
|
@@ -4904,8 +5730,8 @@ async function buildTargetResolver(rootDir, artifact) {
|
|
|
4904
5730
|
const ecosystem = ecosystemId(project.type);
|
|
4905
5731
|
const dir = relProjectDir(rootDir, project.path);
|
|
4906
5732
|
for (const dep of project.dependencies ?? []) {
|
|
4907
|
-
const
|
|
4908
|
-
(owners.get(
|
|
5733
|
+
const key2 = `${ecosystem}\0${dep.package}`;
|
|
5734
|
+
(owners.get(key2) ?? owners.set(key2, /* @__PURE__ */ new Set()).get(key2)).add(dir);
|
|
4909
5735
|
}
|
|
4910
5736
|
}
|
|
4911
5737
|
const rootFallback = [{ dir: ".", isWorkspaceRoot: rootIsWorkspace }];
|
|
@@ -4946,7 +5772,7 @@ function promptPlanSelection(response) {
|
|
|
4946
5772
|
});
|
|
4947
5773
|
});
|
|
4948
5774
|
}
|
|
4949
|
-
async function runApplyFlow(rootDir, artifact, response, opts) {
|
|
5775
|
+
async function runApplyFlow(rootDir, artifact, response, opts, canonicalTier) {
|
|
4950
5776
|
const nonEmpty = response.plans.filter((p) => p.upgrades.length > 0);
|
|
4951
5777
|
if (nonEmpty.length === 0) {
|
|
4952
5778
|
console.log(chalk8.green("\n\u2714 Nothing to upgrade \u2014 every tracked dependency is current."));
|
|
@@ -4954,7 +5780,8 @@ async function runApplyFlow(rootDir, artifact, response, opts) {
|
|
|
4954
5780
|
}
|
|
4955
5781
|
let chosen;
|
|
4956
5782
|
if (opts.plan) {
|
|
4957
|
-
|
|
5783
|
+
const tier = canonicalTier?.get(opts.plan) ?? opts.plan;
|
|
5784
|
+
chosen = response.plans.find((p) => p.tier === tier);
|
|
4958
5785
|
if (!chosen) {
|
|
4959
5786
|
console.error(chalk8.red(`Unknown plan '${opts.plan}'. Use safe, balanced, or aggressive.`));
|
|
4960
5787
|
process.exit(1);
|
|
@@ -6199,21 +7026,21 @@ function formatDeltaText(base, current) {
|
|
|
6199
7026
|
const added = [];
|
|
6200
7027
|
const removed = [];
|
|
6201
7028
|
const changed = [];
|
|
6202
|
-
for (const [
|
|
6203
|
-
if (!baseMap.has(
|
|
6204
|
-
added.push(`${
|
|
7029
|
+
for (const [key2, dep] of currentMap.entries()) {
|
|
7030
|
+
if (!baseMap.has(key2)) {
|
|
7031
|
+
added.push(`${key2} @ ${dep.resolvedVersion ?? dep.currentSpec}`);
|
|
6205
7032
|
continue;
|
|
6206
7033
|
}
|
|
6207
|
-
const prev = baseMap.get(
|
|
7034
|
+
const prev = baseMap.get(key2);
|
|
6208
7035
|
const prevVersion = prev.resolvedVersion ?? prev.currentSpec;
|
|
6209
7036
|
const nowVersion = dep.resolvedVersion ?? dep.currentSpec;
|
|
6210
7037
|
if (prevVersion !== nowVersion || prev.majorsBehind !== dep.majorsBehind) {
|
|
6211
|
-
changed.push(`${
|
|
7038
|
+
changed.push(`${key2} ${prevVersion} -> ${nowVersion} (majorsBehind ${prev.majorsBehind ?? "unknown"} -> ${dep.majorsBehind ?? "unknown"})`);
|
|
6212
7039
|
}
|
|
6213
7040
|
}
|
|
6214
|
-
for (const [
|
|
6215
|
-
if (!currentMap.has(
|
|
6216
|
-
removed.push(`${
|
|
7041
|
+
for (const [key2, dep] of baseMap.entries()) {
|
|
7042
|
+
if (!currentMap.has(key2)) {
|
|
7043
|
+
removed.push(`${key2} @ ${dep.resolvedVersion ?? dep.currentSpec}`);
|
|
6217
7044
|
}
|
|
6218
7045
|
}
|
|
6219
7046
|
const lines = [
|
|
@@ -6300,6 +7127,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
6300
7127
|
"oddities",
|
|
6301
7128
|
"serve",
|
|
6302
7129
|
"lsp",
|
|
7130
|
+
"doctor",
|
|
6303
7131
|
"install",
|
|
6304
7132
|
"uninstall",
|
|
6305
7133
|
"share",
|
|
@@ -6355,6 +7183,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
|
|
|
6355
7183
|
registerLib(program);
|
|
6356
7184
|
registerExport(program);
|
|
6357
7185
|
registerBundle(program);
|
|
7186
|
+
registerDoctor(program);
|
|
6358
7187
|
program.addCommand(initCommand);
|
|
6359
7188
|
program.addCommand(scanCommand);
|
|
6360
7189
|
program.addCommand(fixCommand);
|
|
@@ -6422,7 +7251,7 @@ function dispatch(argv, cwd) {
|
|
|
6422
7251
|
}
|
|
6423
7252
|
const looksLikeQuestion = /\s/.test(first) || first.endsWith("?");
|
|
6424
7253
|
if (looksLikeQuestion) return ["ask", ...args];
|
|
6425
|
-
if (
|
|
7254
|
+
if (fs8.existsSync(path.resolve(cwd, first))) {
|
|
6426
7255
|
return [hasBuildOnlyFlag(args) ? "build" : "scan", ...args];
|
|
6427
7256
|
}
|
|
6428
7257
|
const moved = MOVED_COMMANDS[first];
|