@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/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-D4BPWARI.js';
3
- import { resolvedGrammarFiles, grammarSetVersion, canonicalize } from './chunk-VFO5UDAT.js';
4
- import { loadAdvancedScanHook, baselineCommand } from './chunk-6DKWB2OI.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-C5AVF7PT.js';
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 fs7 from 'fs';
9
- import fs7__default, { realpathSync } from 'fs';
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 fs13 from 'fs/promises';
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 (!fs7.existsSync(keyPath)) {
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
- fs7.mkdirSync(path.dirname(keyPath), { recursive: true });
187
- fs7.writeFileSync(keyPath, kp.privatePem, { mode: 384 });
188
- fs7.writeFileSync(`${keyPath}.pub`, kp.publicPem);
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(fs7.readFileSync(keyPath, "utf8"));
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
- fs7.mkdirSync(path.dirname(outPath), { recursive: true });
211
- fs7.writeFileSync(outPath, serializeEnvelope(envelope));
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 (!fs7.existsSync(bundlePath)) {
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(fs7.readFileSync(bundlePath, "utf8"));
236
- const publicKeyPem = opts.pub ? fs7.readFileSync(path.resolve(opts.pub), "utf8") : void 0;
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
- fs7.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
588
- fs7.writeFileSync(target, content);
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 = fs7.existsSync(path.join(cacheDir(root), "parse-cache.json"));
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 installId() {
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 = fs7.readFileSync(INSTALL_ID_FILE, "utf8").trim();
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
- fs7.mkdirSync(INSTALL_ID_DIR, { recursive: true });
1299
- fs7.writeFileSync(INSTALL_ID_FILE, id + "\n", { mode: 384 });
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(fs7.readFileSync(offsetPath(root), "utf8"));
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
- fs7.mkdirSync(cacheDir(root), { recursive: true });
1332
- fs7.writeFileSync(offsetPath(root), JSON.stringify({ offset }) + "\n");
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 = fs7.statSync(file).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 = fs7.openSync(file, "r");
1372
+ const fd = fs8.openSync(file, "r");
1349
1373
  try {
1350
1374
  const buf = Buffer.alloc(size - start);
1351
- fs7.readSync(fd, buf, 0, buf.length, start);
1375
+ fs8.readSync(fd, buf, 0, buf.length, start);
1352
1376
  text = buf.toString("utf8");
1353
1377
  } finally {
1354
- fs7.closeSync(fd);
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 key = `${source}|${client}|${e.tool}|${outcome}`;
1378
- const row = rows.get(key) ?? {
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(key, row);
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 shareStats = opts.shareStats === true && !local;
1520
+ const optOut = telemetryOptOut();
1521
+ const shareStats = opts.shareStats === true && !local && optOut === null;
1476
1522
  const serveOpts = {
1477
- savings: opts.savings === true || shareStats,
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 (!fs7.existsSync(graphPath)) {
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 (!fs7.existsSync(graphPath)) {
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", () => this.scheduleScan(0));
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 scanOpts = {
1892
- format: "json",
1893
- concurrency: 8,
1894
- offline: this.opts.offline,
1895
- noLocalArtifacts: true,
1896
- // an editor scan must not litter the repo
1897
- noGraph: true,
1898
- // the code map is not needed to score drift; skip the cost
1899
- quiet: true,
1900
- // no spinners, no artifact dump — an editor is not a terminal
1901
- vibgrateVersion: VERSION
1902
- };
1903
- const advanced = await loadAdvancedScanHook();
1904
- this.artifact = await runCoreScan(this.opts.root, scanOpts, advanced);
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
- mode: hasReleaseDates(a) ? "verified" : "estimated",
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 mode = hasReleaseDates(a) ? "" : "~";
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 hoverMarkdown(dep) {
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
- lines.push(`**${dep.package}**`);
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 (!fs7.existsSync(opts.root)) {
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) throw usageError("name an assistant (e.g. `vg install claude`) or use --all / --list");
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 (!fs7.existsSync(gitDir)) {
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
- fs7.mkdirSync(dir, { recursive: true });
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
- fs7.writeFileSync(path.join(dir, ".gitignore"), `${lines.join("\n")}
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 = fs7.existsSync(file) ? fs7.readFileSync(file, "utf8") : "";
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
- fs7.writeFileSync(file, content);
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
- fs7.mkdirSync(hooksDir, { recursive: true });
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 = fs7.existsSync(file) ? fs7.readFileSync(file, "utf8") : "#!/bin/sh\n";
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
- fs7.writeFileSync(file, content);
2293
- fs7.chmodSync(file, 493);
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 (fs7.existsSync(hookFile)) {
2922
+ if (fs8.existsSync(hookFile)) {
2298
2923
  const re = new RegExp(`\\n*${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}\\n*`);
2299
- const next2 = fs7.readFileSync(hookFile, "utf8").replace(re, "\n");
2300
- if (next2.trim() === "#!/bin/sh") fs7.rmSync(hookFile);
2301
- else fs7.writeFileSync(hookFile, next2);
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 (fs7.existsSync(attr)) {
2305
- const next2 = fs7.readFileSync(attr, "utf8").split("\n").filter((l) => l.trim() !== ".vibgrate/graph.json merge=vg").join("\n");
2306
- fs7.writeFileSync(attr, next2);
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 = fs7.readFileSync(f.abs, "utf8");
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 fs7.statSync(abs).size;
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 key = `${ecosystem ?? "*"}\0${name.toLowerCase()}`;
2772
- if (seen.has(key)) continue;
2773
- seen.add(key);
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 = fs7__default.readFileSync(p, "utf8");
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 = (key) => key === "mcp" ? "MCP tools" : key === "cli" ? "vg CLI" : key;
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(fs7__default.readFileSync(path__default.join(root, "package.json"), "utf8"));
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 = fs7__default.readFileSync(p, "utf8");
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
- fs7.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
3176
- fs7.writeFileSync(file, content);
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
- fs7.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
3824
+ fs8.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
3200
3825
  let grammarCount = 0;
3201
3826
  for (const { fileName, absPath } of grammarFiles) {
3202
- fs7.copyFileSync(absPath, path.join(outDir, "grammars", fileName));
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 (fs7.existsSync(graphSrc)) {
3208
- fs7.copyFileSync(graphSrc, path.join(outDir, "graph.json"));
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 (fs7.existsSync(cat)) {
3213
- fs7.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
3837
+ if (fs8.existsSync(cat)) {
3838
+ fs8.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
3214
3839
  included.push("vibgrate.lib.json");
3215
- if (fs7.existsSync(libDir(root))) {
3216
- fs7.cpSync(libDir(root), path.join(outDir, "lib"), { recursive: true });
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
- fs7.writeFileSync(path.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
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-BTLZWSNS.js');
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 fs13.stat(absPath);
4107
+ const stat2 = await fs15.stat(absPath);
3364
4108
  if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) return null;
3365
- return await fs13.readFile(absPath, "utf8");
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 key = `${dep.ecosystem}|${dep.package}`;
3395
- if (seenPackages.has(key)) continue;
3396
- seenPackages.add(key);
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(key, evidence);
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 key = `${entry.ecosystem}|${entry.package}`;
3414
- const evidence = evidenceByPackage.get(key) ?? collectPackageImports(input.graph, entry.ecosystem, entry.package);
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 key = `${ecosystem}:${dep.package}:${version}`;
3513
- if (!deduped.has(key)) {
3514
- deduped.set(key, { ecosystem, package: dep.package, version });
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 SKIP_DIRS = /* @__PURE__ */ new Set([
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 = fs7.statSync(artifactPath).mtimeMs;
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 = fs7.readdirSync(dir, { withFileTypes: true });
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 (!SKIP_DIRS.has(e.name) && !e.name.startsWith(".")) walk2(path.join(dir, e.name), depth + 1);
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 = fs7.statSync(abs).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 SKIP_DIRS2 = /* @__PURE__ */ new Set([
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 = fs7.readdirSync(dir, { withFileTypes: true });
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 (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
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 (fs7.statSync(full).size > MAX_FILE_BYTES2) continue;
4190
- content = fs7.readFileSync(full, "utf8");
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
- for (const plan of report.plans) {
4316
- out2.push(planBlock(plan, plan.tier === report.recommended));
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
- for (const plan of report.plans) {
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
- if (plan.upgrades.length) {
4354
- out2.push("| Package | From | To | Kind | Reason |");
4355
- out2.push("|---|---|---|---|---|");
4356
- for (const u of plan.upgrades) {
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 fs13.mkdir(CACHE_DIR, { recursive: true });
4530
- await fs13.writeFile(CACHE_FILE, JSON.stringify(data), "utf-8");
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
- function detectGlobalInstall() {
4537
- const execPath = process.argv[1] || "";
4538
- if (execPath.includes("/lib/node_modules/") || execPath.includes("\\node_modules\\")) {
4539
- if (!execPath.includes(process.cwd())) {
4540
- if (execPath.includes("pnpm")) return "pnpm";
4541
- if (execPath.includes("yarn")) return "yarn";
4542
- if (execPath.includes("bun")) return "bun";
4543
- return "npm";
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 isDevDependency(cwd) {
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 pkg = JSON.parse(raw);
4599
- return Boolean(pkg.devDependencies?.["@vibgrate/cli"]);
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 globalPm = detectGlobalInstall();
4636
- const isGlobal = opts.global || globalPm !== null;
4637
- const pm = opts.pm || (globalPm ?? await detectPackageManager(cwd));
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, "@vibgrate/cli", latest);
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 = await isDevDependency(cwd);
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, "@vibgrate/cli", latest, isDev, { workspaceRoot: true });
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, "@vibgrate/cli", latest, isDev, { workspaceRoot });
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 @vibgrate/cli@${latest}`);
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 key = `${ecosystem}\0${dep.package}`;
4688
- if (seen.has(key)) continue;
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(key);
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 notFixedByRecommended = (response.plans.find((p) => p.tier === "aggressive")?.fixes.bySeverity[sev] ?? 0) - (recommended?.fixes.bySeverity[sev] ?? 0);
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 key = `${ecosystem}\0${dep.package}`;
4908
- (owners.get(key) ?? owners.set(key, /* @__PURE__ */ new Set()).get(key)).add(dir);
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
- chosen = response.plans.find((p) => p.tier === opts.plan);
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 [key, dep] of currentMap.entries()) {
6203
- if (!baseMap.has(key)) {
6204
- added.push(`${key} @ ${dep.resolvedVersion ?? dep.currentSpec}`);
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(key);
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(`${key} ${prevVersion} -> ${nowVersion} (majorsBehind ${prev.majorsBehind ?? "unknown"} -> ${dep.majorsBehind ?? "unknown"})`);
7038
+ changed.push(`${key2} ${prevVersion} -> ${nowVersion} (majorsBehind ${prev.majorsBehind ?? "unknown"} -> ${dep.majorsBehind ?? "unknown"})`);
6212
7039
  }
6213
7040
  }
6214
- for (const [key, dep] of baseMap.entries()) {
6215
- if (!currentMap.has(key)) {
6216
- removed.push(`${key} @ ${dep.resolvedVersion ?? dep.currentSpec}`);
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 (fs7.existsSync(path.resolve(cwd, first))) {
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];