@vibgrate/cli 2026.711.2 → 2026.717.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { resolveDsn, resolveIngestHost, dashHostForIngestHost, resolveCliInvocation, availableRegionIds, createWorkspaceDsn, writeStoredCredentials, credentialsPath, findGitRoot, ensureGitignored, gitignoreEntryForCredentials, clearStoredCredentials, inventory, uploadScanArtifact, ingestHostForRegionId, dsnCommand, pushCommand, CliError, ExitCode, NPX_INVOCATION, refreshIfStale, driftCount, embeddingsCached, resolveEmbedModel, loadEmbedder, getNodeEmbeddings, queryGraphSemantic, queryGraph, unavailableMessage, recordCliCall, CLI_TOOL_ALIASES, countPending, resolveOne, indexFor, countTokens, shortestPath, renderReport, impactOf, testsToRun, detectRunner, defaultGraphPath, serveStdio, ASSISTANTS, usageError, loadGraph, detectServeLaunch, installAssistant, writeNavigationConfig, uninstallAssistant, coveringTests, FREE_PACK, readScanArtifact, enrichOnline, ECOSYSTEMS, discoverModels, readSavings, readUsage, exportGraph, catalogPath, libDir, stableStringify, buildGraph, writeArtifacts, writeSnapshot, UsageError, ResourceLimitError, epistemicBreakdown, cacheDir, probeFreshness, discover, vibgrateDir, modelCacheInfo, clearModelCache, GraphSource, createServer, assistantById, serializeGraph, resolveLimits, loadCatalog, driftFor, addLibrary, parseDsn as parseDsn$1, publishPrivateLibrary, resolveLib, libId, resolveVersion, readDoc, localPackageDocs, localApiSurface, assessDocQuality, symbolsFromApi, fetchHostedDocsCached, selectForBudget, formatForExt, verifyDeterminism, renderHtml, isModelReady } from './chunk-75ZJYYJE.js';
3
- import { resolvedGrammarFiles, grammarSetVersion, canonicalize } from './chunk-X5YT263H.js';
4
- import { loadAdvancedScanHook, baselineCommand } from './chunk-4IHO6VUL.js';
5
- import { ensureDir, pathExists, writeDefaultConfig, parseDsn, detectVcs, computeRepoFingerprint, resolveRepositoryName, fetchScanPreflight, runCoreScan, VERSION, readJsonFile, writeTextFile, prepareCompressedUpload, computeDriftScore, titleBox, projectTypeToVulnEcosystem, driftBar, gitHistoryAvailable, buildVersionTimelines, findPackageAnyEcosystem, normalizeConstraint, findVersionCrossings, versionSatisfies, PROJECT_TYPE_TO_OSV_ECOSYSTEM, fetchRiskySymbols, brandProgressBar, resolveHead, workingTreeDirty } from './chunk-NNU2PW2H.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 fs6 from 'fs';
9
- import fs6__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
- import { fileURLToPath } from 'url';
12
+ import { fileURLToPath, pathToFileURL } from 'url';
13
13
  import { Command, CommanderError, Option } from 'commander';
14
14
  import { execSync, spawn, spawnSync, execFileSync, execFile } from 'child_process';
15
15
  import * as crypto from 'crypto';
16
16
  import { randomUUID } from 'crypto';
17
17
  import chalk8 from 'chalk';
18
18
  import * as os from 'os';
