@vibgrate/cli 2026.715.1 → 2026.717.1

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