19
- import * as fs12 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 (!fs6.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
- fs6.mkdirSync(path.dirname(keyPath), { recursive: true });
187
- fs6.writeFileSync(keyPath, kp.privatePem, { mode: 384 });
188
- fs6.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(fs6.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
- fs6.mkdirSync(path.dirname(outPath), { recursive: true });
211
- fs6.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 (!fs6.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(fs6.readFileSync(bundlePath, "utf8"));
236
- const publicKeyPem = opts.pub ? fs6.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
  }
@@ -245,15 +245,15 @@ async function gitCommitInfo(root) {
245
245
  return { sha, shortSha: sha.slice(0, 7), branch, dirty: dirty === true ? true : dirty === false ? false : void 0 };
246
246
  }
247
247
  function gitBranch(root) {
248
- return new Promise((resolve16) => {
248
+ return new Promise((resolve17) => {
249
249
  execFile(
250
250
  "git",
251
251
  ["rev-parse", "--abbrev-ref", "HEAD"],
252
252
  { cwd: root, timeout: 5e3, windowsHide: true, encoding: "utf8" },
253
253
  (err, stdout) => {
254
- if (err) return resolve16(void 0);
254
+ if (err) return resolve17(void 0);
255
255
  const name = String(stdout).trim();
256
- resolve16(name && name !== "HEAD" ? name : void 0);
256
+ resolve17(name && name !== "HEAD" ? name : void 0);
257
257
  }
258
258
  );
259
259
  });
@@ -584,8 +584,8 @@ function writeExport(graph, target) {
584
584
  ExitCode.USAGE_ERROR
585
585
  );
586
586
  }
587
- fs6.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
588
- fs6.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 = fs6.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 = fs6.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
- fs6.mkdirSync(INSTALL_ID_DIR, { recursive: true });
1299
- fs6.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(fs6.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
- fs6.mkdirSync(cacheDir(root), { recursive: true });
1332
- fs6.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 = fs6.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 = fs6.openSync(file, "r");
1348
+ const fd = fs8.openSync(file, "r");
1349
1349
  try {
1350
1350
  const buf = Buffer.alloc(size - start);
1351
- fs6.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
- fs6.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 (!fs6.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 (!fs6.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
@@ -1569,19 +1569,1153 @@ async function serveHttp(graphPath, host, port, opts, freshness) {
1569
1569
  info(c.red(`vg serve: request error: ${err.message}`));
1570
1570
  }
1571
1571
  });
1572
- await new Promise((resolve16) => httpServer.listen(port, host, resolve16));
1572
+ await new Promise((resolve17) => httpServer.listen(port, host, resolve17));
1573
1573
  info(c.dim(`vg \xB7 MCP server on http://${host}:${port}/mcp (read-only, local, ${freshness})`));
1574
1574
  await new Promise(() => {
1575
1575
  });
1576
1576
  }
1577
1577
 
1578
+ // src/lsp/protocol.ts
1579
+ var ErrorCodes = {
1580
+ MethodNotFound: -32601,
1581
+ InternalError: -32603
1582
+ };
1583
+ var MessageReader = class {
1584
+ buf = Buffer.alloc(0);
1585
+ /** Feed a chunk; returns every complete message now available. */
1586
+ push(chunk) {
1587
+ this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
1588
+ const out2 = [];
1589
+ for (; ; ) {
1590
+ const headerEnd = this.buf.indexOf("\r\n\r\n");
1591
+ if (headerEnd === -1) break;
1592
+ const header = this.buf.subarray(0, headerEnd).toString("ascii");
1593
+ const match = /content-length:\s*(\d+)/i.exec(header);
1594
+ if (!match) {
1595
+ this.buf = this.buf.subarray(headerEnd + 4);
1596
+ continue;
1597
+ }
1598
+ const length = Number(match[1]);
1599
+ const bodyStart = headerEnd + 4;
1600
+ if (this.buf.length < bodyStart + length) break;
1601
+ const body = this.buf.subarray(bodyStart, bodyStart + length).toString("utf8");
1602
+ this.buf = this.buf.subarray(bodyStart + length);
1603
+ try {
1604
+ out2.push(JSON.parse(body));
1605
+ } catch {
1606
+ }
1607
+ }
1608
+ return out2;
1609
+ }
1610
+ };
1611
+ var MessageWriter = class {
1612
+ constructor(out2) {
1613
+ this.out = out2;
1614
+ }
1615
+ out;
1616
+ write(msg) {
1617
+ const body = Buffer.from(JSON.stringify(msg), "utf8");
1618
+ this.out.write(`Content-Length: ${body.length}\r
1619
+ \r
1620
+ `);
1621
+ this.out.write(body);
1622
+ }
1623
+ };
1624
+ var Connection = class {
1625
+ reader = new MessageReader();
1626
+ writer;
1627
+ requests = /* @__PURE__ */ new Map();
1628
+ notifications = /* @__PURE__ */ new Map();
1629
+ constructor(input = process.stdin, output = process.stdout) {
1630
+ this.writer = new MessageWriter(output);
1631
+ input.on("data", (chunk) => {
1632
+ for (const msg of this.reader.push(chunk)) void this.dispatch(msg);
1633
+ });
1634
+ }
1635
+ onRequest(method, handler) {
1636
+ this.requests.set(method, handler);
1637
+ }
1638
+ onNotification(method, handler) {
1639
+ this.notifications.set(method, handler);
1640
+ }
1641
+ /** Push a server→client notification (diagnostics, `vibgrate/score`, …). */
1642
+ notify(method, params) {
1643
+ this.writer.write({ jsonrpc: "2.0", method, params });
1644
+ }
1645
+ async dispatch(msg) {
1646
+ if (!msg.method) return;
1647
+ const isRequest = msg.id !== void 0 && msg.id !== null;
1648
+ if (!isRequest) {
1649
+ const handler2 = this.notifications.get(msg.method);
1650
+ if (handler2) {
1651
+ try {
1652
+ await handler2(msg.method, msg.params);
1653
+ } catch {
1654
+ }
1655
+ }
1656
+ return;
1657
+ }
1658
+ const handler = this.requests.get(msg.method);
1659
+ if (!handler) {
1660
+ this.writer.write({
1661
+ jsonrpc: "2.0",
1662
+ id: msg.id,
1663
+ error: { code: ErrorCodes.MethodNotFound, message: `Unhandled method: ${msg.method}` }
1664
+ });
1665
+ return;
1666
+ }
1667
+ try {
1668
+ const result = await handler(msg.method, msg.params);
1669
+ this.writer.write({ jsonrpc: "2.0", id: msg.id, result: result ?? null });
1670
+ } catch (err) {
1671
+ this.writer.write({
1672
+ jsonrpc: "2.0",
1673
+ id: msg.id,
1674
+ error: {
1675
+ code: ErrorCodes.InternalError,
1676
+ message: err instanceof Error ? err.message : "Internal error"
1677
+ }
1678
+ });
1679
+ }
1680
+ }
1681
+ };
1682
+
1683
+ // src/lsp/manifest-positions.ts
1684
+ function manifestKind(filePath) {
1685
+ const base = (filePath.split(/[\\/]/).pop() ?? "").toLowerCase();
1686
+ if (base === "package.json") return "package.json";
1687
+ if (base === "go.mod") return "go.mod";
1688
+ if (base === "requirements.txt" || /^requirements.*\.txt$/.test(base)) return "requirements.txt";
1689
+ if (base === "pyproject.toml") return "pyproject.toml";
1690
+ if (base === "cargo.toml") return "Cargo.toml";
1691
+ if (base === "pom.xml") return "pom.xml";
1692
+ if (base === "build.gradle" || base === "build.gradle.kts") return "build.gradle";
1693
+ if (base === "gemfile") return "Gemfile";
1694
+ if (base === "composer.json") return "composer.json";
1695
+ if (base === "pubspec.yaml") return "pubspec.yaml";
1696
+ if (base === "mix.exs") return "mix.exs";
1697
+ if (base.endsWith(".csproj") || base.endsWith(".fsproj") || base.endsWith(".vbproj")) return "csproj";
1698
+ if (base === "dockerfile" || base.startsWith("dockerfile.")) return "Dockerfile";
1699
+ return "unknown";
1700
+ }
1701
+ function isManifest(filePath) {
1702
+ return manifestKind(filePath) !== "unknown";
1703
+ }
1704
+ function esc(s) {
1705
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1706
+ }
1707
+ function findPackageLine(text, pkg, kind) {
1708
+ const lines = text.split(/\r?\n/);
1709
+ const p = esc(pkg);
1710
+ const patterns = (() => {
1711
+ switch (kind) {
1712
+ case "package.json":
1713
+ case "composer.json":
1714
+ return [new RegExp(`^\\s*"${p}"\\s*:`)];
1715
+ case "go.mod":
1716
+ return [new RegExp(`^\\s*(?:require\\s+)?${p}\\s+v?\\d`)];
1717
+ case "requirements.txt":
1718
+ return [new RegExp(`^\\s*${p}\\s*(?:\\[[^\\]]*\\])?\\s*(?:[=<>!~]|$)`, "i")];
1719
+ case "pyproject.toml":
1720
+ case "Cargo.toml":
1721
+ return [
1722
+ new RegExp(`^\\s*${p}\\s*=`),
1723
+ new RegExp(`^\\s*"${p}\\s*(?:[=<>!~\\[]|")`)
1724
+ ];
1725
+ case "pom.xml":
1726
+ return [new RegExp(`<artifactId>\\s*${p}\\s*</artifactId>`)];
1727
+ case "build.gradle":
1728
+ return [new RegExp(`["']${p}(?:["':])`)];
1729
+ case "Gemfile":
1730
+ return [new RegExp(`^\\s*gem\\s+["']${p}["']`)];
1731
+ case "pubspec.yaml":
1732
+ return [new RegExp(`^\\s*${p}\\s*:`)];
1733
+ case "mix.exs":
1734
+ return [new RegExp(`\\{\\s*:${p}\\s*,`)];
1735
+ case "csproj":
1736
+ return [new RegExp(`Include\\s*=\\s*"${p}"`, "i")];
1737
+ case "Dockerfile":
1738
+ return [new RegExp(`^\\s*FROM\\s+.*${p}`, "i")];
1739
+ default:
1740
+ return [];
1741
+ }
1742
+ })();
1743
+ for (const re of patterns) {
1744
+ for (let i = 0; i < lines.length; i++) {
1745
+ if (re.test(lines[i])) return i;
1746
+ }
1747
+ }
1748
+ return -1;
1749
+ }
1750
+ function findRuntimeLine(text, kind) {
1751
+ const lines = text.split(/\r?\n/);
1752
+ const patterns = (() => {
1753
+ switch (kind) {
1754
+ case "package.json":
1755
+ return [/^\s*"node"\s*:/, /^\s*"engines"\s*:/];
1756
+ case "go.mod":
1757
+ return [/^\s*go\s+\d/];
1758
+ case "pyproject.toml":
1759
+ return [/^\s*(?:requires-python|python)\s*=/];
1760
+ case "pom.xml":
1761
+ return [/<(?:java\.version|maven\.compiler\.(?:source|target|release))>/];
1762
+ case "build.gradle":
1763
+ return [/(?:sourceCompatibility|targetCompatibility|JavaVersion)/];
1764
+ case "csproj":
1765
+ return [/<TargetFrameworks?>/i];
1766
+ case "Dockerfile":
1767
+ return [/^\s*FROM\s+/i];
1768
+ default:
1769
+ return [];
1770
+ }
1771
+ })();
1772
+ for (const re of patterns) {
1773
+ for (let i = 0; i < lines.length; i++) {
1774
+ if (re.test(lines[i])) return i;
1775
+ }
1776
+ }
1777
+ return -1;
1778
+ }
1779
+ function endOfLine(text, line) {
1780
+ const lines = text.split(/\r?\n/);
1781
+ return (lines[line] ?? "").length;
1782
+ }
1783
+ var CACHE_VERSION = "vg-lsp-scan-cache/2";
1784
+ function cachePath(root) {
1785
+ return path.join(cacheDir(root), "lsp-scan.json");
1786
+ }
1787
+ function manifestHash(root) {
1788
+ const files = [];
1789
+ const walk2 = (dir) => {
1790
+ let entries;
1791
+ try {
1792
+ entries = fs8.readdirSync(dir, { withFileTypes: true });
1793
+ } catch {
1794
+ return;
1795
+ }
1796
+ for (const entry of entries) {
1797
+ const abs = path.join(dir, entry.name);
1798
+ if (entry.isDirectory()) {
1799
+ if (SKIP_DIRS.has(entry.name)) continue;
1800
+ walk2(abs);
1801
+ continue;
1802
+ }
1803
+ if (!entry.isFile()) continue;
1804
+ const base = entry.name.toLowerCase();
1805
+ if (!isManifest(abs) && !SKIP_FILES.has(base)) continue;
1806
+ try {
1807
+ const rel3 = path.relative(root, abs).split(path.sep).join("/");
1808
+ files.push({ rel: rel3, hash: hashBytes(fs8.readFileSync(abs)) });
1809
+ } catch {
1810
+ }
1811
+ }
1812
+ };
1813
+ walk2(root);
1814
+ files.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
1815
+ return hashString(canonicalize(files));
1816
+ }
1817
+ function loadScanCache(root, key2) {
1818
+ try {
1819
+ const raw = JSON.parse(fs8.readFileSync(cachePath(root), "utf8"));
1820
+ if (raw.version === CACHE_VERSION && raw.manifestHash === key2.manifestHash && raw.toolVersion === key2.toolVersion && raw.offline === key2.offline && raw.artifact) {
1821
+ return { manifestHash: raw.manifestHash, artifact: raw.artifact };
1822
+ }
1823
+ } catch {
1824
+ }
1825
+ return null;
1826
+ }
1827
+ function writeScanCache(root, key2, artifact) {
1828
+ try {
1829
+ fs8.mkdirSync(cacheDir(root), { recursive: true });
1830
+ fs8.writeFileSync(
1831
+ cachePath(root),
1832
+ stableStringify(
1833
+ { version: CACHE_VERSION, manifestHash: key2.manifestHash, toolVersion: key2.toolVersion, offline: key2.offline, artifact },
1834
+ 0
1835
+ )
1836
+ );
1837
+ } catch {
1838
+ }
1839
+ }
1840
+
1841
+ // src/lsp/graph-query.ts
1842
+ async function runGraphQuery(graph, params, ctx) {
1843
+ switch (params.mode) {
1844
+ case "ask":
1845
+ return runAsk(graph, params, ctx);
1846
+ case "areas":
1847
+ return runAreas(graph, params);
1848
+ case "hubs":
1849
+ return runHubs(graph, params);
1850
+ case "impact":
1851
+ return runImpact(graph, params);
1852
+ case "path":
1853
+ return runPath(graph, params);
1854
+ case "show":
1855
+ return runShow(graph, params);
1856
+ case "tree":
1857
+ return runTree(graph, params);
1858
+ default:
1859
+ return { ok: false, mode: params.mode, error: "bad-request", message: `unknown mode "${String(params.mode)}"` };
1860
+ }
1861
+ }
1862
+ async function runAsk(graph, params, ctx) {
1863
+ const question = (params.question ?? "").trim();
1864
+ if (!question) return { ok: false, mode: "ask", error: "bad-request", message: "question is required" };
1865
+ const budget = params.budget ?? 2e3;
1866
+ const wantSemantic = !!params.semantic && !ctx.offline && ctx.semantic !== false;
1867
+ let result;
1868
+ let mode = "lexical";
1869
+ let note;
1870
+ if (wantSemantic) {
1871
+ let reason;
1872
+ const embedder = await loadEmbedder({ onUnavailable: (r) => reason = r });
1873
+ if (embedder) {
1874
+ const vectors = await getNodeEmbeddings(graph, embedder, ctx.root);
1875
+ result = await queryGraphSemantic(graph, question, { budget, embedder, nodeVectors: vectors });
1876
+ mode = `semantic (${embedder.id})`;
1877
+ } else {
1878
+ result = queryGraph(graph, question, { budget });
1879
+ note = reason ? unavailableMessage(reason) : "semantic unavailable; used lexical";
1880
+ }
1881
+ } else {
1882
+ result = queryGraph(graph, question, { budget });
1883
+ if (params.semantic && ctx.semantic === false) note = "semantic search is turned off \u2014 used lexical";
1884
+ else if (params.semantic && ctx.offline) note = "semantic skipped \u2014 Vibgrate is running offline";
1885
+ }
1886
+ return {
1887
+ ok: true,
1888
+ mode: "ask",
1889
+ data: {
1890
+ question: result.question,
1891
+ mode,
1892
+ note,
1893
+ tokensEstimate: result.tokensEstimate,
1894
+ matches: result.matches.map((m) => ({
1895
+ name: m.node.qualifiedName,
1896
+ kind: m.node.kind,
1897
+ file: m.node.file,
1898
+ line: m.node.span.start,
1899
+ score: m.score,
1900
+ why: m.why
1901
+ })),
1902
+ context: result.context
1903
+ }
1904
+ };
1905
+ }
1906
+ function runAreas(graph, params) {
1907
+ const limit = params.limit ?? 30;
1908
+ const list = [...graph.areas].sort((a, b) => b.size - a.size || a.id - b.id).slice(0, limit);
1909
+ return { ok: true, mode: "areas", data: list };
1910
+ }
1911
+ function runHubs(graph, params) {
1912
+ const limit = params.limit ?? 20;
1913
+ const list = graph.nodes.filter((n) => n.kind !== "file" && n.kind !== "external").sort((a, b) => b.importance - a.importance || a.qualifiedName.localeCompare(b.qualifiedName)).slice(0, limit).map(nodeSummary2);
1914
+ return { ok: true, mode: "hubs", data: list };
1915
+ }
1916
+ function nodeSummary2(n) {
1917
+ return {
1918
+ id: n.id,
1919
+ name: n.qualifiedName,
1920
+ kind: n.kind,
1921
+ file: n.file,
1922
+ line: n.span.start,
1923
+ importance: n.importance,
1924
+ isHub: n.isHub,
1925
+ area: n.area
1926
+ };
1927
+ }
1928
+ function resolveOrError(graph, name, mode) {
1929
+ const { node, candidates } = resolveOne(graph, name);
1930
+ if (node) return { node };
1931
+ if (candidates.length === 0) {
1932
+ return { ok: false, mode, error: "not-found", message: `no node matches "${name}"` };
1933
+ }
1934
+ return {
1935
+ ok: false,
1936
+ mode,
1937
+ error: "ambiguous",
1938
+ message: `"${name}" is ambiguous`,
1939
+ candidates: candidates.slice(0, 10).map((n) => ({ name: n.qualifiedName, kind: n.kind, file: n.file, line: n.span.start }))
1940
+ };
1941
+ }
1942
+ function runImpact(graph, params) {
1943
+ const name = (params.name ?? "").trim();
1944
+ if (!name) return { ok: false, mode: "impact", error: "bad-request", message: "name is required" };
1945
+ const resolved = resolveOrError(graph, name, "impact");
1946
+ if (!("node" in resolved)) return resolved;
1947
+ const result = impactOf(graph, resolved.node.id, { depth: params.depth ?? 4 });
1948
+ return { ok: true, mode: "impact", data: result };
1949
+ }
1950
+ function runPath(graph, params) {
1951
+ const a = (params.a ?? "").trim();
1952
+ const b = (params.b ?? "").trim();
1953
+ if (!a || !b) return { ok: false, mode: "path", error: "bad-request", message: "a and b are required" };
1954
+ const ra = resolveOrError(graph, a, "path");
1955
+ if (!("node" in ra)) return ra;
1956
+ const rb = resolveOrError(graph, b, "path");
1957
+ if (!("node" in rb)) return rb;
1958
+ const result = shortestPath(graph, ra.node.id, rb.node.id);
1959
+ if (!result) {
1960
+ return {
1961
+ ok: false,
1962
+ mode: "path",
1963
+ error: "not-found",
1964
+ message: `no path between ${ra.node.qualifiedName} and ${rb.node.qualifiedName}`
1965
+ };
1966
+ }
1967
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
1968
+ const names = result.ids.map((id) => byId.get(id)?.qualifiedName ?? id);
1969
+ return {
1970
+ ok: true,
1971
+ mode: "path",
1972
+ data: { from: ra.node.qualifiedName, to: rb.node.qualifiedName, hops: names.length - 1, direction: result.direction, path: names }
1973
+ };
1974
+ }
1975
+ function runShow(graph, params) {
1976
+ const name = (params.name ?? "").trim();
1977
+ if (!name) return { ok: false, mode: "show", error: "bad-request", message: "name is required" };
1978
+ const resolved = resolveOrError(graph, name, "show");
1979
+ if (!("node" in resolved)) return resolved;
1980
+ const node = resolved.node;
1981
+ const index = indexFor(graph);
1982
+ const callees = dedupeNodes(index.callees(node.id).map((x) => x.node));
1983
+ const callers = dedupeNodes(index.callers(node.id).map((x) => x.node));
1984
+ const extendsEdges = index.out(node.id, "extends").concat(index.out(node.id, "implements"));
1985
+ const supertypes = extendsEdges.map((e) => index.node(e.dst)?.qualifiedName).filter(Boolean);
1986
+ const area = graph.areas.find((a) => a.id === node.area);
1987
+ return {
1988
+ ok: true,
1989
+ mode: "show",
1990
+ data: {
1991
+ id: node.id,
1992
+ name: node.qualifiedName,
1993
+ kind: node.kind,
1994
+ file: node.file,
1995
+ line: node.span.start,
1996
+ signature: node.signature ?? null,
1997
+ importance: node.importance,
1998
+ centrality: node.centrality,
1999
+ isHub: node.isHub,
2000
+ area: node.area,
2001
+ areaLabel: area?.label ?? null,
2002
+ tested: node.tested,
2003
+ calls: callees.map((n) => n.qualifiedName),
2004
+ calledBy: callers.map((n) => n.qualifiedName),
2005
+ extends: supertypes
2006
+ }
2007
+ };
2008
+ }
2009
+ function dedupeNodes(nodes) {
2010
+ const seen = /* @__PURE__ */ new Set();
2011
+ const out2 = [];
2012
+ for (const n of nodes) {
2013
+ if (seen.has(n.id)) continue;
2014
+ seen.add(n.id);
2015
+ out2.push(n);
2016
+ }
2017
+ return out2;
2018
+ }
2019
+ function runTree(graph, params) {
2020
+ const name = (params.name ?? "").trim();
2021
+ if (!name) return { ok: false, mode: "tree", error: "bad-request", message: "name is required" };
2022
+ const resolved = resolveOrError(graph, name, "tree");
2023
+ if (!("node" in resolved)) return resolved;
2024
+ const index = indexFor(graph);
2025
+ const direction = params.callers ? "callers" : "callees";
2026
+ const maxDepth = Math.max(1, params.depth ?? 3);
2027
+ const data = toJsonTree(index, resolved.node, direction, maxDepth, /* @__PURE__ */ new Set());
2028
+ return { ok: true, mode: "tree", data };
2029
+ }
2030
+
2031
+ // src/lsp/enrich.ts
2032
+ var OSV_QUERY_URL = "https://api.osv.dev/v1/query";
2033
+ var cache = /* @__PURE__ */ new Map();
2034
+ function key(ecosystem, name, version) {
2035
+ return `${ecosystem}:${name}@${version}`;
2036
+ }
2037
+ function severityOf(v) {
2038
+ const dbSpecific = v.database_specific;
2039
+ const label = typeof dbSpecific?.severity === "string" ? dbSpecific.severity.toLowerCase() : "";
2040
+ if (label.includes("critical")) return "critical";
2041
+ if (label.includes("high")) return "high";
2042
+ if (label.includes("moderate") || label.includes("medium")) return "moderate";
2043
+ if (label.includes("low")) return "low";
2044
+ const sev = Array.isArray(v.severity) ? v.severity : [];
2045
+ const cvss = sev.find((s) => typeof s.score === "string" && String(s.type).startsWith("CVSS"));
2046
+ if (cvss) {
2047
+ const m = /\/AV:.*$/.test(String(cvss.score)) ? null : Number(cvss.score);
2048
+ if (m != null && !Number.isNaN(m)) {
2049
+ if (m >= 9) return "critical";
2050
+ if (m >= 7) return "high";
2051
+ if (m >= 4) return "moderate";
2052
+ if (m > 0) return "low";
2053
+ }
2054
+ }
2055
+ return "unknown";
2056
+ }
2057
+ function toSummary(raw) {
2058
+ const v = raw ?? {};
2059
+ const aliases = Array.isArray(v.aliases) ? v.aliases.map(String) : [];
2060
+ const details = typeof v.details === "string" ? v.details : "";
2061
+ const summary = typeof v.summary === "string" && v.summary || (details ? details.split("\n")[0].slice(0, 140) : "") || "Advisory";
2062
+ return {
2063
+ id: String(v.id ?? "unknown"),
2064
+ cves: aliases.filter((a) => a.startsWith("CVE-")),
2065
+ summary,
2066
+ severity: severityOf(v)
2067
+ };
2068
+ }
2069
+ async function enrichVulns(ecosystem, name, version, opts = { offline: false }) {
2070
+ if (!ecosystem || !version) return { vulns: [], cveCount: 0, source: "error" };
2071
+ if (opts.offline) return { vulns: [], cveCount: 0, source: "offline" };
2072
+ const k = key(ecosystem, name, version);
2073
+ const cached = cache.get(k);
2074
+ if (cached) return { ...cached, source: "cache" };
2075
+ try {
2076
+ const controller = new AbortController();
2077
+ const timer = setTimeout(() => controller.abort(), 8e3);
2078
+ const res = await fetch(OSV_QUERY_URL, {
2079
+ method: "POST",
2080
+ headers: { "content-type": "application/json" },
2081
+ body: JSON.stringify({ version, package: { name, ecosystem } }),
2082
+ signal: controller.signal
2083
+ }).finally(() => clearTimeout(timer));
2084
+ if (!res.ok) return { vulns: [], cveCount: 0, source: "error" };
2085
+ const data = await res.json();
2086
+ const vulns = (data.vulns ?? []).map(toSummary);
2087
+ const cveCount = new Set(vulns.flatMap((v) => v.cves)).size;
2088
+ const result = { vulns, cveCount, source: "osv" };
2089
+ cache.set(k, result);
2090
+ return result;
2091
+ } catch {
2092
+ return { vulns: [], cveCount: 0, source: "error" };
2093
+ }
2094
+ }
2095
+
2096
+ // src/lsp/server.ts
2097
+ function bandForDependency(dep) {
2098
+ if (dep.drift === "major-behind") {
2099
+ return (dep.majorsBehind ?? 1) >= 2 ? "high" : "moderate";
2100
+ }
2101
+ if (dep.drift === "minor-behind") return "moderate";
2102
+ return "low";
2103
+ }
2104
+ function severityFor(level) {
2105
+ if (level === "note") return 3;
2106
+ return 2;
2107
+ }
2108
+ var DIAGNOSTIC_RULES = /* @__PURE__ */ new Set([
2109
+ "vibgrate/runtime-eol",
2110
+ "vibgrate/runtime-lag",
2111
+ "vibgrate/unmaintained",
2112
+ "vibgrate/abandoned",
2113
+ "vibgrate/license-change",
2114
+ "vibgrate/eol"
2115
+ ]);
2116
+ var VibgrateLanguageServer = class {
2117
+ constructor(opts, output = process.stdout) {
2118
+ this.opts = opts;
2119
+ this.conn = new Connection(process.stdin, output);
2120
+ this.register();
2121
+ }
2122
+ opts;
2123
+ conn;
2124
+ docs = /* @__PURE__ */ new Map();
2125
+ // uri → text
2126
+ artifact = null;
2127
+ scanning = false;
2128
+ queued = false;
2129
+ debounce = null;
2130
+ shuttingDown = false;
2131
+ graph = null;
2132
+ graphBuilding = false;
2133
+ register() {
2134
+ this.conn.onRequest("initialize", () => ({
2135
+ capabilities: {
2136
+ // 1 = Full sync. We only actually act on save, but full sync keeps our
2137
+ // buffer honest so hovers land on the text the user is looking at.
2138
+ textDocumentSync: 1,
2139
+ hoverProvider: true,
2140
+ codeLensProvider: { resolveProvider: false },
2141
+ executeCommandProvider: {
2142
+ commands: ["vibgrate.rescan", "vibgrate.explain", "vibgrate.routeFix"]
2143
+ }
2144
+ },
2145
+ serverInfo: { name: "Vibgrate", version: VERSION }
2146
+ }));
2147
+ this.conn.onNotification("initialized", () => {
2148
+ this.scheduleScan(0);
2149
+ void this.ensureGraph();
2150
+ });
2151
+ this.conn.onNotification("textDocument/didOpen", (_m, params) => {
2152
+ const p = params;
2153
+ const uri = p.textDocument?.uri;
2154
+ if (!uri || !isManifest(uriToPath(uri))) return;
2155
+ this.docs.set(uri, p.textDocument?.text ?? "");
2156
+ if (this.artifact) this.publishForDoc(uri);
2157
+ else this.scheduleScan(0);
2158
+ });
2159
+ this.conn.onNotification("textDocument/didChange", (_m, params) => {
2160
+ const p = params;
2161
+ const uri = p.textDocument?.uri;
2162
+ if (!uri || !this.docs.has(uri)) return;
2163
+ const full = p.contentChanges?.[p.contentChanges.length - 1]?.text;
2164
+ if (typeof full === "string") this.docs.set(uri, full);
2165
+ if (this.artifact) this.publishForDoc(uri);
2166
+ });
2167
+ this.conn.onNotification("textDocument/didSave", (_m, params) => {
2168
+ const p = params;
2169
+ const uri = p.textDocument?.uri;
2170
+ if (!uri || !this.docs.has(uri)) return;
2171
+ this.scheduleScan(400);
2172
+ });
2173
+ this.conn.onNotification("textDocument/didClose", (_m, params) => {
2174
+ const p = params;
2175
+ if (p.textDocument?.uri) this.docs.delete(p.textDocument.uri);
2176
+ });
2177
+ this.conn.onRequest("textDocument/hover", (_m, params) => this.onHover(params));
2178
+ this.conn.onRequest("textDocument/codeLens", (_m, params) => this.onCodeLens(params));
2179
+ this.conn.onRequest("vibgrate/graph/query", (_m, params) => this.onGraphQuery(params));
2180
+ this.conn.onRequest("vibgrate/score/forFile", (_m, params) => this.onScoreForFile(params));
2181
+ this.conn.onRequest("vibgrate/enrich", (_m, params) => this.onEnrich(params));
2182
+ this.conn.onRequest("workspace/executeCommand", (_m, params) => {
2183
+ const p = params;
2184
+ if (p.command === "vibgrate.rescan") {
2185
+ this.scheduleScan(0);
2186
+ return { ok: true };
2187
+ }
2188
+ return null;
2189
+ });
2190
+ this.conn.onRequest("shutdown", () => {
2191
+ this.shuttingDown = true;
2192
+ if (this.debounce) clearTimeout(this.debounce);
2193
+ return null;
2194
+ });
2195
+ this.conn.onNotification("exit", () => {
2196
+ process.exit(this.shuttingDown ? 0 : 1);
2197
+ });
2198
+ }
2199
+ // ── Scanning ─────────────────────────────────────────────────────────────
2200
+ scheduleScan(delayMs) {
2201
+ if (this.debounce) clearTimeout(this.debounce);
2202
+ this.debounce = setTimeout(() => void this.scan(), delayMs);
2203
+ }
2204
+ async scan() {
2205
+ if (this.scanning) {
2206
+ this.queued = true;
2207
+ return;
2208
+ }
2209
+ this.scanning = true;
2210
+ try {
2211
+ const cacheKey = { manifestHash: manifestHash(this.opts.root), toolVersion: VERSION, offline: this.opts.offline };
2212
+ const cached = loadScanCache(this.opts.root, cacheKey);
2213
+ if (cached) {
2214
+ this.artifact = cached.artifact;
2215
+ } else {
2216
+ const scanOpts = {
2217
+ format: "json",
2218
+ concurrency: 8,
2219
+ offline: this.opts.offline,
2220
+ noLocalArtifacts: true,
2221
+ // an editor scan must not litter the repo
2222
+ noGraph: true,
2223
+ // the code map is not needed to score drift; skip the cost
2224
+ quiet: true,
2225
+ // no spinners, no artifact dump — an editor is not a terminal
2226
+ vibgrateVersion: VERSION
2227
+ };
2228
+ const advanced = await loadAdvancedScanHook();
2229
+ this.artifact = await runCoreScan(this.opts.root, scanOpts, advanced);
2230
+ writeScanCache(this.opts.root, cacheKey, this.artifact);
2231
+ }
2232
+ this.publishScore();
2233
+ for (const uri of this.docs.keys()) this.publishForDoc(uri);
2234
+ } catch (err) {
2235
+ this.conn.notify("window/logMessage", {
2236
+ type: 3,
2237
+ message: `Vibgrate scan failed: ${err instanceof Error ? err.message : String(err)}`
2238
+ });
2239
+ this.conn.notify("vibgrate/status", { state: "error" });
2240
+ } finally {
2241
+ this.scanning = false;
2242
+ if (this.queued) {
2243
+ this.queued = false;
2244
+ this.scheduleScan(0);
2245
+ }
2246
+ }
2247
+ }
2248
+ // ── Publishing ───────────────────────────────────────────────────────────
2249
+ publishScore() {
2250
+ const a = this.artifact;
2251
+ if (!a) return;
2252
+ let behind = 0;
2253
+ let unmaintained = 0;
2254
+ let total = 0;
2255
+ for (const proj of a.projects ?? []) {
2256
+ for (const dep of proj.dependencies ?? []) {
2257
+ total++;
2258
+ if (dep.drift === "major-behind" || dep.drift === "minor-behind") behind++;
2259
+ }
2260
+ }
2261
+ const eol = (a.findings ?? []).filter(
2262
+ (f) => f.ruleId === "vibgrate/runtime-eol" || f.ruleId === "vibgrate/eol"
2263
+ ).length;
2264
+ unmaintained = (a.findings ?? []).filter(
2265
+ (f) => f.ruleId === "vibgrate/unmaintained" || f.ruleId === "vibgrate/abandoned"
2266
+ ).length;
2267
+ const band = a.drift.riskLevel ?? "low";
2268
+ const payload = {
2269
+ score: a.drift.score,
2270
+ band,
2271
+ // v3 §2.4: `estimated` means "no timestamps at all" — it is NOT an
2272
+ // offline marker. An air-gapped scan against a dated snapshot is Verified.
2273
+ // The score now carries the authoritative provenance (driftscore-3.0
2274
+ // envelope); fall back to the artifact heuristic for pre-v3 engines.
2275
+ mode: a.drift.mode ?? (hasReleaseDates(a) ? "verified" : "estimated"),
2276
+ methodology: a.drift.methodologyVersion ?? "unknown",
2277
+ scale: "0 best, 100 worst",
2278
+ counts: { behind, eol, unmaintained, total },
2279
+ rootPath: a.rootPath,
2280
+ scannedAt: a.timestamp,
2281
+ ...buildBreakdown(a.drift, a.projects ?? []) ? { breakdown: buildBreakdown(a.drift, a.projects ?? []) } : {}
2282
+ };
2283
+ this.conn.notify("vibgrate/score", payload);
2284
+ }
2285
+ /** Decorations + (optionally) diagnostics for one open manifest. */
2286
+ publishForDoc(uri) {
2287
+ const a = this.artifact;
2288
+ const text = this.docs.get(uri);
2289
+ if (!a || text === void 0) return;
2290
+ const filePath = uriToPath(uri);
2291
+ const kind = manifestKind(filePath);
2292
+ const project = projectForFile(this.opts.root, a, filePath);
2293
+ const items = [];
2294
+ for (const dep of project?.dependencies ?? []) {
2295
+ if (dep.drift === "current" || dep.drift === "unknown") continue;
2296
+ const line = findPackageLine(text, dep.package, kind);
2297
+ if (line === -1) continue;
2298
+ items.push({
2299
+ line,
2300
+ endCol: endOfLine(text, line),
2301
+ text: inlineLabel(dep),
2302
+ band: bandForDependency(dep),
2303
+ package: dep.package
2304
+ });
2305
+ }
2306
+ this.conn.notify("vibgrate/inline", { uri, items });
2307
+ if (!this.opts.diagnostics) {
2308
+ this.conn.notify("textDocument/publishDiagnostics", { uri, diagnostics: [] });
2309
+ return;
2310
+ }
2311
+ const diagnostics = (a.findings ?? []).filter((f) => DIAGNOSTIC_RULES.has(f.ruleId)).filter((f) => project && sameProject(f.location, project.path)).map((f) => {
2312
+ const line = Math.max(0, findRuntimeLine(text, kind));
2313
+ return {
2314
+ range: {
2315
+ start: { line, character: 0 },
2316
+ end: { line, character: endOfLine(text, line) }
2317
+ },
2318
+ severity: severityFor(f.level),
2319
+ // never 1 — see file header
2320
+ source: "vibgrate",
2321
+ code: f.ruleId,
2322
+ message: f.message
2323
+ };
2324
+ });
2325
+ this.conn.notify("textDocument/publishDiagnostics", { uri, diagnostics });
2326
+ }
2327
+ // ── Requests ─────────────────────────────────────────────────────────────
2328
+ onHover(params) {
2329
+ const p = params;
2330
+ const uri = p.textDocument?.uri;
2331
+ const line = p.position?.line;
2332
+ const a = this.artifact;
2333
+ const text = uri ? this.docs.get(uri) : void 0;
2334
+ if (!uri || line === void 0 || !a || text === void 0) return null;
2335
+ const filePath = uriToPath(uri);
2336
+ const kind = manifestKind(filePath);
2337
+ const project = projectForFile(this.opts.root, a, filePath);
2338
+ const dep = (project?.dependencies ?? []).find(
2339
+ (d) => findPackageLine(text, d.package, kind) === line
2340
+ );
2341
+ if (!dep) return null;
2342
+ const contribution = a.drift.dependencyDrift?.top.find((t) => t.package === dep.package)?.drift ?? null;
2343
+ return {
2344
+ contents: { kind: "markdown", value: hoverMarkdown(dep, contribution) }
2345
+ };
2346
+ }
2347
+ onCodeLens(params) {
2348
+ const p = params;
2349
+ const uri = p.textDocument?.uri;
2350
+ const a = this.artifact;
2351
+ if (!uri || !a || !this.docs.has(uri)) return [];
2352
+ const behind = (a.projects ?? []).reduce(
2353
+ (n, proj) => n + (proj.dependencies ?? []).filter(
2354
+ (d) => d.drift === "major-behind" || d.drift === "minor-behind"
2355
+ ).length,
2356
+ 0
2357
+ );
2358
+ const eol = (a.findings ?? []).filter(
2359
+ (f) => f.ruleId === "vibgrate/runtime-eol" || f.ruleId === "vibgrate/eol"
2360
+ ).length;
2361
+ const estimated = (a.drift.mode ?? (hasReleaseDates(a) ? "verified" : "estimated")) === "estimated";
2362
+ const mode = estimated ? "~" : "";
2363
+ const title = `Vibgrate \xB7 drift ${mode}${a.drift.score} (${a.drift.riskLevel}) \xB7 ${behind} behind \xB7 ${eol} EOL`;
2364
+ return [
2365
+ {
2366
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
2367
+ command: { title, command: "vibgrate.openPanel", arguments: [] }
2368
+ }
2369
+ ];
2370
+ }
2371
+ // ── Graph ────────────────────────────────────────────────────────────────
2372
+ /**
2373
+ * Load the committed map if one exists, else build it once (this is the
2374
+ * "first activation sets up the graph" step). Fire-and-forget from
2375
+ * `initialized` so it never blocks the drift score; a query that arrives
2376
+ * mid-build awaits the same in-flight promise via the `graphBuilding` guard.
2377
+ */
2378
+ async ensureGraph() {
2379
+ if (!this.opts.graph) {
2380
+ this.conn.notify("vibgrate/graph/status", { state: "disabled" });
2381
+ return;
2382
+ }
2383
+ if (this.graph || this.graphBuilding) return;
2384
+ this.graphBuilding = true;
2385
+ try {
2386
+ const existing = loadGraph(this.opts.root);
2387
+ if (existing) {
2388
+ this.graph = existing;
2389
+ this.conn.notify("vibgrate/graph/status", { state: "ready" });
2390
+ return;
2391
+ }
2392
+ this.conn.notify("vibgrate/graph/status", { state: "building" });
2393
+ const result = await buildGraph({ root: this.opts.root });
2394
+ writeArtifacts(result.graph, { root: this.opts.root, html: false, report: false });
2395
+ writeSnapshot(this.opts.root, result.graph.provenance.corpusHash, result.fileStats, {});
2396
+ this.graph = result.graph;
2397
+ this.conn.notify("vibgrate/graph/status", { state: "ready" });
2398
+ } catch (err) {
2399
+ const message = err instanceof Error ? err.message : String(err);
2400
+ this.conn.notify("window/logMessage", { type: 3, message: `Vibgrate graph build failed: ${message}` });
2401
+ this.conn.notify("vibgrate/graph/status", { state: "error", message });
2402
+ } finally {
2403
+ this.graphBuilding = false;
2404
+ }
2405
+ }
2406
+ /** The graph, refreshed if the working tree drifted since the last build (same probe `vg ask` uses). */
2407
+ async graphForQuery() {
2408
+ await this.ensureGraph();
2409
+ if (!this.graph) return null;
2410
+ const refreshed = await refreshIfStale(this.opts.root);
2411
+ if (refreshed.status === "refreshed" && refreshed.wrote) {
2412
+ const reloaded = loadGraph(this.opts.root);
2413
+ if (reloaded) this.graph = reloaded;
2414
+ }
2415
+ return this.graph;
2416
+ }
2417
+ async onGraphQuery(params) {
2418
+ const p = params;
2419
+ if (!this.opts.graph) {
2420
+ return { ok: false, mode: p.mode, error: "disabled", message: "the local Vibgrate Graph is turned off" };
2421
+ }
2422
+ const graph = await this.graphForQuery();
2423
+ if (!graph) {
2424
+ return { ok: false, mode: p.mode, error: "not-found", message: "no code map yet \u2014 it is still building" };
2425
+ }
2426
+ return runGraphQuery(graph, p, { root: this.opts.root, offline: this.opts.offline, semantic: this.opts.semantic });
2427
+ }
2428
+ /**
2429
+ * `vibgrate/enrich` — lazy vulnerability enrichment for one expanded package,
2430
+ * straight from OSV.dev (never our API). Honours the server's offline flag.
2431
+ */
2432
+ onEnrich(params) {
2433
+ const p = params ?? {};
2434
+ return enrichVulns(p.ecosystem ?? null, p.package ?? "", p.version ?? null, { offline: this.opts.offline });
2435
+ }
2436
+ onScoreForFile(params) {
2437
+ const p = params;
2438
+ const a = this.artifact;
2439
+ if (!a || !p.uri) return null;
2440
+ const project = projectForFile(this.opts.root, a, uriToPath(p.uri));
2441
+ return project ? scoreForProject(a, project) : null;
2442
+ }
2443
+ };
2444
+ function uriToPath(uri) {
2445
+ try {
2446
+ return fileURLToPath(uri);
2447
+ } catch {
2448
+ return uri;
2449
+ }
2450
+ }
2451
+ function projectForFile(root, a, filePath) {
2452
+ const dir = path.dirname(path.resolve(filePath));
2453
+ let best;
2454
+ let bestLen = -1;
2455
+ for (const proj of a.projects ?? []) {
2456
+ const projDir = path.resolve(root, proj.path);
2457
+ if ((dir === projDir || dir.startsWith(projDir + path.sep)) && projDir.length > bestLen) {
2458
+ best = proj;
2459
+ bestLen = projDir.length;
2460
+ }
2461
+ }
2462
+ return best;
2463
+ }
2464
+ function sameProject(location, projectPath) {
2465
+ return path.normalize(location) === path.normalize(projectPath);
2466
+ }
2467
+ function hasReleaseDates(a) {
2468
+ for (const proj of a.projects ?? []) {
2469
+ for (const dep of proj.dependencies ?? []) {
2470
+ if (dep.ageDays !== null && dep.ageDays !== void 0) return true;
2471
+ }
2472
+ }
2473
+ return false;
2474
+ }
2475
+ function scoreForProject(a, proj) {
2476
+ if (!proj.drift) return null;
2477
+ let behind = 0;
2478
+ let total = 0;
2479
+ for (const dep of proj.dependencies ?? []) {
2480
+ total++;
2481
+ if (dep.drift === "major-behind" || dep.drift === "minor-behind") behind++;
2482
+ }
2483
+ const projFindings = (a.findings ?? []).filter((f) => sameProject(f.location, proj.path));
2484
+ const eol = projFindings.filter((f) => f.ruleId === "vibgrate/runtime-eol" || f.ruleId === "vibgrate/eol").length;
2485
+ const unmaintained = projFindings.filter(
2486
+ (f) => f.ruleId === "vibgrate/unmaintained" || f.ruleId === "vibgrate/abandoned"
2487
+ ).length;
2488
+ const hasDates = (proj.dependencies ?? []).some((d) => d.ageDays !== null && d.ageDays !== void 0);
2489
+ return {
2490
+ score: proj.drift.score,
2491
+ band: proj.drift.riskLevel ?? "low",
2492
+ mode: proj.drift.mode ?? (hasDates ? "verified" : "estimated"),
2493
+ methodology: proj.drift.methodologyVersion ?? a.drift.methodologyVersion ?? "unknown",
2494
+ scale: "0 best, 100 worst",
2495
+ counts: { behind, eol, unmaintained, total },
2496
+ rootPath: proj.path,
2497
+ scannedAt: a.timestamp,
2498
+ ...buildBreakdown(proj.drift, [proj]) ? { breakdown: buildBreakdown(proj.drift, [proj]) } : {}
2499
+ };
2500
+ }
2501
+ function bandForScore(n) {
2502
+ if (n <= 30) return "low";
2503
+ if (n <= 60) return "moderate";
2504
+ return "high";
2505
+ }
2506
+ function buildBreakdown(drift, projects) {
2507
+ const top = drift.dependencyDrift?.top;
2508
+ if (!top || top.length === 0) return void 0;
2509
+ const byPkg = /* @__PURE__ */ new Map();
2510
+ for (const p of projects) {
2511
+ const ecosystem = osvEcosystem(p.type);
2512
+ for (const d of p.dependencies ?? []) if (!byPkg.has(d.package)) byPkg.set(d.package, { row: d, ecosystem });
2513
+ }
2514
+ const items = top.filter((t) => t.drift > 0).map((t) => {
2515
+ const found = byPkg.get(t.package);
2516
+ const row = found?.row;
2517
+ return {
2518
+ package: t.package,
2519
+ section: row?.section ?? "dependencies",
2520
+ ecosystem: found?.ecosystem ?? null,
2521
+ drift: t.drift,
2522
+ band: bandForScore(t.drift),
2523
+ currentSpec: row?.currentSpec ?? "",
2524
+ resolvedVersion: row?.resolvedVersion ?? null,
2525
+ latestStable: row?.latestStable ?? null,
2526
+ majorsBehind: row?.majorsBehind ?? null,
2527
+ ageDays: row?.ageDays ?? null,
2528
+ mode: t.mode,
2529
+ unsupported: t.unsupported,
2530
+ abandoned: t.flags.includes("abandoned-floor"),
2531
+ flags: t.flags
2532
+ };
2533
+ });
2534
+ return items.length > 0 ? items : void 0;
2535
+ }
2536
+ function osvEcosystem(type) {
2537
+ switch (type) {
2538
+ case "node":
2539
+ case "typescript":
2540
+ return "npm";
2541
+ case "python":
2542
+ return "PyPI";
2543
+ case "java":
2544
+ case "kotlin":
2545
+ case "scala":
2546
+ case "groovy":
2547
+ return "Maven";
2548
+ case "go":
2549
+ return "Go";
2550
+ case "rust":
2551
+ return "crates.io";
2552
+ case "ruby":
2553
+ return "RubyGems";
2554
+ case "php":
2555
+ return "Packagist";
2556
+ case "dotnet":
2557
+ return "NuGet";
2558
+ case "dart":
2559
+ return "Pub";
2560
+ case "swift":
2561
+ return "SwiftURL";
2562
+ case "elixir":
2563
+ return "Hex";
2564
+ default:
2565
+ return null;
2566
+ }
2567
+ }
2568
+ function inlineLabel(dep) {
2569
+ const bits = [];
2570
+ if (dep.drift === "major-behind") {
2571
+ const n = dep.majorsBehind ?? 1;
2572
+ bits.push(`${n} major behind`);
2573
+ } else if (dep.drift === "minor-behind") {
2574
+ bits.push("minor behind");
2575
+ }
2576
+ if (dep.latestStable) bits.push(dep.latestStable);
2577
+ if (dep.ageDays != null && dep.ageDays >= 1) {
2578
+ bits.push(`${Math.round(dep.ageDays).toLocaleString("en-US")}d stale`);
2579
+ }
2580
+ return bits.join(" \xB7 ");
2581
+ }
2582
+ function commandUri(command, args) {
2583
+ return `command:${command}?${encodeURIComponent(JSON.stringify([args]))}`;
2584
+ }
2585
+ function driftBadge(dep) {
2586
+ if (dep.drift === "major-behind") {
2587
+ const n = dep.majorsBehind ?? 1;
2588
+ return `${n} major${n === 1 ? "" : "s"} behind`;
2589
+ }
2590
+ if (dep.drift === "minor-behind") return "minor behind";
2591
+ return null;
2592
+ }
2593
+ function versionJourney(yours, latest) {
2594
+ const from = Number(/^(\d+)/.exec(yours)?.[1]);
2595
+ const to = Number(/^(\d+)/.exec(latest)?.[1]);
2596
+ if (!Number.isFinite(from) || !Number.isFinite(to) || to <= from) return null;
2597
+ const majors = [];
2598
+ for (let m = from; m <= to; m++) majors.push(m);
2599
+ const shown = majors.length > 5 ? [majors[0], majors[1], -1, majors[majors.length - 2], majors[majors.length - 1]] : majors;
2600
+ return shown.map((m) => m === -1 ? "\u2026" : `${m}.x`).join(" \u2192 ");
2601
+ }
2602
+ function hoverMarkdown(dep, contribution) {
2603
+ const lines = [];
2604
+ const badge = driftBadge(dep);
2605
+ const contributionText = contribution != null ? ` \xB7 contributes +${Math.round(contribution)} to drift` : "";
2606
+ lines.push(badge ? `**${dep.package}** \`${badge}\`${contributionText}` : `**${dep.package}**`);
2607
+ lines.push("");
2608
+ const yours = dep.resolvedVersion ?? dep.currentSpec;
2609
+ const drifted = dep.drift === "major-behind" || dep.drift === "minor-behind";
2610
+ if (dep.latestStable && dep.latestStable !== yours) {
2611
+ lines.push("**Currency**");
2612
+ lines.push(`| | |`);
2613
+ lines.push(`|---|---|`);
2614
+ lines.push(`| yours | \`${yours}\` |`);
2615
+ lines.push(`| latest | \`${dep.latestStable}\` |`);
2616
+ const journey = versionJourney(yours, dep.latestStable);
2617
+ if (journey) lines.push(`| journey | ${journey} |`);
2618
+ if (dep.majorsBehind) lines.push(`| majors behind | ${dep.majorsBehind} |`);
2619
+ if (dep.ageDays != null && dep.ageDays >= 1) {
2620
+ lines.push(`| age | ${Math.round(dep.ageDays).toLocaleString("en-US")} days |`);
2621
+ }
2622
+ if (dep.libyears != null && dep.libyears >= 0.01) {
2623
+ lines.push(`| libyears | ${dep.libyears.toFixed(2)} |`);
2624
+ }
2625
+ const license = dep.license?.spdxId ?? dep.license?.raw;
2626
+ if (license) lines.push(`| license | ${license} |`);
2627
+ } else {
2628
+ lines.push(`\`${yours}\` \u2014 current.`);
2629
+ }
2630
+ if (dep.unsupported || dep.abandoned) {
2631
+ lines.push("");
2632
+ lines.push("**Maintenance**");
2633
+ if (dep.unsupported) lines.push("$(warning) Unsupported / deprecated \u2014 floored at 70.");
2634
+ if (dep.abandoned) lines.push("$(warning) Abandoned \u2014 no recent releases \u2014 floored at 50.");
2635
+ }
2636
+ if (drifted) {
2637
+ lines.push("");
2638
+ lines.push("---");
2639
+ const routeFixUri = commandUri("vibgrate.routeFix", {
2640
+ package: dep.package,
2641
+ from: yours,
2642
+ to: dep.latestStable ?? yours,
2643
+ majorsBehind: dep.majorsBehind ?? 0
2644
+ });
2645
+ const explainUri = commandUri("vibgrate.explain", { package: dep.package });
2646
+ lines.push(`[Route this fix \u2192](${routeFixUri}) \xB7 [Explain this drift](${explainUri})`);
2647
+ }
2648
+ lines.push("");
2649
+ lines.push("---");
2650
+ lines.push("$(shield) Resolved locally from your lockfile. No source uploaded.");
2651
+ return lines.join("\n");
2652
+ }
2653
+ function startLanguageServer(opts) {
2654
+ if (!fs8.existsSync(opts.root)) {
2655
+ process.stderr.write(`vg lsp: root does not exist: ${opts.root}
2656
+ `);
2657
+ process.exit(1);
2658
+ }
2659
+ const realWrite = process.stdout.write.bind(process.stdout);
2660
+ const protocolOut = {
2661
+ write(chunk) {
2662
+ return realWrite(chunk);
2663
+ }
2664
+ };
2665
+ process.stdout.write = ((chunk, ...rest) => process.stderr.write(chunk, ...rest));
2666
+ process.stderr.write(
2667
+ `vg lsp ${VERSION} \u2014 serving ${pathToFileURL(opts.root).href}${opts.offline ? " (offline)" : ""}
2668
+ `
2669
+ );
2670
+ new VibgrateLanguageServer(opts, protocolOut);
2671
+ }
2672
+
2673
+ // src/commands/lsp.ts
2674
+ function registerLsp(program) {
2675
+ const cmd = program.command("lsp").description("start the Vibgrate language server (stdio) \u2014 the engine behind the IDE extensions").option(
2676
+ "--diagnostics",
2677
+ "also publish Problems-panel diagnostics (EOL runtime, unmaintained, license change). Off by default: drift is not a defect, and the Problems panel is not ours to fill",
2678
+ false
2679
+ ).option("--no-graph", "skip the local Vibgrate Graph entirely: no background build, and graph queries report it as turned off").option("--no-semantic", "never use semantic search for graph queries (lexical only; the embedding model is not downloaded)").action(async function(opts) {
2680
+ const global = readGlobal(this);
2681
+ const root = rootOf(global);
2682
+ startLanguageServer({
2683
+ root,
2684
+ // `--local` is the established air-gap flag across the CLI; honour it
2685
+ // here too, so an offline editor session stays offline.
2686
+ offline: global.local === true,
2687
+ diagnostics: opts.diagnostics === true,
2688
+ graph: opts.graph !== false,
2689
+ semantic: opts.semantic !== false
2690
+ });
2691
+ await new Promise(() => {
2692
+ });
2693
+ });
2694
+ applyGlobalOptions(cmd);
2695
+ }
2696
+
1578
2697
  // src/commands/install.ts
1579
2698
  var SMALL_REPO_FILES = 150;
1580
2699
  function registerInstall(program) {
1581
- const install = program.command("install").description("add vg to your AI assistant(s): skill + MCP + advisory nudge").argument("[tools...]", `assistant ids: ${ASSISTANTS.map((a) => a.id).join(", ")}`).option("--all", "install for every supported assistant").option("--list", "show the support matrix and exit").option("--no-hook", "skip the advisory nudge").action(function(tools, opts) {
2700
+ const install = program.command("install").description("add vg to your AI assistant(s): skill + MCP + advisory nudge").argument("[tools...]", `assistant ids: ${ASSISTANTS.map((a) => a.id).join(", ")}`).option("--all", "install for every supported assistant").option("--detect", "detect assistants in use (repo footprint, home config, PATH) and install for those; with --list, only report what was detected").option("--list", "show the support matrix and exit").option("--no-hook", "skip the advisory nudge").action(function(tools, opts) {
1582
2701
  const global = readGlobal(this);
1583
2702
  const root = rootOf(global);
2703
+ const detected = opts.detect ? detectAssistants(root) : [];
1584
2704
  if (opts.list) {
2705
+ if (opts.detect) {
2706
+ if (global.json) {
2707
+ json(detected.map((d) => ({ id: d.assistant.id, label: d.assistant.label, via: d.via, marker: d.marker })));
2708
+ } else if (detected.length === 0) {
2709
+ info(`${c.cyan("vg install")} \xB7 no AI assistants detected here`);
2710
+ } else {
2711
+ info(`${c.cyan("vg install")} \xB7 detected assistants`);
2712
+ const pad4 = Math.max(...detected.map((d) => d.assistant.id.length)) + 2;
2713
+ for (const d of detected) {
2714
+ info(` ${c.bold(d.assistant.id.padEnd(pad4))} ${d.assistant.label} ${c.dim(`via ${d.via}: ${d.marker}`)}`);
2715
+ }
2716
+ }
2717
+ return;
2718
+ }
1585
2719
  if (global.json) {
1586
2720
  json(ASSISTANTS.map((a) => ({ id: a.id, label: a.label, mcp: !!a.mcp, skill: !!a.skill, nudge: !!a.nudge })));
1587
2721
  } else {
@@ -1593,8 +2727,15 @@ function registerInstall(program) {
1593
2727
  }
1594
2728
  return;
1595
2729
  }
1596
- const targets = opts.all ? ASSISTANTS : tools.map(resolve5);
1597
- 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
+ }
1598
2739
  const graph = loadGraph(root, global.graph);
1599
2740
  const fileCount = graph ? graph.nodes.filter((n) => n.kind === "file").length : 0;
1600
2741
  const smallRepo = graph !== null && fileCount > 0 && fileCount < SMALL_REPO_FILES;
@@ -1617,7 +2758,7 @@ function registerInstall(program) {
1617
2758
  const uninstall = program.command("uninstall").description("remove vg from an AI assistant").argument("<tools...>", "assistant ids").option("--purge", "also delete the skill file").action(function(tools, opts) {
1618
2759
  const global = readGlobal(this);
1619
2760
  const root = rootOf(global);
1620
- const results = tools.map(resolve5).map((a) => ({ id: a.id, removed: uninstallAssistant(a, root, !!opts.purge) }));
2761
+ const results = tools.map(resolve6).map((a) => ({ id: a.id, removed: uninstallAssistant(a, root, !!opts.purge) }));
1621
2762
  if (global.json) {
1622
2763
  json({ root, results });
1623
2764
  return;
@@ -1628,7 +2769,7 @@ function registerInstall(program) {
1628
2769
  });
1629
2770
  applyGlobalOptions(uninstall);
1630
2771
  }
1631
- function resolve5(id) {
2772
+ function resolve6(id) {
1632
2773
  const a = assistantById(id);
1633
2774
  if (!a) {
1634
2775
  const near = ASSISTANTS.filter(
@@ -1649,7 +2790,7 @@ function registerShare(program) {
1649
2790
  const global = readGlobal(this);
1650
2791
  const root = rootOf(global);
1651
2792
  const gitDir = path.join(root, ".git");
1652
- if (!fs6.existsSync(gitDir)) {
2793
+ if (!fs8.existsSync(gitDir)) {
1653
2794
  throw new CliError("not a git repository \u2014 run `vg share` inside a git repo", ExitCode.USAGE_ERROR);
1654
2795
  }
1655
2796
  if (opts.undo) {
@@ -1671,22 +2812,22 @@ function registerShare(program) {
1671
2812
  }
1672
2813
  function writeVibgrateGitignore(root, reports) {
1673
2814
  const dir = path.join(root, ".vibgrate");
1674
- fs6.mkdirSync(dir, { recursive: true });
2815
+ fs8.mkdirSync(dir, { recursive: true });
1675
2816
  const lines = ["# Managed by `vg share` \u2014 keep graph.json committed, ignore the rest", "cache/", "facts.jsonl"];
1676
2817
  if (!reports) lines.push("graph.html", "GRAPH_REPORT.md");
1677
- fs6.writeFileSync(path.join(dir, ".gitignore"), `${lines.join("\n")}
2818
+ fs8.writeFileSync(path.join(dir, ".gitignore"), `${lines.join("\n")}
1678
2819
  `);
1679
2820
  }
1680
2821
  function writeGitAttributes(root) {
1681
2822
  const file = path.join(root, ".gitattributes");
1682
2823
  const line = ".vibgrate/graph.json merge=vg";
1683
- let content = fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : "";
2824
+ let content = fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : "";
1684
2825
  if (!content.split("\n").some((l) => l.trim() === line)) {
1685
2826
  content = content.length ? `${content.replace(/\s*$/, "")}
1686
2827
  ${line}
1687
2828
  ` : `${line}
1688
2829
  `;
1689
- fs6.writeFileSync(file, content);
2830
+ fs8.writeFileSync(file, content);
1690
2831
  }
1691
2832
  }
1692
2833
  function configureMergeDriver(root) {
@@ -1695,7 +2836,7 @@ function configureMergeDriver(root) {
1695
2836
  }
1696
2837
  function installPreCommitHook(root) {
1697
2838
  const hooksDir = hookDir(root);
1698
- fs6.mkdirSync(hooksDir, { recursive: true });
2839
+ fs8.mkdirSync(hooksDir, { recursive: true });
1699
2840
  const file = path.join(hooksDir, "pre-commit");
1700
2841
  const block = [
1701
2842
  HOOK_BEGIN,
@@ -1705,7 +2846,7 @@ function installPreCommitHook(root) {
1705
2846
  "fi",
1706
2847
  HOOK_END
1707
2848
  ].join("\n");
1708
- let content = fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : "#!/bin/sh\n";
2849
+ let content = fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : "#!/bin/sh\n";
1709
2850
  const re = new RegExp(`${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}`);
1710
2851
  if (re.test(content)) content = content.replace(re, block);
1711
2852
  else content = `${content.replace(/\s*$/, "")}
@@ -1714,21 +2855,21 @@ ${block}
1714
2855
  `;
1715
2856
  if (!content.startsWith("#!")) content = `#!/bin/sh
1716
2857
  ${content}`;
1717
- fs6.writeFileSync(file, content);
1718
- fs6.chmodSync(file, 493);
2858
+ fs8.writeFileSync(file, content);
2859
+ fs8.chmodSync(file, 493);
1719
2860
  }
1720
2861
  function undo(root) {
1721
2862
  const hookFile = path.join(hookDir(root), "pre-commit");
1722
- if (fs6.existsSync(hookFile)) {
2863
+ if (fs8.existsSync(hookFile)) {
1723
2864
  const re = new RegExp(`\\n*${escapeRe(HOOK_BEGIN)}[\\s\\S]*?${escapeRe(HOOK_END)}\\n*`);
1724
- const next2 = fs6.readFileSync(hookFile, "utf8").replace(re, "\n");
1725
- if (next2.trim() === "#!/bin/sh") fs6.rmSync(hookFile);
1726
- else fs6.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);
1727
2868
  }
1728
2869
  const attr = path.join(root, ".gitattributes");
1729
- if (fs6.existsSync(attr)) {
1730
- const next2 = fs6.readFileSync(attr, "utf8").split("\n").filter((l) => l.trim() !== ".vibgrate/graph.json merge=vg").join("\n");
1731
- fs6.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);
1732
2873
  }
1733
2874
  try {
1734
2875
  git(root, ["config", "--remove-section", "merge.vg"]);
@@ -1861,7 +3002,7 @@ function grepBaselineTokens(term, files) {
1861
3002
  if (f.size > 512 * 1024) continue;
1862
3003
  let content = "";
1863
3004
  try {
1864
- content = fs6.readFileSync(f.abs, "utf8");
3005
+ content = fs8.readFileSync(f.abs, "utf8");
1865
3006
  } catch {
1866
3007
  continue;
1867
3008
  }
@@ -1871,7 +3012,7 @@ function grepBaselineTokens(term, files) {
1871
3012
  }
1872
3013
  function safeSize(abs) {
1873
3014
  try {
1874
- return fs6.statSync(abs).size;
3015
+ return fs8.statSync(abs).size;
1875
3016
  } catch {
1876
3017
  return 0;
1877
3018
  }
@@ -2193,9 +3334,9 @@ function normalizeStandards(raw) {
2193
3334
  const name = asString(r.name);
2194
3335
  if (!name) continue;
2195
3336
  const ecosystem = asString(r.ecosystem)?.toLowerCase();
2196
- const key = `${ecosystem ?? "*"}\0${name.toLowerCase()}`;
2197
- if (seen.has(key)) continue;
2198
- seen.add(key);
3337
+ const key2 = `${ecosystem ?? "*"}\0${name.toLowerCase()}`;
3338
+ if (seen.has(key2)) continue;
3339
+ seen.add(key2);
2199
3340
  banned.push({ name, ...ecosystem ? { ecosystem } : {}, ...asString(r.use) ? { use: asString(r.use) } : {}, ...asString(r.reason) ? { reason: asString(r.reason) } : {} });
2200
3341
  }
2201
3342
  return { banned };
@@ -2205,7 +3346,7 @@ function loadStandards(root) {
2205
3346
  const p = path__default.join(root, rel3);
2206
3347
  let text;
2207
3348
  try {
2208
- text = fs6__default.readFileSync(p, "utf8");
3349
+ text = fs8__default.readFileSync(p, "utf8");
2209
3350
  } catch {
2210
3351
  continue;
2211
3352
  }
@@ -2373,7 +3514,7 @@ function printBreakdown(usage) {
2373
3514
  info(c.dim(` avg success across commands: ${usage.avgSuccessPct}%`));
2374
3515
  }
2375
3516
  function printSplit(usage) {
2376
- const label = (key) => key === "mcp" ? "MCP tools" : key === "cli" ? "vg CLI" : key;
3517
+ const label = (key2) => key2 === "mcp" ? "MCP tools" : key2 === "cli" ? "vg CLI" : key2;
2377
3518
  if (usage.sources.length) {
2378
3519
  info("");
2379
3520
  info(c.bold(" by source") + c.dim(" (how the call arrived)"));
@@ -2443,7 +3584,7 @@ async function addCmd(root, rest, opts, asJson) {
2443
3584
  }
2444
3585
  function readPackageVersion(root) {
2445
3586
  try {
2446
- const pkg = JSON.parse(fs6__default.readFileSync(path__default.join(root, "package.json"), "utf8"));
3587
+ const pkg = JSON.parse(fs8__default.readFileSync(path__default.join(root, "package.json"), "utf8"));
2447
3588
  return typeof pkg.version === "string" ? pkg.version : void 0;
2448
3589
  } catch {
2449
3590
  return void 0;
@@ -2451,7 +3592,7 @@ function readPackageVersion(root) {
2451
3592
  }
2452
3593
  function readOptionalFile(p) {
2453
3594
  try {
2454
- const t = fs6__default.readFileSync(p, "utf8");
3595
+ const t = fs8__default.readFileSync(p, "utf8");
2455
3596
  return t.trim() ? t : void 0;
2456
3597
  } catch {
2457
3598
  return void 0;
@@ -2597,8 +3738,8 @@ function registerExport(program) {
2597
3738
  out(content.trimEnd());
2598
3739
  return;
2599
3740
  }
2600
- fs6.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
2601
- fs6.writeFileSync(file, content);
3741
+ fs8.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
3742
+ fs8.writeFileSync(file, content);
2602
3743
  info(`${c.green("\u2714")} wrote ${file} ${c.dim(`(${format})`)}`);
2603
3744
  });
2604
3745
  applyGlobalOptions(cmd);
@@ -2621,24 +3762,24 @@ function registerBundle(program) {
2621
3762
  } catch {
2622
3763
  throw new CliError("no grammars found to bundle (run a build first)", ExitCode.ERROR);
2623
3764
  }
2624
- fs6.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
3765
+ fs8.mkdirSync(path.join(outDir, "grammars"), { recursive: true });
2625
3766
  let grammarCount = 0;
2626
3767
  for (const { fileName, absPath } of grammarFiles) {
2627
- fs6.copyFileSync(absPath, path.join(outDir, "grammars", fileName));
3768
+ fs8.copyFileSync(absPath, path.join(outDir, "grammars", fileName));
2628
3769
  grammarCount++;
2629
3770
  }
2630
3771
  const included = [`grammars/ (${grammarCount} .wasm)`];
2631
3772
  const graphSrc = global.graph ?? defaultGraphPath(root);
2632
- if (fs6.existsSync(graphSrc)) {
2633
- fs6.copyFileSync(graphSrc, path.join(outDir, "graph.json"));
3773
+ if (fs8.existsSync(graphSrc)) {
3774
+ fs8.copyFileSync(graphSrc, path.join(outDir, "graph.json"));
2634
3775
  included.push("graph.json");
2635
3776
  }
2636
3777
  const cat = catalogPath(root);
2637
- if (fs6.existsSync(cat)) {
2638
- fs6.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
3778
+ if (fs8.existsSync(cat)) {
3779
+ fs8.copyFileSync(cat, path.join(outDir, "vibgrate.lib.json"));
2639
3780
  included.push("vibgrate.lib.json");
2640
- if (fs6.existsSync(libDir(root))) {
2641
- fs6.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 });
2642
3783
  included.push("lib/");
2643
3784
  }
2644
3785
  }
@@ -2650,7 +3791,7 @@ function registerBundle(program) {
2650
3791
  included,
2651
3792
  usage: "Run `vg build --grammars <this dir>/grammars` to build fully offline."
2652
3793
  };
2653
- fs6.writeFileSync(path.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
3794
+ fs8.writeFileSync(path.join(outDir, "MANIFEST.json"), `${stableStringify(manifest, 2)}
2654
3795
  `);
2655
3796
  if (global.json) {
2656
3797
  json({ out: path.relative(root, outDir), ...manifest });
@@ -2675,7 +3816,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
2675
3816
  console.log(chalk8.green("\u2714") + ` Created ${chalk8.bold("vibgrate.config.ts")}`);
2676
3817
  }
2677
3818
  if (opts.baseline) {
2678
- const { runBaseline } = await import('./baseline-SRVG4ZAD.js');
3819
+ const { runBaseline } = await import('./baseline-YWGSHCKW.js');
2679
3820
  await runBaseline(rootDir);
2680
3821
  }
2681
3822
  console.log("");
@@ -2785,9 +3926,9 @@ async function fileReferencesIdentifier(rootDir, relFile, identifier, readFile3)
2785
3926
  }
2786
3927
  async function defaultReadFile(absPath) {
2787
3928
  try {
2788
- const stat2 = await fs12.stat(absPath);
3929
+ const stat2 = await fs14.stat(absPath);
2789
3930
  if (!stat2.isFile() || stat2.size > MAX_FILE_BYTES) return null;
2790
- return await fs12.readFile(absPath, "utf8");
3931
+ return await fs14.readFile(absPath, "utf8");
2791
3932
  } catch {
2792
3933
  return null;
2793
3934
  }
@@ -2816,11 +3957,11 @@ async function analyzeReachability(input) {
2816
3957
  const importedModules = [];
2817
3958
  const seenPackages = /* @__PURE__ */ new Set();
2818
3959
  for (const dep of input.dependencies) {
2819
- const key = `${dep.ecosystem}|${dep.package}`;
2820
- if (seenPackages.has(key)) continue;
2821
- seenPackages.add(key);
3960
+ const key2 = `${dep.ecosystem}|${dep.package}`;
3961
+ if (seenPackages.has(key2)) continue;
3962
+ seenPackages.add(key2);
2822
3963
  const evidence = collectPackageImports(input.graph, dep.ecosystem, dep.package);
2823
- evidenceByPackage.set(key, evidence);
3964
+ evidenceByPackage.set(key2, evidence);
2824
3965
  if (evidence.supported && importedModules.length < MAX_IMPORTED_MODULE_PACKAGES) {
2825
3966
  importedModules.push({
2826
3967
  ecosystem: dep.ecosystem,
@@ -2835,8 +3976,8 @@ async function analyzeReachability(input) {
2835
3976
  );
2836
3977
  const findings = [];
2837
3978
  for (const entry of input.manifest) {
2838
- const key = `${entry.ecosystem}|${entry.package}`;
2839
- 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);
2840
3981
  const baseConfidence = ECOSYSTEM_MATCH_CONFIDENCE[entry.ecosystem] ?? 0;
2841
3982
  if (!evidence.supported) {
2842
3983
  findings.push({
@@ -2934,9 +4075,9 @@ function collectPreflightDependencies(projects, projectTypeToEcosystem) {
2934
4075
  for (const dep of project.dependencies) {
2935
4076
  const version = dep.resolvedVersion || normalizeVersionSpec(dep.currentSpec);
2936
4077
  if (!version) continue;
2937
- const key = `${ecosystem}:${dep.package}:${version}`;
2938
- if (!deduped.has(key)) {
2939
- 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 });
2940
4081
  }
2941
4082
  }
2942
4083
  }
@@ -3058,7 +4199,9 @@ async function autoPush(artifact, rootDir, opts) {
3058
4199
  if (opts.strict) process.exit(1);
3059
4200
  return;
3060
4201
  }
3061
- const { body, contentEncoding } = await prepareCompressedUpload(artifact);
4202
+ const config = await loadConfig(rootDir);
4203
+ const databaseSchemaCaps = config.scanners !== false ? config.scanners?.databaseSchema : void 0;
4204
+ const { body, contentEncoding } = await prepareCompressedUpload(artifact, { databaseSchemaCaps });
3062
4205
  const timestamp = String(Date.now());
3063
4206
  let host = parsed.host;
3064
4207
  if (opts.region) {
@@ -3083,7 +4226,11 @@ async function autoPush(artifact, rootDir, opts) {
3083
4226
  body,
3084
4227
  contentEncoding,
3085
4228
  timestamp,
3086
- force: opts.force
4229
+ force: opts.force,
4230
+ // Set only by an automated caller running the scan on the workspace's
4231
+ // behalf (e.g. a Vibgrate-hosted remediation run) — never a customer flag.
4232
+ runId: process.env.VIBGRATE_SCAN_RUN_ID,
4233
+ runToken: process.env.VIBGRATE_SCAN_RUN_TOKEN
3087
4234
  });
3088
4235
  host = uploadedHost;
3089
4236
  if (!response.ok) {
@@ -3158,7 +4305,7 @@ var scanCommand = new Command("scan").description("Scan a project for upgrade dr
3158
4305
  'Exclude paths matching a glob pattern. Repeatable, and a single value may list several patterns separated by commas or semicolons (e.g. --exclude "legacy/**,vendor/**"). Merged with excludes from the config file.',
3159
4306
  collectExcludes,
3160
4307
  []
3161
- ).option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--full", "Comprehensive scan: turns on known-vulnerability detection (= --vulns) and, when a standards policy exists, a banned-dependency report \u2014 on top of drift scoring and the code map").option("--vulns", "Also scan installed dependencies for known vulnerabilities (OSV online, or advisories from --package-manifest when offline)").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--project-scan-timeout <seconds>", "Per-project scan timeout in seconds (default: 180)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").option("--repository-name <name>", "Override the repository name recorded for this scan (defaults to the directory or package.json name)").option("--force", "Always create a fresh scan ingest, even if the repository is unchanged since the last scan (skips the unchanged/reuse optimization). Used by scheduled and dashboard-triggered scans.").option("--no-graph", "Skip building the local code map (the AI/docs index) that scan produces after scoring drift").option("--quiet", "Suppress promotional output (the free-plan tracking panel and the AI Context install prompt); scan results are unaffected").action(async (targetPath, opts) => {
4308
+ ).option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--full", "Comprehensive scan: turns on known-vulnerability detection (= --vulns) and, when a standards policy exists, a banned-dependency report \u2014 on top of drift scoring and the code map").option("--vulns", "Also scan installed dependencies for known vulnerabilities (OSV online, or advisories from --package-manifest when offline)").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--project-scan-timeout <seconds>", "Per-project scan timeout in seconds (default: 180)").option("--drift-budget <score>", "Fail if DriftScore is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").option("--repository-name <name>", "Override the repository name recorded for this scan (defaults to the directory or package.json name)").option("--force", "Always create a fresh scan ingest, even if the repository is unchanged since the last scan (skips the unchanged/reuse optimization). Used by scheduled and dashboard-triggered scans.").option("--no-graph", "Skip building the local code map (the AI/docs index) that scan produces after scoring drift").option("--quiet", "Suppress promotional output (the free-plan tracking panel and the AI Context install prompt); scan results are unaffected").action(async (targetPath, opts) => {
3162
4309
  const rootDir = path.resolve(targetPath);
3163
4310
  if (!await pathExists$1(rootDir)) {
3164
4311
  console.error(chalk8.red(`Path does not exist: ${rootDir}`));
@@ -3332,7 +4479,7 @@ Failing: findings detected at warn level or above.`));
3332
4479
  }
3333
4480
  if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
3334
4481
  console.error(chalk8.red(`
3335
- Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
4482
+ Failing fitness function: DriftScore ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
3336
4483
  process.exit(2);
3337
4484
  }
3338
4485
  if (scanOpts.driftWorseningPercent !== void 0) {
@@ -3405,7 +4552,7 @@ var WATCHED_FILES = /* @__PURE__ */ new Set([
3405
4552
  "packages.lock.json"
3406
4553
  ]);
3407
4554
  var WATCHED_EXT = /\.(cs|fs)proj$/i;
3408
- var SKIP_DIRS = /* @__PURE__ */ new Set([
4555
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3409
4556
  "node_modules",
3410
4557
  ".git",
3411
4558
  "dist",
@@ -3426,7 +4573,7 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
3426
4573
  function scanStaleness(rootDir, artifactPath) {
3427
4574
  let ref;
3428
4575
  try {
3429
- ref = fs6.statSync(artifactPath).mtimeMs;
4576
+ ref = fs8.statSync(artifactPath).mtimeMs;
3430
4577
  } catch {
3431
4578
  return { stale: false };
3432
4579
  }
@@ -3438,7 +4585,7 @@ function scanStaleness(rootDir, artifactPath) {
3438
4585
  if (depth > 8 || scanned > MAX_ENTRIES) return;
3439
4586
  let entries;
3440
4587
  try {
3441
- entries = fs6.readdirSync(dir, { withFileTypes: true });
4588
+ entries = fs8.readdirSync(dir, { withFileTypes: true });
3442
4589
  } catch {
3443
4590
  return;
3444
4591
  }
@@ -3446,14 +4593,14 @@ function scanStaleness(rootDir, artifactPath) {
3446
4593
  scanned++;
3447
4594
  if (scanned > MAX_ENTRIES) break;
3448
4595
  if (e.isDirectory()) {
3449
- if (!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);
3450
4597
  continue;
3451
4598
  }
3452
4599
  if (!WATCHED_FILES.has(e.name) && !WATCHED_EXT.test(e.name)) continue;
3453
4600
  const abs = path.join(dir, e.name);
3454
4601
  let mtimeMs;
3455
4602
  try {
3456
- mtimeMs = fs6.statSync(abs).mtimeMs;
4603
+ mtimeMs = fs8.statSync(abs).mtimeMs;
3457
4604
  } catch {
3458
4605
  continue;
3459
4606
  }
@@ -3520,7 +4667,7 @@ function extractContracts(content, pkg, ecosystem) {
3520
4667
  }
3521
4668
 
3522
4669
  // src/reporting/planning/usage.ts
3523
- var SKIP_DIRS2 = /* @__PURE__ */ new Set([
4670
+ var SKIP_DIRS3 = /* @__PURE__ */ new Set([
3524
4671
  "node_modules",
3525
4672
  ".git",
3526
4673
  "dist",
@@ -3587,7 +4734,7 @@ function analyzeTree(root, packages) {
3587
4734
  if (depth > MAX_DEPTH || filesSeen >= MAX_FILES) return;
3588
4735
  let entries;
3589
4736
  try {
3590
- entries = fs6.readdirSync(dir, { withFileTypes: true });
4737
+ entries = fs8.readdirSync(dir, { withFileTypes: true });
3591
4738
  } catch {
3592
4739
  return;
3593
4740
  }
@@ -3595,7 +4742,7 @@ function analyzeTree(root, packages) {
3595
4742
  for (const entry of entries) {
3596
4743
  if (filesSeen >= MAX_FILES) return;
3597
4744
  if (entry.isDirectory()) {
3598
- if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
4745
+ if (SKIP_DIRS3.has(entry.name) || entry.name.startsWith(".")) continue;
3599
4746
  walk2(path.join(dir, entry.name), depth + 1);
3600
4747
  continue;
3601
4748
  }
@@ -3605,8 +4752,8 @@ function analyzeTree(root, packages) {
3605
4752
  const full = path.join(dir, entry.name);
3606
4753
  let content;
3607
4754
  try {
3608
- if (fs6.statSync(full).size > MAX_FILE_BYTES2) continue;
3609
- content = fs6.readFileSync(full, "utf8");
4755
+ if (fs8.statSync(full).size > MAX_FILE_BYTES2) continue;
4756
+ content = fs8.readFileSync(full, "utf8");
3610
4757
  } catch {
3611
4758
  continue;
3612
4759
  }
@@ -3702,9 +4849,6 @@ function planBlock(plan, recommended) {
3702
4849
  if (plan.upgrades.length > shown.length) {
3703
4850
  lines.push(chalk8.dim(` \u2026 and ${plan.upgrades.length - shown.length} more`));
3704
4851
  }
3705
- if (plan.upgrades.length === 0) {
3706
- lines.push(chalk8.dim(" (no upgrades in this plan)"));
3707
- }
3708
4852
  return lines.join("\n");
3709
4853
  }
3710
4854
  function dataNote(report) {
@@ -3731,9 +4875,15 @@ function renderText(report) {
3731
4875
  out2.push(chalk8.red(`\u26A0 ${report.exploitability.kevPackages} package(s) carry a KNOWN-EXPLOITED (KEV) advisory${epss} \u2014 prioritise these.`));
3732
4876
  }
3733
4877
  out2.push("");
3734
- for (const plan of report.plans) {
3735
- 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."));
3736
4881
  out2.push("");
4882
+ } else {
4883
+ for (const plan of visiblePlans) {
4884
+ out2.push(planBlock(plan, plan.tier === report.recommended));
4885
+ out2.push("");
4886
+ }
3737
4887
  }
3738
4888
  out2.push(chalk8.bold("Recommendation"));
3739
4889
  out2.push(` ${chalk8.green(report.plans.find((p) => p.tier === report.recommended)?.label ?? report.recommended)} \u2014 ${report.rationale}`);
@@ -3756,7 +4906,12 @@ function renderMarkdown(report) {
3756
4906
  out2.push(`_${report.vulnerabilityData === "unavailable" ? "Advisory data was unavailable \u2014 vulnerability impact is not shown." : "Advisory data was partial \u2014 some ecosystems were not checked."}_`);
3757
4907
  }
3758
4908
  out2.push("");
3759
- 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) {
3760
4915
  const rec = plan.tier === report.recommended ? " \u2705 **recommended**" : "";
3761
4916
  out2.push(`## ${plan.label}${rec}`);
3762
4917
  out2.push("");
@@ -3769,14 +4924,12 @@ function renderMarkdown(report) {
3769
4924
  out2.push(`- \u26A0 Introduces advisories in target versions: ${deltaSummary(plan.introduces)}`);
3770
4925
  }
3771
4926
  out2.push("");
3772
- if (plan.upgrades.length) {
3773
- out2.push("| Package | From | To | Kind | Reason |");
3774
- out2.push("|---|---|---|---|---|");
3775
- for (const u of plan.upgrades) {
3776
- out2.push(`| \`${u.package}\` | ${u.from ?? "?"} | ${u.to ?? "?"} | ${u.kind} | ${u.reason} |`);
3777
- }
3778
- out2.push("");
4927
+ out2.push("| Package | From | To | Kind | Reason |");
4928
+ out2.push("|---|---|---|---|---|");
4929
+ for (const u of plan.upgrades) {
4930
+ out2.push(`| \`${u.package}\` | ${u.from ?? "?"} | ${u.to ?? "?"} | ${u.kind} | ${u.reason} |`);
3779
4931
  }
4932
+ out2.push("");
3780
4933
  }
3781
4934
  out2.push("## Recommendation");
3782
4935
  out2.push("");
@@ -3789,6 +4942,29 @@ function renderMarkdown(report) {
3789
4942
  return out2.join("\n");
3790
4943
  }
3791
4944
 
4945
+ // src/reporting/planning/dedupe.ts
4946
+ var TIER_ORDER = ["safe", "balanced", "aggressive"];
4947
+ function upgradeSetKey(plan) {
4948
+ return plan.upgrades.map((u) => `${u.ecosystem}\0${u.package}\0${u.from ?? ""}\0${u.to ?? ""}`).sort().join("\n");
4949
+ }
4950
+ function dedupePlans(plans) {
4951
+ const byTier = (plan) => {
4952
+ const i = TIER_ORDER.indexOf(plan.tier);
4953
+ return i === -1 ? TIER_ORDER.length : i;
4954
+ };
4955
+ const ordered = [...plans].sort((a, b) => byTier(a) - byTier(b));
4956
+ const survivorByKey = /* @__PURE__ */ new Map();
4957
+ const canonicalTier = /* @__PURE__ */ new Map();
4958
+ for (const plan of ordered) {
4959
+ const key2 = upgradeSetKey(plan);
4960
+ const survivor = survivorByKey.get(key2) ?? plan;
4961
+ if (!survivorByKey.has(key2)) survivorByKey.set(key2, plan);
4962
+ canonicalTier.set(plan.tier, survivor.tier);
4963
+ }
4964
+ const survivors = new Set(survivorByKey.values());
4965
+ return { plans: plans.filter((p) => survivors.has(p)), canonicalTier };
4966
+ }
4967
+
3792
4968
  // src/reporting/planning/expected-drift.ts
3793
4969
  function estimateDriftScore(artifact, upgraded) {
3794
4970
  const projects = JSON.parse(JSON.stringify(artifact.projects ?? []));
@@ -3797,6 +4973,11 @@ function estimateDriftScore(artifact, upgraded) {
3797
4973
  for (const dep of p.dependencies ?? []) {
3798
4974
  if (!upgraded.has(dep.package)) continue;
3799
4975
  const mb3 = dep.majorsBehind ?? 0;
4976
+ dep.majorsBehind = 0;
4977
+ dep.drift = "current";
4978
+ if (dep.libyears != null) dep.libyears = 0;
4979
+ if (dep.ageDays != null) dep.ageDays = 0;
4980
+ if (dep.latestStable) dep.resolvedVersion = dep.latestStable;
3800
4981
  if (mb3 >= 2 && buckets.twoPlusBehind > 0) {
3801
4982
  buckets.twoPlusBehind--;
3802
4983
  buckets.current++;
@@ -3940,22 +5121,56 @@ async function fetchLatestVersion() {
3940
5121
  }
3941
5122
  async function writeCache(data) {
3942
5123
  try {
3943
- await fs12.mkdir(CACHE_DIR, { recursive: true });
3944
- await fs12.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");
3945
5126
  } catch {
3946
5127
  }
3947
5128
  }
3948
5129
 
3949
5130
  // src/reporting/commands/update.ts
3950
- function detectGlobalInstall() {
3951
- const execPath = process.argv[1] || "";
3952
- if (execPath.includes("/lib/node_modules/") || execPath.includes("\\node_modules\\")) {
3953
- if (!execPath.includes(process.cwd())) {
3954
- if (execPath.includes("pnpm")) return "pnpm";
3955
- if (execPath.includes("yarn")) return "yarn";
3956
- if (execPath.includes("bun")) return "bun";
3957
- 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;
3958
5157
  }
5158
+ };
5159
+ const probes = [
5160
+ { pm: "npm", root: () => tryRun("npm root -g") },
5161
+ { pm: "pnpm", root: () => tryRun("pnpm root -g") },
5162
+ {
5163
+ pm: "yarn",
5164
+ root: () => {
5165
+ const dir = tryRun("yarn global dir");
5166
+ return dir ? path.join(dir, "node_modules") : null;
5167
+ }
5168
+ },
5169
+ { pm: "bun", root: () => path.join(os.homedir(), ".bun", "install", "global", "node_modules") }
5170
+ ];
5171
+ for (const probe of probes) {
5172
+ const root = probe.root();
5173
+ if (root && await pathExists(path.join(root, pkg))) return probe.pm;
3959
5174
  }
3960
5175
  return null;
3961
5176
  }
@@ -4005,24 +5220,25 @@ function getInstallCommand(pm, pkg, version, isDev, opts = {}) {
4005
5220
  return isDev ? `npm install --save-dev ${spec}` : `npm install ${spec}`;
4006
5221
  }
4007
5222
  }
4008
- async function isDevDependency(cwd) {
5223
+ async function getLocalDependencyState(cwd, pkg = PACKAGE_NAME) {
4009
5224
  try {
4010
5225
  const pkgPath = path.join(cwd, "package.json");
4011
5226
  const raw = await (await import('fs/promises')).readFile(pkgPath, "utf-8");
4012
- const pkg = JSON.parse(raw);
4013
- 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";
4014
5230
  } catch {
4015
- return true;
4016
5231
  }
5232
+ return null;
4017
5233
  }
4018
5234
  async function confirmWorkspaceRoot(pm) {
4019
5235
  if (!(process.stdin.isTTY && process.stdout.isTTY)) return false;
4020
5236
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4021
- return new Promise((resolve16) => {
5237
+ return new Promise((resolve17) => {
4022
5238
  rl.question(chalk8.yellow(`Update the ${pm} workspace root with -w? [y/N]: `), (ans) => {
4023
5239
  rl.close();
4024
5240
  const t = ans.trim().toLowerCase();
4025
- resolve16(t === "y" || t === "yes");
5241
+ resolve17(t === "y" || t === "yes");
4026
5242
  });
4027
5243
  });
4028
5244
  }
@@ -4046,15 +5262,29 @@ var updateCommand = new Command("update").description("Update vibgrate to the la
4046
5262
  return;
4047
5263
  }
4048
5264
  const cwd = process.cwd();
4049
- const globalPm = detectGlobalInstall();
4050
- const isGlobal = opts.global || globalPm !== null;
4051
- 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);
4052
5282
  let cmd;
4053
5283
  if (isGlobal) {
4054
- cmd = getGlobalUpdateCommand(pm, "@vibgrate/cli", latest);
5284
+ cmd = getGlobalUpdateCommand(pm, PACKAGE_NAME, latest);
4055
5285
  console.log(chalk8.dim(`Updating global installation with ${pm}: ${cmd}`));
4056
5286
  } else {
4057
- const isDev = await isDevDependency(cwd);
5287
+ const isDev = localState !== "prod";
4058
5288
  let workspaceRoot = false;
4059
5289
  if (pm === "pnpm" && await detectWorkspaceRoot(cwd)) {
4060
5290
  console.log(
@@ -4062,19 +5292,19 @@ var updateCommand = new Command("update").description("Update vibgrate to the la
4062
5292
  );
4063
5293
  const proceed = opts.yes || opts.workspaceRoot || await confirmWorkspaceRoot(pm);
4064
5294
  if (!proceed) {
4065
- const rootCmd = getInstallCommand(pm, "@vibgrate/cli", latest, isDev, { workspaceRoot: true });
5295
+ const rootCmd = getInstallCommand(pm, PACKAGE_NAME, latest, isDev, { workspaceRoot: true });
4066
5296
  console.log(chalk8.dim(`Skipped. To update the workspace root, run: ${rootCmd}`));
4067
5297
  console.log(chalk8.dim('or re-run "vg update --yes" (or --workspace-root) to let vg do it.'));
4068
5298
  return;
4069
5299
  }
4070
5300
  workspaceRoot = true;
4071
5301
  }
4072
- cmd = getInstallCommand(pm, "@vibgrate/cli", latest, isDev, { workspaceRoot });
5302
+ cmd = getInstallCommand(pm, PACKAGE_NAME, latest, isDev, { workspaceRoot });
4073
5303
  console.log(chalk8.dim(`Using ${pm}: ${cmd}`));
4074
5304
  }
4075
5305
  try {
4076
5306
  execSync(cmd, { cwd, stdio: "inherit" });
4077
- console.log(chalk8.green("\u2714") + ` Updated to @vibgrate/cli@${latest}`);
5307
+ console.log(chalk8.green("\u2714") + ` Updated to ${PACKAGE_NAME}@${latest}`);
4078
5308
  } catch {
4079
5309
  console.error(chalk8.red(`Update failed. Run manually: ${cmd}`));
4080
5310
  process.exit(1);
@@ -4098,11 +5328,11 @@ function collectCandidates(artifact) {
4098
5328
  for (const project of artifact.projects ?? []) {
4099
5329
  const ecosystem = ecosystemId(project.type);
4100
5330
  for (const dep of project.dependencies ?? []) {
4101
- const key = `${ecosystem}\0${dep.package}`;
4102
- if (seen.has(key)) continue;
5331
+ const key2 = `${ecosystem}\0${dep.package}`;
5332
+ if (seen.has(key2)) continue;
4103
5333
  if (!dep.latestStable || dep.latestStable === dep.resolvedVersion) continue;
4104
5334
  if (dep.drift === "current" || dep.drift === "unknown") continue;
4105
- seen.add(key);
5335
+ seen.add(key2);
4106
5336
  out2.push({
4107
5337
  package: dep.package,
4108
5338
  ecosystem,
@@ -4150,7 +5380,7 @@ async function repositoryIdentity(rootDir, override) {
4150
5380
  return { name: override?.trim() || void 0 };
4151
5381
  }
4152
5382
  }
4153
- var fixCommand = new Command("fix").description("Analyse drift and get ranked, risk-tiered upgrade plans from the hosted planner (requires login/DSN; read-only \u2014 never edits your project)").argument("[path]", "Path to analyse", ".").option("--in <file>", "Scan artifact to read", ".vibgrate/scan_result.json").option("--format <format>", "Output format (text|json|md)", "text").option("--dsn <dsn>", 'DSN token (or use VIBGRATE_DSN env / "vg login")').option("--region <region>", "Override data residency region (us, eu)").option("--repository-name <name>", "Override the repository name recorded for this plan").option("--plan <tier>", "Apply a specific plan non-interactively (safe|balanced|aggressive)").option("--yes", "Apply the recommended plan without prompting").option("--dry-run", "Show what would change without applying").option("--no-apply", "Only print the plans; never modify the project").option("--fail-on-vulns <severity>", "Exit non-zero if the recommended plan leaves an advisory at/above this severity unresolved (low|moderate|high|critical)").action(async (targetPath, opts) => {
5383
+ var fixCommand = new Command("fix").description("Get ranked, risk-tiered upgrade plans from the hosted planner and optionally apply them (prompts before changing files; --yes applies non-interactively, --dry-run previews, --no-apply only prints). Requires login/DSN.").argument("[path]", "Path to analyse", ".").option("--in <file>", "Scan artifact to read", ".vibgrate/scan_result.json").option("--format <format>", "Output format (text|json|md)", "text").option("--dsn <dsn>", 'DSN token (or use VIBGRATE_DSN env / "vg login")').option("--region <region>", "Override data residency region (us, eu)").option("--repository-name <name>", "Override the repository name recorded for this plan").option("--plan <tier>", "Apply a specific plan non-interactively (safe|balanced|aggressive)").option("--yes", "Apply the recommended plan without prompting").option("--dry-run", "Show what would change without applying").option("--no-apply", "Only print the plans; never modify the project").option("--fail-on-vulns <severity>", "Exit non-zero if the recommended plan leaves an advisory at/above this severity unresolved (low|moderate|high|critical)").action(async (targetPath, opts) => {
4154
5384
  const rootDir = path.resolve(targetPath);
4155
5385
  if (!await pathExists(rootDir)) {
4156
5386
  console.error(chalk8.red(`Path does not exist: ${rootDir}`));
@@ -4240,6 +5470,9 @@ var fixCommand = new Command("fix").description("Analyse drift and get ranked, r
4240
5470
  if (response.requestId) console.error(chalk8.dim(` (ref ${response.requestId})`));
4241
5471
  process.exit(1);
4242
5472
  }
5473
+ const { plans: dedupedPlans, canonicalTier } = dedupePlans(response.plans);
5474
+ response.plans = dedupedPlans;
5475
+ response.recommended = canonicalTier.get(response.recommended) ?? response.recommended;
4243
5476
  const currentDrift = artifact.drift?.score;
4244
5477
  if (typeof currentDrift === "number") {
4245
5478
  response.currentDriftScore = currentDrift;
@@ -4252,7 +5485,7 @@ var fixCommand = new Command("fix").description("Analyse drift and get ranked, r
4252
5485
  }
4253
5486
  emit(response, opts.format);
4254
5487
  if (opts.format === "text" && opts.apply !== false) {
4255
- await runApplyFlow(rootDir, artifact, response, opts);
5488
+ await runApplyFlow(rootDir, artifact, response, opts, canonicalTier);
4256
5489
  }
4257
5490
  if (failOn) {
4258
5491
  const recommended = response.plans.find((p) => p.tier === response.recommended);
@@ -4261,7 +5494,8 @@ var fixCommand = new Command("fix").description("Analyse drift and get ranked, r
4261
5494
  for (const sev of Object.keys(SEVERITY_RANK)) {
4262
5495
  if (SEVERITY_RANK[sev] < threshold) continue;
4263
5496
  const open = response.unresolved.bySeverity[sev] ?? 0;
4264
- const 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);
4265
5499
  stillOpen += open + Math.max(0, notFixedByRecommended);
4266
5500
  }
4267
5501
  if (stillOpen > 0) {
@@ -4318,8 +5552,8 @@ async function buildTargetResolver(rootDir, artifact) {
4318
5552
  const ecosystem = ecosystemId(project.type);
4319
5553
  const dir = relProjectDir(rootDir, project.path);
4320
5554
  for (const dep of project.dependencies ?? []) {
4321
- const key = `${ecosystem}\0${dep.package}`;
4322
- (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);
4323
5557
  }
4324
5558
  }
4325
5559
  const rootFallback = [{ dir: ".", isWorkspaceRoot: rootIsWorkspace }];
@@ -4349,18 +5583,18 @@ function promptPlanSelection(response) {
4349
5583
  const defaultIdx = plans.findIndex((p) => p.tier === response.recommended);
4350
5584
  const def = defaultIdx >= 0 ? defaultIdx + 1 : 1;
4351
5585
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
4352
- return new Promise((resolve16) => {
5586
+ return new Promise((resolve17) => {
4353
5587
  rl.question(`Plan [1-${plans.length}, default ${def}, q to cancel]: `, (ans) => {
4354
5588
  rl.close();
4355
5589
  const t = ans.trim().toLowerCase();
4356
- if (t === "q" || t === "quit") return resolve16(null);
5590
+ if (t === "q" || t === "quit") return resolve17(null);
4357
5591
  const n = t === "" ? def : Number.parseInt(t, 10);
4358
- if (!Number.isInteger(n) || n < 1 || n > plans.length) return resolve16(null);
4359
- resolve16(plans[n - 1].tier);
5592
+ if (!Number.isInteger(n) || n < 1 || n > plans.length) return resolve17(null);
5593
+ resolve17(plans[n - 1].tier);
4360
5594
  });
4361
5595
  });
4362
5596
  }
4363
- async function runApplyFlow(rootDir, artifact, response, opts) {
5597
+ async function runApplyFlow(rootDir, artifact, response, opts, canonicalTier) {
4364
5598
  const nonEmpty = response.plans.filter((p) => p.upgrades.length > 0);
4365
5599
  if (nonEmpty.length === 0) {
4366
5600
  console.log(chalk8.green("\n\u2714 Nothing to upgrade \u2014 every tracked dependency is current."));
@@ -4368,7 +5602,8 @@ async function runApplyFlow(rootDir, artifact, response, opts) {
4368
5602
  }
4369
5603
  let chosen;
4370
5604
  if (opts.plan) {
4371
- 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);
4372
5607
  if (!chosen) {
4373
5608
  console.error(chalk8.red(`Unknown plan '${opts.plan}'. Use safe, balanced, or aggressive.`));
4374
5609
  process.exit(1);
@@ -4727,6 +5962,26 @@ function formatExtended(ext) {
4727
5962
  }
4728
5963
  lines.push("");
4729
5964
  }
5965
+ if (ext.databaseSchema) {
5966
+ const ds = ext.databaseSchema;
5967
+ lines.push(chalk8.bold.underline(" Database Schema"));
5968
+ const dsHead = [];
5969
+ if (ds.providers.length > 0) dsHead.push(ds.providers.join(", "));
5970
+ dsHead.push(`${ds.models.length} model${ds.models.length !== 1 ? "s" : ""}`);
5971
+ dsHead.push(`${ds.enums.length} enum${ds.enums.length !== 1 ? "s" : ""}`);
5972
+ lines.push(` ${dsHead.join(" \xB7 ")}`);
5973
+ const bySource = /* @__PURE__ */ new Map();
5974
+ for (const m of ds.models) bySource.set(m.source, (bySource.get(m.source) ?? 0) + 1);
5975
+ if (bySource.size > 1) {
5976
+ const sourceSummary = [...bySource.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([s, n]) => `${s}: ${n}`).join(", ");
5977
+ lines.push(` Sources: ${sourceSummary}`);
5978
+ }
5979
+ if (ds.models.length > 0) {
5980
+ const preview = ds.models.slice(0, 5).map((m) => m.name).join(", ");
5981
+ lines.push(` Models: ${chalk8.white(preview)}${ds.models.length > 5 ? chalk8.dim(` (+${ds.models.length - 5} more)`) : ""}`);
5982
+ }
5983
+ lines.push("");
5984
+ }
4730
5985
  if (ext.dependencyGraph) {
4731
5986
  const dg = ext.dependencyGraph;
4732
5987
  if (dg.lockfileType) {
@@ -5168,7 +6423,7 @@ function openUrl(url) {
5168
6423
  }
5169
6424
 
5170
6425
  // src/reporting/commands/login.ts
5171
- var delay = (ms) => new Promise((resolve16) => setTimeout(resolve16, ms));
6426
+ var delay = (ms) => new Promise((resolve17) => setTimeout(resolve17, ms));
5172
6427
  function isInside(dir, file) {
5173
6428
  const rel3 = path.relative(dir, file);
5174
6429
  return !!rel3 && !rel3.startsWith("..") && !path.isAbsolute(rel3);
@@ -5593,21 +6848,21 @@ function formatDeltaText(base, current) {
5593
6848
  const added = [];
5594
6849
  const removed = [];
5595
6850
  const changed = [];
5596
- for (const [key, dep] of currentMap.entries()) {
5597
- if (!baseMap.has(key)) {
5598
- 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}`);
5599
6854
  continue;
5600
6855
  }
5601
- const prev = baseMap.get(key);
6856
+ const prev = baseMap.get(key2);
5602
6857
  const prevVersion = prev.resolvedVersion ?? prev.currentSpec;
5603
6858
  const nowVersion = dep.resolvedVersion ?? dep.currentSpec;
5604
6859
  if (prevVersion !== nowVersion || prev.majorsBehind !== dep.majorsBehind) {
5605
- changed.push(`${key} ${prevVersion} -> ${nowVersion} (majorsBehind ${prev.majorsBehind ?? "unknown"} -> ${dep.majorsBehind ?? "unknown"})`);
6860
+ changed.push(`${key2} ${prevVersion} -> ${nowVersion} (majorsBehind ${prev.majorsBehind ?? "unknown"} -> ${dep.majorsBehind ?? "unknown"})`);
5606
6861
  }
5607
6862
  }
5608
- for (const [key, dep] of baseMap.entries()) {
5609
- if (!currentMap.has(key)) {
5610
- 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}`);
5611
6866
  }
5612
6867
  }
5613
6868
  const lines = [
@@ -5615,7 +6870,7 @@ function formatDeltaText(base, current) {
5615
6870
  "===================",
5616
6871
  `Baseline: ${base.timestamp}`,
5617
6872
  `Current: ${current.timestamp}`,
5618
- `Drift score delta: ${(current.drift.score - base.drift.score).toFixed(2)} points`,
6873
+ `DriftScore delta: ${(current.drift.score - base.drift.score).toFixed(2)} points`,
5619
6874
  "",
5620
6875
  `Added dependencies (${added.length})`,
5621
6876
  ...added.map((d) => ` + ${d}`),
@@ -5693,6 +6948,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
5693
6948
  "areas",
5694
6949
  "oddities",
5695
6950
  "serve",
6951
+ "lsp",
5696
6952
  "install",
5697
6953
  "uninstall",
5698
6954
  "share",
@@ -5733,6 +6989,7 @@ ${aliasNote}Docs: https://vibgrate.com/help`;
5733
6989
  registerImpact(program);
5734
6990
  registerUnknowns(program);
5735
6991
  registerServe(program);
6992
+ registerLsp(program);
5736
6993
  registerInstall(program);
5737
6994
  registerShare(program);
5738
6995
  registerBenchmark(program);
@@ -5814,7 +7071,7 @@ function dispatch(argv, cwd) {
5814
7071
  }
5815
7072
  const looksLikeQuestion = /\s/.test(first) || first.endsWith("?");
5816
7073
  if (looksLikeQuestion) return ["ask", ...args];
5817
- if (fs6.existsSync(path.resolve(cwd, first))) {
7074
+ if (fs8.existsSync(path.resolve(cwd, first))) {
5818
7075
  return [hasBuildOnlyFlag(args) ? "build" : "scan", ...args];
5819
7076
  }
5820
7077
  const moved = MOVED_COMMANDS[first];