@wrongstack/tools 0.296.3 → 0.297.0

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.
Files changed (55) hide show
  1. package/dist/audit.js +15 -0
  2. package/dist/audit.js.map +2 -2
  3. package/dist/bash.js +15 -0
  4. package/dist/bash.js.map +2 -2
  5. package/dist/builtin.js +1291 -1216
  6. package/dist/builtin.js.map +4 -4
  7. package/dist/codebase-index/background-indexer.d.ts.map +1 -1
  8. package/dist/codebase-index/index.d.ts +1 -0
  9. package/dist/codebase-index/index.d.ts.map +1 -1
  10. package/dist/codebase-index/index.js +1259 -1196
  11. package/dist/codebase-index/index.js.map +4 -4
  12. package/dist/codebase-index/project-server-client.d.ts +13 -1
  13. package/dist/codebase-index/project-server-client.d.ts.map +1 -1
  14. package/dist/codebase-index/project-server-endpoint.d.ts +29 -1
  15. package/dist/codebase-index/project-server-endpoint.d.ts.map +1 -1
  16. package/dist/codebase-index/project-server.js +20 -5
  17. package/dist/codebase-index/project-server.js.map +2 -2
  18. package/dist/codebase-index/refs-extractor.d.ts.map +1 -1
  19. package/dist/exec.js +15 -0
  20. package/dist/exec.js.map +2 -2
  21. package/dist/format.js +15 -0
  22. package/dist/format.js.map +2 -2
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +1260 -1182
  26. package/dist/index.js.map +4 -4
  27. package/dist/install.js +15 -0
  28. package/dist/install.js.map +2 -2
  29. package/dist/languages/index.js +15 -0
  30. package/dist/languages/index.js.map +2 -2
  31. package/dist/lint.js +15 -0
  32. package/dist/lint.js.map +2 -2
  33. package/dist/next-steps.d.ts +23 -0
  34. package/dist/next-steps.d.ts.map +1 -1
  35. package/dist/next-steps.js +4 -0
  36. package/dist/next-steps.js.map +2 -2
  37. package/dist/outdated.js +15 -0
  38. package/dist/outdated.js.map +2 -2
  39. package/dist/pack.js +1291 -1216
  40. package/dist/pack.js.map +4 -4
  41. package/dist/process-registry.d.ts +9 -0
  42. package/dist/process-registry.d.ts.map +1 -1
  43. package/dist/process-registry.js +15 -0
  44. package/dist/process-registry.js.map +2 -2
  45. package/dist/ps-slash.js +15 -0
  46. package/dist/ps-slash.js.map +2 -2
  47. package/dist/read.js +67 -9
  48. package/dist/read.js.map +2 -2
  49. package/dist/test.js +15 -0
  50. package/dist/test.js.map +2 -2
  51. package/dist/tool-tier.js +1291 -1216
  52. package/dist/tool-tier.js.map +4 -4
  53. package/dist/typecheck.js +15 -0
  54. package/dist/typecheck.js.map +2 -2
  55. package/package.json +3 -3
@@ -14,16 +14,16 @@ var __export = (target, all) => {
14
14
  };
15
15
 
16
16
  // src/codebase-index/languages.ts
17
- import * as path2 from "node:path";
17
+ import * as path6 from "node:path";
18
18
  function detectLang(file) {
19
- const base = path2.basename(file);
19
+ const base = path6.basename(file);
20
20
  const lowerBase = base.toLowerCase();
21
21
  if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
22
22
  return "ts";
23
23
  }
24
24
  const special = SPECIAL_FILENAMES[lowerBase];
25
25
  if (special) return special;
26
- const ext = path2.extname(base).toLowerCase();
26
+ const ext = path6.extname(base).toLowerCase();
27
27
  if (!ext) return null;
28
28
  return EXT_TO_LANG[ext] ?? null;
29
29
  }
@@ -340,21 +340,21 @@ var init_ts_parser = __esm({
340
340
  });
341
341
 
342
342
  // src/_win32-resolve.ts
343
- import * as fs2 from "node:fs";
344
- import * as path3 from "node:path";
343
+ import * as fs6 from "node:fs";
344
+ import * as path7 from "node:path";
345
345
  function resolveWin32Command(cmd) {
346
346
  if (process.platform !== "win32") return cmd;
347
- if (cmd.includes("/") || cmd.includes("\\") || path3.extname(cmd.replace(/\//g, "\\"))) {
347
+ if (cmd.includes("/") || cmd.includes("\\") || path7.extname(cmd.replace(/\//g, "\\"))) {
348
348
  return cmd;
349
349
  }
350
350
  const pathext = (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC").toLowerCase().split(";");
351
- const pathDirs = (process.env["PATH"] ?? "").split(path3.delimiter);
351
+ const pathDirs = (process.env["PATH"] ?? "").split(path7.delimiter);
352
352
  for (const dir of pathDirs) {
353
- const base = path3.join(dir, cmd);
353
+ const base = path7.join(dir, cmd);
354
354
  for (const ext of pathext) {
355
355
  const full = `${base}${ext}`;
356
356
  try {
357
- fs2.accessSync(full, fs2.constants.X_OK);
357
+ fs6.accessSync(full, fs6.constants.X_OK);
358
358
  return full;
359
359
  } catch {
360
360
  }
@@ -391,10 +391,10 @@ __export(go_parser_exports, {
391
391
  detectLang: () => detectLang,
392
392
  parseSymbols: () => parseSymbols2
393
393
  });
394
- import { spawn } from "node:child_process";
395
- import * as os from "node:os";
396
- import * as path4 from "node:path";
397
- import * as fs3 from "node:fs/promises";
394
+ import { spawn as spawn2 } from "node:child_process";
395
+ import * as os2 from "node:os";
396
+ import * as path8 from "node:path";
397
+ import * as fs7 from "node:fs/promises";
398
398
  async function parseSymbols2(opts) {
399
399
  const { file, content, lang } = opts;
400
400
  try {
@@ -466,16 +466,16 @@ async function syncGoParse(filePath, content, lang) {
466
466
  try {
467
467
  let scriptPath = _cachedGoScriptPath;
468
468
  if (!scriptPath) {
469
- const tmpDir = await fs3.mkdtemp(path4.join(os.tmpdir(), "ws-go-parse-"));
470
- scriptPath = path4.join(tmpDir, "parse.go");
471
- await fs3.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
469
+ const tmpDir = await fs7.mkdtemp(path8.join(os2.tmpdir(), "ws-go-parse-"));
470
+ scriptPath = path8.join(tmpDir, "parse.go");
471
+ await fs7.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
472
472
  _cachedGoScriptPath = scriptPath;
473
473
  }
474
474
  const goBinary = resolveWin32Command("go");
475
475
  const goResult = await new Promise(
476
476
  (resolve4, reject) => {
477
477
  let settled = false;
478
- const proc = spawn(goBinary, ["run", scriptPath], {
478
+ const proc = spawn2(goBinary, ["run", scriptPath], {
479
479
  stdio: ["pipe", "pipe", "pipe"],
480
480
  windowsHide: true
481
481
  });
@@ -1078,10 +1078,10 @@ __export(py_parser_exports, {
1078
1078
  detectLang: () => detectLang,
1079
1079
  parseSymbols: () => parseSymbols4
1080
1080
  });
1081
- import { spawn as spawn2 } from "node:child_process";
1082
- import * as fs4 from "node:fs/promises";
1083
- import * as os2 from "node:os";
1084
- import * as path5 from "node:path";
1081
+ import { spawn as spawn3 } from "node:child_process";
1082
+ import * as fs8 from "node:fs/promises";
1083
+ import * as os3 from "node:os";
1084
+ import * as path9 from "node:path";
1085
1085
  async function parseSymbols4(opts) {
1086
1086
  const { file, content, lang } = opts;
1087
1087
  try {
@@ -1103,7 +1103,7 @@ async function resolvePython() {
1103
1103
  function commandIsAvailable(command) {
1104
1104
  return new Promise((resolve4) => {
1105
1105
  let settled = false;
1106
- const proc = spawn2(command, ["--version"], {
1106
+ const proc = spawn3(command, ["--version"], {
1107
1107
  stdio: "ignore",
1108
1108
  windowsHide: true
1109
1109
  });
@@ -1125,7 +1125,7 @@ function commandIsAvailable(command) {
1125
1125
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
1126
1126
  return new Promise((resolve4, reject) => {
1127
1127
  let settled = false;
1128
- const proc = spawn2(pyBinary, [scriptPath, filePath], {
1128
+ const proc = spawn3(pyBinary, [scriptPath, filePath], {
1129
1129
  stdio: ["pipe", "pipe", "pipe"],
1130
1130
  windowsHide: true
1131
1131
  });
@@ -1159,10 +1159,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
1159
1159
  async function syncPyParse(filePath, content, lang) {
1160
1160
  try {
1161
1161
  if (!_cachedScriptPath) {
1162
- const tmpDir = path5.join(os2.tmpdir(), "ws-py-parse");
1163
- await fs4.mkdir(tmpDir, { recursive: true });
1164
- _cachedScriptPath = path5.join(tmpDir, "parse.py");
1165
- await fs4.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
1162
+ const tmpDir = path9.join(os3.tmpdir(), "ws-py-parse");
1163
+ await fs8.mkdir(tmpDir, { recursive: true });
1164
+ _cachedScriptPath = path9.join(tmpDir, "parse.py");
1165
+ await fs8.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
1166
1166
  }
1167
1167
  cachedPyBinary ??= resolvePython();
1168
1168
  const pyBinary = await cachedPyBinary;
@@ -1416,10 +1416,10 @@ __export(rs_parser_exports, {
1416
1416
  detectLang: () => detectLang,
1417
1417
  parseSymbols: () => parseSymbols5
1418
1418
  });
1419
- import { expectDefined } from "@wrongstack/core/utils";
1420
- import { execFile, spawn as spawn3 } from "node:child_process";
1421
- import * as fs5 from "node:fs/promises";
1422
- import * as path6 from "node:path";
1419
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
1420
+ import { execFile, spawn as spawn4 } from "node:child_process";
1421
+ import * as fs9 from "node:fs/promises";
1422
+ import * as path10 from "node:path";
1423
1423
  async function parseSymbols5(opts) {
1424
1424
  const { file, content, lang } = opts;
1425
1425
  const nativeAvailable = await checkNativeParser();
@@ -1441,7 +1441,7 @@ function checkNativeParser() {
1441
1441
  nativeParserAvailability ??= (async () => {
1442
1442
  try {
1443
1443
  await probe("rustc", ["--version"]);
1444
- const toolsDir = path6.join(process.cwd(), "tools");
1444
+ const toolsDir = path10.join(process.cwd(), "tools");
1445
1445
  await probe(
1446
1446
  "cargo",
1447
1447
  [
@@ -1450,7 +1450,7 @@ function checkNativeParser() {
1450
1450
  "--format-version",
1451
1451
  "1",
1452
1452
  "--manifest-path",
1453
- path6.join(toolsDir, "Cargo.toml")
1453
+ path10.join(toolsDir, "Cargo.toml")
1454
1454
  ]
1455
1455
  );
1456
1456
  return true;
@@ -1462,17 +1462,17 @@ function checkNativeParser() {
1462
1462
  }
1463
1463
  async function tryNativeParse(file, content) {
1464
1464
  try {
1465
- const toolsDir = path6.join(process.cwd(), "tools");
1466
- const crateDir = path6.join(toolsDir, "syn-parser");
1467
- const tmpFile = path6.join(crateDir, "src", "input.rs");
1468
- await fs5.writeFile(tmpFile, content, "utf8");
1465
+ const toolsDir = path10.join(process.cwd(), "tools");
1466
+ const crateDir = path10.join(toolsDir, "syn-parser");
1467
+ const tmpFile = path10.join(crateDir, "src", "input.rs");
1468
+ await fs9.writeFile(tmpFile, content, "utf8");
1469
1469
  const cargoBinary = resolveWin32Command("cargo");
1470
1470
  const result = await new Promise(
1471
1471
  (resolve4, reject) => {
1472
1472
  let settled = false;
1473
- const proc = spawn3(
1473
+ const proc = spawn4(
1474
1474
  cargoBinary,
1475
- ["run", "--manifest-path", path6.join(toolsDir, "Cargo.toml")],
1475
+ ["run", "--manifest-path", path10.join(toolsDir, "Cargo.toml")],
1476
1476
  {
1477
1477
  cwd: process.cwd(),
1478
1478
  stdio: ["pipe", "pipe", "pipe"],
@@ -1531,7 +1531,7 @@ function regexParse(opts) {
1531
1531
  let hi = lineOffsets.length - 1;
1532
1532
  while (lo < hi) {
1533
1533
  const mid = lo + hi + 1 >>> 1;
1534
- if (expectDefined(lineOffsets[mid]) <= offset) lo = mid;
1534
+ if (expectDefined2(lineOffsets[mid]) <= offset) lo = mid;
1535
1535
  else hi = mid - 1;
1536
1536
  }
1537
1537
  return lo + 1;
@@ -1543,7 +1543,7 @@ function regexParse(opts) {
1543
1543
  for (const pattern of RS_PATTERNS) {
1544
1544
  pattern.regex.lastIndex = 0;
1545
1545
  for (let match = pattern.regex.exec(content); match !== null; match = pattern.regex.exec(content)) {
1546
- const name = expectDefined(match[1]);
1546
+ const name = expectDefined2(match[1]);
1547
1547
  const offset = match.index ?? 0;
1548
1548
  const line = lineFromOffset(offset);
1549
1549
  const col = offset - (lineOffsets[line - 1] ?? 0);
@@ -1600,8 +1600,8 @@ __export(json_parser_exports, {
1600
1600
  detectLang: () => detectLang,
1601
1601
  parseSymbols: () => parseSymbols6
1602
1602
  });
1603
- import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
1604
- import * as path7 from "node:path";
1603
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
1604
+ import * as path11 from "node:path";
1605
1605
  function parseSymbols6(opts) {
1606
1606
  const { file, content, lang } = opts;
1607
1607
  try {
@@ -1613,7 +1613,7 @@ function parseSymbols6(opts) {
1613
1613
  function regexParse2(opts) {
1614
1614
  const { file, content, lang } = opts;
1615
1615
  const symbols = [];
1616
- const basename5 = path7.basename(file).toLowerCase();
1616
+ const basename5 = path11.basename(file).toLowerCase();
1617
1617
  const isPackageJson = basename5 === "package.json";
1618
1618
  const isTsconfig = basename5 === "tsconfig.json" || basename5 === "tsconfig.build.json";
1619
1619
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -1628,22 +1628,22 @@ function regexParse2(opts) {
1628
1628
  let hi = lineOffsets.length - 1;
1629
1629
  while (lo < hi) {
1630
1630
  const mid = lo + hi + 1 >>> 1;
1631
- if (expectDefined2(lineOffsets[mid]) <= offset) lo = mid;
1631
+ if (expectDefined3(lineOffsets[mid]) <= offset) lo = mid;
1632
1632
  else hi = mid - 1;
1633
1633
  }
1634
1634
  return lo + 1;
1635
1635
  }
1636
1636
  const rootMatch = content.match(/^\s*\{/m);
1637
1637
  if (rootMatch) {
1638
- const offset = expectDefined2(rootMatch.index);
1638
+ const offset = expectDefined3(rootMatch.index);
1639
1639
  const line = lineFromOffset(offset);
1640
1640
  symbols.push(
1641
1641
  makeSymbol({
1642
- name: path7.basename(file),
1642
+ name: path11.basename(file),
1643
1643
  kind: "object",
1644
1644
  line,
1645
1645
  col: 0,
1646
- signature: `"${path7.basename(file)}" = { ... }`,
1646
+ signature: `"${path11.basename(file)}" = { ... }`,
1647
1647
  file,
1648
1648
  lang
1649
1649
  })
@@ -1651,7 +1651,7 @@ function regexParse2(opts) {
1651
1651
  }
1652
1652
  const topLevelKeyRegex = /^\s*"([^"]+)"\s*:/gm;
1653
1653
  for (let match = topLevelKeyRegex.exec(content); match !== null; match = topLevelKeyRegex.exec(content)) {
1654
- const key = expectDefined2(match[1]);
1654
+ const key = expectDefined3(match[1]);
1655
1655
  const offset = match.index ?? 0;
1656
1656
  const line = lineFromOffset(offset);
1657
1657
  const col = offset - (lineOffsets[line - 1] ?? 0);
@@ -1698,7 +1698,7 @@ function regexParse2(opts) {
1698
1698
  const defsRegex = /"\$defs"\s*:|"\$defs"\s*:/g;
1699
1699
  const defsMatch = defsRegex.exec(content);
1700
1700
  if (defsMatch !== null) {
1701
- const offset = expectDefined2(defsMatch.index);
1701
+ const offset = expectDefined3(defsMatch.index);
1702
1702
  const line = lineFromOffset(offset);
1703
1703
  symbols.push(
1704
1704
  makeSymbol({
@@ -1723,7 +1723,7 @@ function regexParse2(opts) {
1723
1723
  for (let match = pat.exec(content); match !== null; match = pat.exec(content)) {
1724
1724
  const offset = match.index ?? 0;
1725
1725
  const line = lineFromOffset(offset);
1726
- const key = match[0]?.match(/"([^"]+)"/)?.[1] ?? expectDefined2(match[0]);
1726
+ const key = match[0]?.match(/"([^"]+)"/)?.[1] ?? expectDefined3(match[0]);
1727
1727
  symbols.push(
1728
1728
  makeSymbol({
1729
1729
  name: key,
@@ -1742,12 +1742,12 @@ function regexParse2(opts) {
1742
1742
  function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFromOffset) {
1743
1743
  const scriptsBlockRegex = /"scripts"\s*:\s*\{([^}]+)\}/g;
1744
1744
  for (let match = scriptsBlockRegex.exec(content); match !== null; match = scriptsBlockRegex.exec(content)) {
1745
- const blockContent = expectDefined2(match[0]);
1745
+ const blockContent = expectDefined3(match[0]);
1746
1746
  const blockOffset = match.index ?? 0;
1747
1747
  const scriptKeyRegex = /"(\w[\w-]*)"\s*:/g;
1748
1748
  for (let scriptMatch = scriptKeyRegex.exec(blockContent); scriptMatch !== null; scriptMatch = scriptKeyRegex.exec(blockContent)) {
1749
- const key = expectDefined2(scriptMatch[1]);
1750
- const keyOffset = blockOffset + expectDefined2(scriptMatch.index);
1749
+ const key = expectDefined3(scriptMatch[1]);
1750
+ const keyOffset = blockOffset + expectDefined3(scriptMatch.index);
1751
1751
  const line = lineFromOffset(keyOffset);
1752
1752
  symbols.push(
1753
1753
  makeSymbol({
@@ -1766,12 +1766,12 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets, lineFr
1766
1766
  function extractCompilerOptions(content, symbols, file, lang, lineOffsets, parentLine, lineFromOffset) {
1767
1767
  const optsBlockRegex = /"compilerOptions"\s*:\s*\{([^}]+)\}/g;
1768
1768
  for (let match = optsBlockRegex.exec(content); match !== null; match = optsBlockRegex.exec(content)) {
1769
- const blockContent = expectDefined2(match[0]);
1769
+ const blockContent = expectDefined3(match[0]);
1770
1770
  const blockOffset = match.index ?? 0;
1771
1771
  const optKeyRegex = /"(\w[\w]*)"\s*:/g;
1772
1772
  for (let optMatch = optKeyRegex.exec(blockContent); optMatch !== null; optMatch = optKeyRegex.exec(blockContent)) {
1773
- const key = expectDefined2(optMatch[1]);
1774
- const keyOffset = blockOffset + expectDefined2(optMatch.index);
1773
+ const key = expectDefined3(optMatch[1]);
1774
+ const keyOffset = blockOffset + expectDefined3(optMatch.index);
1775
1775
  const line = lineFromOffset(keyOffset);
1776
1776
  if (line <= parentLine) continue;
1777
1777
  symbols.push(
@@ -1816,7 +1816,7 @@ __export(yaml_parser_exports, {
1816
1816
  detectLang: () => detectLang,
1817
1817
  parseSymbols: () => parseSymbols7
1818
1818
  });
1819
- import { expectDefined as expectDefined3, truncate } from "@wrongstack/core/utils";
1819
+ import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
1820
1820
  function parseSymbols7(opts) {
1821
1821
  const { file, content, lang } = opts;
1822
1822
  try {
@@ -1838,14 +1838,14 @@ function regexParse3(opts) {
1838
1838
  let hi = lineOffsets.length - 1;
1839
1839
  while (lo < hi) {
1840
1840
  const mid = lo + hi + 1 >>> 1;
1841
- if (expectDefined3(lineOffsets[mid]) <= offset) lo = mid;
1841
+ if (expectDefined4(lineOffsets[mid]) <= offset) lo = mid;
1842
1842
  else hi = mid - 1;
1843
1843
  }
1844
1844
  return lo + 1;
1845
1845
  }
1846
1846
  const anchorRegex = /&(\w[\w-]*)/g;
1847
1847
  for (let match = anchorRegex.exec(content); match !== null; match = anchorRegex.exec(content)) {
1848
- const name = expectDefined3(match[1]);
1848
+ const name = expectDefined4(match[1]);
1849
1849
  const offset = match.index ?? 0;
1850
1850
  const line = lineFromOffset(offset);
1851
1851
  const col = offset - (lineOffsets[line - 1] ?? 0);
@@ -1863,7 +1863,7 @@ function regexParse3(opts) {
1863
1863
  }
1864
1864
  const aliasRegex = /\*(\w[\w-]*)/g;
1865
1865
  for (let match = aliasRegex.exec(content); match !== null; match = aliasRegex.exec(content)) {
1866
- const name = expectDefined3(match[1]);
1866
+ const name = expectDefined4(match[1]);
1867
1867
  const offset = match.index ?? 0;
1868
1868
  const line = lineFromOffset(offset);
1869
1869
  const col = offset - (lineOffsets[line - 1] ?? 0);
@@ -1898,7 +1898,7 @@ function regexParse3(opts) {
1898
1898
  }
1899
1899
  const listItemRegex = /^-(\s+)([^:#\s][^:#\s]*)\s*:/gm;
1900
1900
  for (let match = listItemRegex.exec(content); match !== null; match = listItemRegex.exec(content)) {
1901
- const key = expectDefined3(match[2]);
1901
+ const key = expectDefined4(match[2]);
1902
1902
  const offset = match.index ?? 0;
1903
1903
  const line = lineFromOffset(offset);
1904
1904
  const col = offset - (lineOffsets[line - 1] ?? 0);
@@ -1918,7 +1918,7 @@ function regexParse3(opts) {
1918
1918
  }
1919
1919
  const blockScalarRegex = /^(\s*)([^:#\s][^:#\s]*)\s*:\s*[|>](\s|$)/gm;
1920
1920
  for (let match = blockScalarRegex.exec(content); match !== null; match = blockScalarRegex.exec(content)) {
1921
- const key = expectDefined3(match[2]);
1921
+ const key = expectDefined4(match[2]);
1922
1922
  const offset = match.index ?? 0;
1923
1923
  const line = lineFromOffset(offset);
1924
1924
  const col = offset - (lineOffsets[line - 1] ?? 0);
@@ -1970,10 +1970,12 @@ var init_yaml_parser = __esm({
1970
1970
  }
1971
1971
  });
1972
1972
 
1973
- // src/codebase-index/background-indexer.ts
1974
- import * as fs11 from "node:fs";
1975
- import { fileURLToPath as fileURLToPath3 } from "node:url";
1976
- import { Worker } from "node:worker_threads";
1973
+ // src/codebase-index/project-server-client.ts
1974
+ import { spawn } from "node:child_process";
1975
+ import * as fs4 from "node:fs";
1976
+ import * as net from "node:net";
1977
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1978
+ import { checkUnixSocketPath } from "@wrongstack/core/utils";
1977
1979
 
1978
1980
  // src/codebase-index/circuit-breaker.ts
1979
1981
  var CircuitOpenError = class extends Error {
@@ -2058,117 +2060,18 @@ function resetIndexCircuitBreaker() {
2058
2060
  indexCircuitBreaker.reset();
2059
2061
  }
2060
2062
 
2061
- // src/codebase-index/indexer.ts
2062
- import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
2063
- import { execFile as execFile2 } from "node:child_process";
2064
- import * as fs8 from "node:fs/promises";
2065
- import { availableParallelism } from "node:os";
2066
- import * as path11 from "node:path";
2067
- import {
2068
- DEFAULT_WALK_IGNORE_DIRS,
2069
- indexParallelBatchSize,
2070
- isFrugalPerf
2071
- } from "@wrongstack/core/utils";
2072
-
2073
- // src/codebase-index/gitignore.ts
2074
- import * as fs from "node:fs/promises";
2075
- import * as path from "node:path";
2076
- import { compileGlob } from "@wrongstack/core/utils";
2077
- function globBody(glob) {
2078
- return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
2079
- }
2080
- function compileGitignore(lines) {
2081
- const rules = [];
2082
- for (const raw of lines) {
2083
- let line = raw.replace(/\r$/, "");
2084
- if (!line.trim() || line.trimStart().startsWith("#")) continue;
2085
- line = line.trim();
2086
- let negated = false;
2087
- if (line.startsWith("!")) {
2088
- negated = true;
2089
- line = line.slice(1);
2090
- }
2091
- let dirOnly = false;
2092
- if (line.endsWith("/")) {
2093
- dirOnly = true;
2094
- line = line.slice(0, -1);
2095
- }
2096
- if (!line) continue;
2097
- const anchored = line.startsWith("/") || line.includes("/");
2098
- if (line.startsWith("/")) line = line.slice(1);
2099
- const body = globBody(line);
2100
- const prefix = anchored ? "^" : "(?:^|.*/)";
2101
- rules.push({
2102
- eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
2103
- under: new RegExp(`${prefix}${body}/.*$`),
2104
- negated,
2105
- dirOnly
2106
- });
2107
- }
2108
- return (relPath, isDir) => {
2109
- const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
2110
- let ignored = false;
2111
- for (const r of rules) {
2112
- const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
2113
- if (re.test(p)) ignored = !r.negated;
2114
- }
2115
- return ignored;
2116
- };
2117
- }
2118
- async function loadGitignoreMatcher(projectRoot) {
2119
- let lines = [];
2120
- try {
2121
- const raw = await fs.readFile(path.join(projectRoot, ".gitignore"), "utf8");
2122
- lines = raw.split("\n");
2123
- } catch {
2124
- }
2125
- return compileGitignore(lines);
2126
- }
2127
-
2128
- // src/codebase-index/indexer.ts
2129
- init_languages();
2130
-
2131
- // src/codebase-index/parser-dispatch.ts
2132
- async function parseFileContent(file, content, lang) {
2133
- switch (lang) {
2134
- case "ts":
2135
- case "tsx":
2136
- case "js":
2137
- case "jsx": {
2138
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
2139
- return parseSymbols8({ file, content, lang });
2140
- }
2141
- case "go": {
2142
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
2143
- return parseSymbols8({ file, content, lang: "go" });
2144
- }
2145
- case "py": {
2146
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
2147
- return parseSymbols8({ file, content, lang: "py" });
2148
- }
2149
- case "rs": {
2150
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
2151
- return parseSymbols8({ file, content, lang: "rs" });
2152
- }
2153
- case "json": {
2154
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
2155
- return parseSymbols8({ file, content, lang: "json" });
2156
- }
2157
- case "yaml": {
2158
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
2159
- return parseSymbols8({ file, content, lang: "yaml" });
2160
- }
2161
- default: {
2162
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
2163
- return parseSymbols8({ file, content, lang });
2164
- }
2165
- }
2166
- }
2063
+ // src/codebase-index/project-server-endpoint.ts
2064
+ import { createHash } from "node:crypto";
2065
+ import * as fs3 from "node:fs";
2066
+ import * as os from "node:os";
2067
+ import * as path4 from "node:path";
2068
+ import { fileURLToPath } from "node:url";
2069
+ import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
2167
2070
 
2168
2071
  // src/codebase-index/writer.ts
2169
- import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
2170
- import * as fs7 from "node:fs";
2171
- import * as path10 from "node:path";
2072
+ import { expectDefined } from "@wrongstack/core/utils";
2073
+ import * as fs2 from "node:fs";
2074
+ import * as path3 from "node:path";
2172
2075
 
2173
2076
  // src/codebase-index/bm25.ts
2174
2077
  var K1 = 1.5;
@@ -2392,8 +2295,8 @@ function runSqliteWithRetry(fn) {
2392
2295
  }
2393
2296
 
2394
2297
  // src/codebase-index/writer-admin.ts
2395
- import * as fs6 from "node:fs";
2396
- import * as path8 from "node:path";
2298
+ import * as fs from "node:fs";
2299
+ import * as path from "node:path";
2397
2300
  var DB_FILE = "index.db";
2398
2301
  function getAllIndexableWithStatement(stmt) {
2399
2302
  return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
@@ -2452,7 +2355,7 @@ function getAllFileMetasWithStatement(stmt) {
2452
2355
  }
2453
2356
  function getIndexDbSizeBytes(indexDir) {
2454
2357
  try {
2455
- return fs6.statSync(path8.join(indexDir, DB_FILE)).size;
2358
+ return fs.statSync(path.join(indexDir, DB_FILE)).size;
2456
2359
  } catch {
2457
2360
  return 0;
2458
2361
  }
@@ -2519,7 +2422,7 @@ function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
2519
2422
  }
2520
2423
 
2521
2424
  // src/codebase-index/writer-graph-helpers.ts
2522
- import * as path9 from "node:path";
2425
+ import * as path2 from "node:path";
2523
2426
  function derivePackage(filePath) {
2524
2427
  const f = filePath.replace(/\\/g, "/");
2525
2428
  const pkgsIdx = f.indexOf("/packages/");
@@ -2634,16 +2537,16 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
2634
2537
  function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
2635
2538
  if (!moduleName.startsWith(".")) return void 0;
2636
2539
  const normalizedFrom = fromFile.replace(/\\/g, "/");
2637
- const absolute = path9.posix.normalize(
2638
- path9.posix.join(path9.posix.dirname(normalizedFrom), moduleName)
2540
+ const absolute = path2.posix.normalize(
2541
+ path2.posix.join(path2.posix.dirname(normalizedFrom), moduleName)
2639
2542
  );
2640
- const extension = path9.posix.extname(absolute);
2543
+ const extension = path2.posix.extname(absolute);
2641
2544
  const base = extension ? absolute.slice(0, -extension.length) : absolute;
2642
2545
  const candidates = [
2643
2546
  absolute,
2644
2547
  ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
2645
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path9.posix.join(absolute, `index${ext}`)),
2646
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path9.posix.join(base, `index${ext}`))
2548
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path2.posix.join(absolute, `index${ext}`)),
2549
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path2.posix.join(base, `index${ext}`))
2647
2550
  ];
2648
2551
  const indexedByPortablePath = new Map(
2649
2552
  [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
@@ -3159,9 +3062,9 @@ var IndexStore = class _IndexStore {
3159
3062
  }
3160
3063
  constructor(projectRoot, opts = {}) {
3161
3064
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
3162
- fs7.mkdirSync(this.indexDir, { recursive: true });
3065
+ fs2.mkdirSync(this.indexDir, { recursive: true });
3163
3066
  const Database = loadDatabaseSync();
3164
- this.db = new Database(path10.join(this.indexDir, DB_FILE2));
3067
+ this.db = new Database(path3.join(this.indexDir, DB_FILE2));
3165
3068
  applyIndexStorePragmas(this.db);
3166
3069
  this.initSchema();
3167
3070
  }
@@ -3576,13 +3479,13 @@ var IndexStore = class _IndexStore {
3576
3479
  if (rankDiff !== 0) return rankDiff;
3577
3480
  const scoreDiff = b.score - a.score;
3578
3481
  if (scoreDiff !== 0) return scoreDiff;
3579
- const left = expectDefined4(candidateById.get(a.id));
3580
- const right = expectDefined4(candidateById.get(b.id));
3482
+ const left = expectDefined(candidateById.get(a.id));
3483
+ const right = expectDefined(candidateById.get(b.id));
3581
3484
  return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
3582
3485
  });
3583
3486
  const qTokens = tokenise(query);
3584
3487
  const results = scored.slice(0, limit).map(({ id, score }) => {
3585
- const c = expectDefined4(candidateById.get(id));
3488
+ const c = expectDefined(candidateById.get(id));
3586
3489
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
3587
3490
  });
3588
3491
  return { results, total: candidates.length };
@@ -3996,721 +3899,212 @@ var indexStorePool = new StorePool(
3996
3899
  (projectRoot, opts) => new IndexStore(projectRoot, opts)
3997
3900
  );
3998
3901
 
3999
- // src/codebase-index/indexer.ts
4000
- var YIELD_EVERY_N = 50;
4001
- function resolveParallelBatch() {
4002
- return indexParallelBatchSize(availableParallelism());
4003
- }
4004
- function yieldEventLoop() {
4005
- return new Promise((resolve4) => setImmediate(resolve4));
3902
+ // src/codebase-index/project-server-endpoint.ts
3903
+ var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
3904
+ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
3905
+ var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
3906
+ var buildIdCache;
3907
+ function projectIndexServerBuildId(entrypoint) {
3908
+ const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path4.resolve(entrypoint);
3909
+ try {
3910
+ const stat2 = fs3.statSync(file);
3911
+ if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat2.mtimeMs && buildIdCache.size === stat2.size) {
3912
+ return buildIdCache.buildId;
3913
+ }
3914
+ const buildId = createHash("sha256").update(fs3.readFileSync(file)).digest("hex").slice(0, 24);
3915
+ buildIdCache = { file, mtimeMs: stat2.mtimeMs, size: stat2.size, buildId };
3916
+ return buildId;
3917
+ } catch {
3918
+ return `unreadable:${path4.basename(file)}`;
3919
+ }
4006
3920
  }
4007
- function throwIfAborted(signal) {
4008
- if (!signal?.aborted) return;
4009
- if (signal.reason instanceof Error) throw signal.reason;
4010
- throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
3921
+ function normalizeLocalPath(value) {
3922
+ const resolved = path4.resolve(value);
3923
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
4011
3924
  }
4012
- function isAbortError(err) {
4013
- return err instanceof DOMException && err.name === "AbortError";
3925
+ function projectIndexServerKey(projectRoot, indexDir) {
3926
+ const resolvedIndexDir = normalizeLocalPath(resolveIndexDir(projectRoot, indexDir));
3927
+ return createHash("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
4014
3928
  }
4015
- var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
4016
- var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
4017
- var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
4018
- var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
4019
- var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
4020
- function isWithinProject(projectRoot, file) {
4021
- const rel = path11.relative(projectRoot, file);
4022
- return rel !== "" && !rel.startsWith(`..${path11.sep}`) && rel !== ".." && !path11.isAbsolute(rel);
3929
+ function projectIndexServerEndpoint(projectRoot, indexDir) {
3930
+ const key = projectIndexServerKey(projectRoot, indexDir);
3931
+ if (process.platform === "win32") {
3932
+ return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
3933
+ }
3934
+ return path4.join(os.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
4023
3935
  }
4024
- function isMissingPathError(err) {
4025
- const code = err?.code;
4026
- return code === "ENOENT" || code === "ENOTDIR";
3936
+ function projectIndexServerMetadataPath(projectRoot, indexDir) {
3937
+ return path4.join(
3938
+ path4.resolve(resolveIndexDir(projectRoot, indexDir)),
3939
+ PROJECT_INDEX_SERVER_METADATA_FILE
3940
+ );
4027
3941
  }
4028
- function normalizeComparablePath(value) {
4029
- const resolved = path11.resolve(value);
4030
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
3942
+
3943
+ // src/codebase-index/project-server-protocol.ts
3944
+ var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
3945
+ function encodeProjectServerMessage(message) {
3946
+ return `${JSON.stringify(message)}
3947
+ `;
4031
3948
  }
4032
- function gitOutput(projectRoot, args) {
4033
- return new Promise((resolve4, reject) => {
4034
- execFile2(
4035
- "git",
4036
- ["-C", projectRoot, ...args],
4037
- {
4038
- encoding: "buffer",
4039
- maxBuffer: MAX_GIT_FILE_LIST_BYTES,
4040
- windowsHide: true
4041
- },
4042
- (error, stdout) => {
4043
- if (error) reject(error);
4044
- else resolve4(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
4045
- }
4046
- );
4047
- });
4048
- }
4049
- async function findGitSourceFiles(projectRoot, ignore, signal) {
4050
- try {
4051
- throwIfAborted(signal);
4052
- const topLevel = (await gitOutput(projectRoot, ["rev-parse", "--show-toplevel"])).toString("utf8").trim();
4053
- if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
4054
- throwIfAborted(signal);
4055
- const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
4056
- const [output, statusOutput] = await Promise.all([
4057
- gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
4058
- gitOutput(projectRoot, [
4059
- "status",
4060
- "--porcelain=v1",
4061
- "-z",
4062
- "--untracked-files=all",
4063
- "--ignored=no"
4064
- ])
4065
- ]);
4066
- throwIfAborted(signal);
4067
- const dirty = /* @__PURE__ */ new Set();
4068
- const deleted = /* @__PURE__ */ new Set();
4069
- const statusRecords = statusOutput.toString("utf8").split("\0");
4070
- for (let i = 0; i < statusRecords.length; i++) {
4071
- const record = statusRecords[i];
4072
- if (!record) continue;
4073
- const status = record.slice(0, 2);
4074
- const changedPath = path11.resolve(projectRoot, record.slice(3));
4075
- dirty.add(changedPath);
4076
- if (status.includes("D")) deleted.add(changedPath);
4077
- if (status.includes("R") || status.includes("C")) {
4078
- const source = statusRecords[++i];
4079
- if (source) dirty.add(path11.resolve(projectRoot, source));
3949
+
3950
+ // src/codebase-index/project-server-client.ts
3951
+ var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
3952
+ var SERVER_START_TIMEOUT_MS = 1e4;
3953
+ var SERVER_CONTROL_TIMEOUT_MS = 5e3;
3954
+ var SERVER_HEALTH_TIMEOUT_MS = 3e3;
3955
+ var SERVER_HEARTBEAT_INTERVAL_MS = 1e4;
3956
+ var StaleProjectIndexServerError = class extends Error {
3957
+ constructor(message, pid) {
3958
+ super(message);
3959
+ this.pid = pid;
3960
+ }
3961
+ pid;
3962
+ name = "StaleProjectIndexServerError";
3963
+ };
3964
+ var connectionStates = /* @__PURE__ */ new Map();
3965
+ var connectionStateListeners = /* @__PURE__ */ new Set();
3966
+ var latestConnectionState = {
3967
+ status: "offline",
3968
+ connected: false
3969
+ };
3970
+ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
3971
+ if (process.env["WRONGSTACK_INDEX_INLINE"] || process.env["WRONGSTACK_INDEX_SERVER"] === "0") {
3972
+ return { kind: "inline-requested" };
3973
+ }
3974
+ let builtUrl = null;
3975
+ for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
3976
+ try {
3977
+ const url = new URL(rel, import.meta.url);
3978
+ if (url.protocol === "file:" && fs4.existsSync(fileURLToPath2(url))) {
3979
+ builtUrl = url;
3980
+ break;
4080
3981
  }
3982
+ } catch {
4081
3983
  }
4082
- const files = [];
4083
- for (const relative2 of output.toString("utf8").split("\0")) {
4084
- if (!relative2) continue;
4085
- const portable = relative2.replace(/\\/g, "/");
4086
- if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path11.posix.basename(portable))) {
4087
- continue;
4088
- }
4089
- const full = path11.resolve(projectRoot, relative2);
4090
- if (deleted.has(full)) continue;
4091
- const ext = path11.extname(relative2).toLowerCase();
4092
- if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
3984
+ }
3985
+ if (builtUrl === null) return { kind: "missing-build" };
3986
+ if (projectRoot !== void 0) {
3987
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
3988
+ const check = checkUnixSocketPath(endpoint);
3989
+ if (!check.ok) {
3990
+ return {
3991
+ kind: "endpoint-invalid",
3992
+ endpoint,
3993
+ byteLength: check.byteLength,
3994
+ maxBytes: check.maxBytes
3995
+ };
4093
3996
  }
4094
- return {
4095
- files,
4096
- trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
4097
- };
4098
- } catch {
4099
- return null;
4100
3997
  }
3998
+ return { kind: "available", url: builtUrl };
4101
3999
  }
4102
- async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
4103
- const gitFiles = await findGitSourceFiles(projectRoot, ignore, signal);
4104
- if (gitFiles) {
4000
+ function resolveProjectServerUrl() {
4001
+ const availability = resolveProjectIndexDaemonAvailability();
4002
+ return availability.kind === "available" ? availability.url : null;
4003
+ }
4004
+ function projectIndexServerExpectedBuildId() {
4005
+ const override = process.env["WRONGSTACK_INDEX_SERVER_BUILD_ID"]?.trim();
4006
+ if (override) return override;
4007
+ const url = resolveProjectServerUrl();
4008
+ return url ? projectIndexServerBuildId(url) : null;
4009
+ }
4010
+ function isProjectIndexServerAvailable() {
4011
+ return resolveProjectServerUrl() !== null;
4012
+ }
4013
+ function publishConnectionState(endpoint, state) {
4014
+ connectionStates.set(endpoint, state);
4015
+ latestConnectionState = state;
4016
+ for (const listener of connectionStateListeners) listener(state);
4017
+ }
4018
+ function getProjectIndexServerConnectionState(projectRoot, indexDir) {
4019
+ if (projectRoot) {
4020
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
4021
+ const existing = connectionStates.get(endpoint);
4022
+ if (existing) return existing;
4023
+ if (!isProjectIndexServerAvailable()) {
4024
+ return { status: "unavailable", connected: false };
4025
+ }
4105
4026
  return {
4106
- files: gitFiles.files,
4107
- complete: true,
4108
- errors: [],
4109
- trustedUnchanged: gitFiles.trustedUnchanged
4027
+ status: "offline",
4028
+ connected: false,
4029
+ projectRoot,
4030
+ indexDir,
4031
+ endpoint
4110
4032
  };
4111
4033
  }
4112
- const results = [];
4113
- const errors = [];
4114
- let complete = true;
4115
- const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
4116
- const indexableExts = new Set(INDEXABLE_EXTENSIONS);
4117
- let dirCount = 0;
4118
- const walk = async (dir) => {
4119
- throwIfAborted(signal);
4120
- if (dirCount > 0 && dirCount % YIELD_EVERY_N === 0) {
4121
- await yieldEventLoop();
4122
- throwIfAborted(signal);
4123
- }
4124
- let entries;
4125
- try {
4126
- entries = await fs8.readdir(dir, { withFileTypes: true });
4127
- } catch (err) {
4128
- complete = false;
4129
- errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
4130
- return;
4131
- }
4132
- dirCount++;
4133
- for (const e of entries) {
4134
- if (ignoreSet.has(e.name)) continue;
4135
- const full = path11.join(dir, e.name);
4136
- const rel = path11.relative(projectRoot, full).replace(/\\/g, "/");
4137
- if (e.isDirectory()) {
4138
- if (isGitIgnored(rel, true)) continue;
4139
- await walk(full);
4140
- } else if (e.isFile()) {
4141
- if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
4142
- const ext = path11.extname(e.name).toLowerCase();
4143
- if (indexableExts.has(ext) || detectLang(full) !== null) {
4144
- results.push(full);
4145
- }
4146
- }
4147
- }
4148
- };
4149
- await walk(projectRoot);
4150
- return { files: results, complete, errors };
4034
+ if (latestConnectionState.endpoint) return latestConnectionState;
4035
+ if (!isProjectIndexServerAvailable()) return { status: "unavailable", connected: false };
4036
+ return latestConnectionState;
4151
4037
  }
4152
- function assignRefsToSymbols2(refs, symbols) {
4153
- if (refs.length === 0 || symbols.length === 0) return [];
4154
- const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
4155
- const seen = /* @__PURE__ */ new Set();
4156
- const assigned = [];
4157
- for (const ref of refs) {
4158
- let owner;
4159
- for (const symbol of ordered) {
4160
- if (symbol.line > ref.line) break;
4161
- owner = symbol;
4162
- }
4163
- if (!owner && ref.callType === "import") owner = ordered[0];
4164
- if (!owner || owner.id <= 0) continue;
4165
- const key = `${owner.id}:${ref.toName}:${ref.callType}`;
4166
- if (seen.has(key)) continue;
4167
- seen.add(key);
4168
- assigned.push({ ...ref, fromId: owner.id });
4169
- }
4170
- return assigned;
4038
+ function onProjectIndexServerConnectionStateChange(listener) {
4039
+ connectionStateListeners.add(listener);
4040
+ return () => connectionStateListeners.delete(listener);
4171
4041
  }
4172
- async function runIndexer(_ctx, opts) {
4173
- const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
4174
- try {
4175
- return await runIndexerWithStore(store, opts);
4176
- } finally {
4177
- try {
4178
- store.close();
4179
- } catch {
4180
- }
4181
- }
4042
+ function remoteError(message, name) {
4043
+ if (name === "LockError") return new LockError(message);
4044
+ if (name === "IndexTimeoutError") return new IndexTimeoutError(message);
4045
+ const error = new Error(message);
4046
+ if (name && name !== "Error") error.name = name;
4047
+ return error;
4182
4048
  }
4183
- async function runIndexerWithStore(store, opts) {
4184
- const { projectRoot, langs, ignore = [], signal } = opts;
4185
- const relationGraphVersion = "2";
4186
- const refResolutionVersion = "2";
4187
- const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
4188
- const needsFullRefResolution = force || store.getMetadata("ref_resolution_version") !== refResolutionVersion;
4189
- const startMs = Date.now();
4190
- const errors = [];
4191
- const langStats = {};
4192
- let filesIndexed = 0;
4193
- let symbolsIndexed = 0;
4194
- const isGitIgnored = await loadGitignoreMatcher(projectRoot);
4195
- let files;
4196
- let discoveredFiles = null;
4197
- let discoveryComplete = true;
4198
- let trustedUnchanged;
4199
- if (opts.files && opts.files.length > 0) {
4200
- files = opts.files.map((f) => path11.resolve(projectRoot, f)).filter((f) => {
4201
- if (!isWithinProject(projectRoot, f)) return false;
4202
- const rel = path11.relative(projectRoot, f).replace(/\\/g, "/");
4203
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path11.basename(f)) && !isGitIgnored(rel, false);
4204
- });
4205
- } else {
4206
- const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
4207
- files = discovery.files;
4208
- errors.push(...discovery.errors);
4209
- discoveryComplete = discovery.complete;
4210
- discoveredFiles = new Set(files);
4211
- trustedUnchanged = discovery.trustedUnchanged;
4049
+ function isProjectIndexServerHealth(value) {
4050
+ if (!value || typeof value !== "object") return false;
4051
+ const health = value;
4052
+ const memory = health.memory && typeof health.memory === "object" ? health.memory : void 0;
4053
+ const activity = health.activity && typeof health.activity === "object" ? health.activity : void 0;
4054
+ return typeof health.checkedAt === "number" && typeof health.uptimeMs === "number" && typeof memory?.rss === "number" && typeof memory.heapUsed === "number" && typeof memory.heapTotal === "number" && typeof memory.external === "number" && typeof health.clients === "number" && typeof health.activeRequests === "number" && typeof health.activeWrites === "number" && typeof health.queuedWrites === "number" && typeof health.pendingExternalFiles === "number" && typeof health.watchingExternal === "boolean" && typeof activity?.indexing === "boolean" && typeof activity.currentFile === "number" && typeof activity.totalFiles === "number" && typeof activity.generation === "number";
4055
+ }
4056
+ function delay(ms) {
4057
+ return new Promise((resolve4) => {
4058
+ const timer = setTimeout(resolve4, ms);
4059
+ timer.unref?.();
4060
+ });
4061
+ }
4062
+ function cancellationError(signal) {
4063
+ return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
4064
+ }
4065
+ var ProjectServerConnection = class {
4066
+ constructor(projectRoot, indexDir, endpoint) {
4067
+ this.projectRoot = projectRoot;
4068
+ this.indexDir = indexDir;
4069
+ this.endpoint = endpoint;
4070
+ this.transition("offline");
4212
4071
  }
4213
- if (langs && langs.length > 0) {
4214
- const langSet = new Set(langs);
4215
- files = files.filter((f) => {
4216
- const lang = detectLang(f);
4217
- return lang ? langSet.has(lang) : false;
4072
+ projectRoot;
4073
+ indexDir;
4074
+ endpoint;
4075
+ socket = null;
4076
+ buffer = "";
4077
+ info = null;
4078
+ activity = null;
4079
+ health = null;
4080
+ healthCheck = null;
4081
+ connecting = null;
4082
+ connectResolve = null;
4083
+ connectReject = null;
4084
+ nextId = 1;
4085
+ pending = /* @__PURE__ */ new Map();
4086
+ transition(status, options = {}) {
4087
+ const previous = connectionStates.get(this.endpoint);
4088
+ const pid = options.pid ?? (status === "connected" ? this.info?.pid : void 0);
4089
+ const lastError = options.error === void 0 ? status === "error" || status === "degraded" || status === "unresponsive" ? previous?.lastError : void 0 : options.error instanceof Error ? options.error.message : String(options.error);
4090
+ publishConnectionState(this.endpoint, {
4091
+ status,
4092
+ connected: status === "connected" || status === "degraded" || status === "unresponsive",
4093
+ projectRoot: this.projectRoot,
4094
+ indexDir: this.indexDir,
4095
+ endpoint: this.endpoint,
4096
+ pid,
4097
+ lastError,
4098
+ ...this.activity ? { activity: this.activity } : {},
4099
+ ...this.health ? { health: this.health } : {}
4218
4100
  });
4219
4101
  }
4220
- if (force) store.clearAll();
4221
- const existingMeta = /* @__PURE__ */ new Map();
4222
- if (!force) {
4223
- for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
4102
+ isConnected() {
4103
+ return this.socket !== null && !this.socket.destroyed && this.info !== null;
4224
4104
  }
4225
- const totalFilesForProgress = files.length;
4226
- let filesPreSkipped = 0;
4227
- if (!force && trustedUnchanged) {
4228
- files = files.filter((file) => {
4229
- const meta = existingMeta.get(file);
4230
- if (!meta || !trustedUnchanged.has(file)) return true;
4231
- langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
4232
- symbolsIndexed += meta.symbolCount;
4233
- filesIndexed++;
4234
- filesPreSkipped++;
4235
- return false;
4236
- });
4237
- if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
4238
- }
4239
- const parallelBatch = resolveParallelBatch();
4240
- let filesSinceLastYield = 0;
4241
- for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
4242
- const batchEnd = Math.min(batchStart + parallelBatch, files.length);
4243
- const batchFiles = files.slice(batchStart, batchEnd);
4244
- opts.onProgress?.(filesPreSkipped + batchEnd, totalFilesForProgress);
4245
- filesSinceLastYield += batchFiles.length;
4246
- if (filesSinceLastYield >= YIELD_EVERY_N) {
4247
- filesSinceLastYield = 0;
4248
- await yieldEventLoop();
4249
- if (isFrugalPerf()) {
4250
- await new Promise((r) => setTimeout(r, 8));
4251
- }
4252
- throwIfAborted(signal);
4253
- }
4254
- const statOpts = signal ? { signal } : {};
4255
- const statReadParse = await Promise.allSettled(
4256
- batchFiles.map(
4257
- async (file) => {
4258
- let stat2;
4259
- try {
4260
- stat2 = await fs8.stat(file, statOpts);
4261
- } catch (e) {
4262
- if (isAbortError(e)) throw e;
4263
- return {
4264
- file,
4265
- stat: null,
4266
- lang: "",
4267
- parsed: null,
4268
- error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
4269
- missing: isMissingPathError(e)
4270
- };
4271
- }
4272
- if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
4273
- const lang = detectLang(file);
4274
- if (!lang) return { file, stat: stat2, lang: "", parsed: null };
4275
- if (stat2.size > MAX_INDEX_FILE_BYTES) {
4276
- return {
4277
- file,
4278
- stat: stat2,
4279
- lang,
4280
- parsed: null,
4281
- error: `file too large (${stat2.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
4282
- };
4283
- }
4284
- const meta = existingMeta.get(file);
4285
- if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
4286
- return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
4287
- }
4288
- let content;
4289
- try {
4290
- content = await fs8.readFile(file, { encoding: "utf8", signal });
4291
- } catch (e) {
4292
- if (isAbortError(e)) throw e;
4293
- return {
4294
- file,
4295
- stat: stat2,
4296
- lang,
4297
- parsed: null,
4298
- error: `read error: ${e instanceof Error ? e.message : String(e)}`
4299
- };
4300
- }
4301
- let parsed;
4302
- try {
4303
- parsed = await parseFileContent(file, content, lang);
4304
- } catch (e) {
4305
- return {
4306
- file,
4307
- stat: stat2,
4308
- lang,
4309
- parsed: null,
4310
- error: `parse error: ${e instanceof Error ? e.message : String(e)}`
4311
- };
4312
- }
4313
- return { file, stat: stat2, lang, parsed, content };
4314
- }
4315
- )
4316
- );
4317
- const batchEntries = [];
4318
- const deleteForFiles = [];
4319
- for (let fi = 0; fi < statReadParse.length; fi++) {
4320
- const settled = statReadParse[fi];
4321
- const file = expectDefined5(batchFiles[fi]);
4322
- if (settled.status === "rejected") {
4323
- const err = settled.reason;
4324
- if (err instanceof Error && isAbortError(err)) throw err;
4325
- errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
4326
- continue;
4327
- }
4328
- const result = settled.value;
4329
- if (result.error) {
4330
- if (result.missing) store.deleteFile(file);
4331
- errors.push(`${file}: ${result.error}`);
4332
- continue;
4333
- }
4334
- const { stat: stat2, lang, parsed } = result;
4335
- if (result.skippedMeta) {
4336
- langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
4337
- symbolsIndexed += result.skippedMeta.symbolCount;
4338
- filesIndexed++;
4339
- continue;
4340
- }
4341
- if (!lang || !parsed) {
4342
- if (lang) {
4343
- store.upsertFile({
4344
- file,
4345
- lang,
4346
- mtimeMs: Math.floor(stat2.mtimeMs),
4347
- symbolCount: 0,
4348
- lastIndexed: Date.now()
4349
- });
4350
- filesIndexed++;
4351
- }
4352
- continue;
4353
- }
4354
- if (parsed.symbols.length === 0) {
4355
- store.replaceEmptyFile({
4356
- file,
4357
- lang,
4358
- mtimeMs: Math.floor(stat2.mtimeMs),
4359
- symbolCount: 0,
4360
- lastIndexed: Date.now()
4361
- });
4362
- filesIndexed++;
4363
- continue;
4364
- }
4365
- batchEntries.push({
4366
- file,
4367
- lang,
4368
- symbols: parsed.symbols,
4369
- refs: parsed.refs ?? [],
4370
- mtimeMs: Math.floor(stat2.mtimeMs),
4371
- symbolCount: parsed.symbols.length
4372
- });
4373
- deleteForFiles.push(file);
4374
- }
4375
- if (batchEntries.length > 0) {
4376
- try {
4377
- store.commitBatch(batchEntries, { deleteForFiles });
4378
- for (const entry of batchEntries) {
4379
- const count = entry.symbols.length;
4380
- symbolsIndexed += count;
4381
- langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
4382
- filesIndexed++;
4383
- }
4384
- } catch (err) {
4385
- const message = err instanceof Error ? err.message : String(err);
4386
- errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
4387
- for (const entry of batchEntries) {
4388
- try {
4389
- store.deleteRefsForFile(entry.file);
4390
- store.deleteSymbolsForFile(entry.file);
4391
- const symbolsWithIds = store.insertSymbols(entry.symbols);
4392
- symbolsIndexed += symbolsWithIds.length;
4393
- langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
4394
- filesIndexed++;
4395
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
4396
- const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
4397
- if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
4398
- }
4399
- store.resolveRefsForNames([
4400
- ...entry.symbols.map((symbol) => symbol.name),
4401
- ...entry.refs.map((ref) => ref.toName)
4402
- ]);
4403
- store.upsertFile({
4404
- file: entry.file,
4405
- lang: entry.lang,
4406
- mtimeMs: entry.mtimeMs,
4407
- symbolCount: entry.symbolCount,
4408
- lastIndexed: Date.now()
4409
- });
4410
- } catch (innerErr) {
4411
- errors.push(
4412
- `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
4413
- );
4414
- }
4415
- }
4416
- }
4417
- }
4418
- }
4419
- if (discoveredFiles && discoveryComplete) {
4420
- for (const [file_] of existingMeta) {
4421
- if (!discoveredFiles.has(file_)) {
4422
- store.deleteFile(file_);
4423
- }
4424
- }
4425
- }
4426
- if (needsFullRefResolution) store.resolveRefs();
4427
- store.setMetadata("ref_resolution_version", refResolutionVersion);
4428
- store.setMetadata("relation_graph_version", relationGraphVersion);
4429
- if (!opts.files || filesIndexed >= 50) store.optimize();
4430
- store.setLastIndexed(Date.now());
4431
- if (!opts.files) store.compactIfNeeded();
4432
- const durationMs = Date.now() - startMs;
4433
- return {
4434
- filesIndexed,
4435
- symbolsIndexed,
4436
- langStats,
4437
- durationMs,
4438
- errors
4439
- };
4440
- }
4441
-
4442
- // src/codebase-index/index-service.ts
4443
- async function indexService(args, hooks = {}) {
4444
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4445
- try {
4446
- return await runIndexerWithStore(store, {
4447
- projectRoot: args.projectRoot,
4448
- indexDir: args.indexDir,
4449
- files: args.files,
4450
- force: args.force,
4451
- langs: args.langs,
4452
- ignore: args.ignore,
4453
- signal: hooks.signal,
4454
- onProgress: hooks.onProgress
4455
- });
4456
- } finally {
4457
- indexStorePool.release(store);
4458
- }
4459
- }
4460
- function searchService(args) {
4461
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4462
- try {
4463
- return store.searchRanked(
4464
- args.query,
4465
- {
4466
- kind: args.kind,
4467
- lang: args.lang,
4468
- file: args.file,
4469
- lspKind: args.lspKind
4470
- },
4471
- args.limit
4472
- );
4473
- } finally {
4474
- indexStorePool.release(store);
4475
- }
4476
- }
4477
- function statsService(args) {
4478
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4479
- try {
4480
- return store.getStats();
4481
- } finally {
4482
- indexStorePool.release(store);
4483
- }
4484
- }
4485
- function packageGraphService(args) {
4486
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4487
- try {
4488
- return store.getPackageGraph();
4489
- } finally {
4490
- indexStorePool.release(store);
4491
- }
4492
- }
4493
- function fileGraphService(args) {
4494
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4495
- try {
4496
- return store.getFileGraph(args.packageFilter);
4497
- } finally {
4498
- indexStorePool.release(store);
4499
- }
4500
- }
4501
- function symbolGraphService(args) {
4502
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
4503
- try {
4504
- return store.getSymbolGraph(args.fileFilter);
4505
- } finally {
4506
- indexStorePool.release(store);
4507
- }
4508
- }
4509
-
4510
- // src/codebase-index/background-indexer.ts
4511
- init_languages();
4512
-
4513
- // src/codebase-index/project-server-client.ts
4514
- import { spawn as spawn4 } from "node:child_process";
4515
- import * as fs10 from "node:fs";
4516
- import * as net from "node:net";
4517
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4518
-
4519
- // src/codebase-index/project-server-endpoint.ts
4520
- import { createHash } from "node:crypto";
4521
- import * as fs9 from "node:fs";
4522
- import * as os3 from "node:os";
4523
- import * as path12 from "node:path";
4524
- import { fileURLToPath } from "node:url";
4525
- var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
4526
- var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
4527
- var buildIdCache;
4528
- function projectIndexServerBuildId(entrypoint) {
4529
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path12.resolve(entrypoint);
4530
- try {
4531
- const stat2 = fs9.statSync(file);
4532
- if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat2.mtimeMs && buildIdCache.size === stat2.size) {
4533
- return buildIdCache.buildId;
4534
- }
4535
- const buildId = createHash("sha256").update(fs9.readFileSync(file)).digest("hex").slice(0, 24);
4536
- buildIdCache = { file, mtimeMs: stat2.mtimeMs, size: stat2.size, buildId };
4537
- return buildId;
4538
- } catch {
4539
- return `unreadable:${path12.basename(file)}`;
4540
- }
4541
- }
4542
- function normalizeLocalPath(value) {
4543
- const resolved = path12.resolve(value);
4544
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
4545
- }
4546
- function projectIndexServerKey(projectRoot, indexDir) {
4547
- const resolvedIndexDir = normalizeLocalPath(resolveIndexDir(projectRoot, indexDir));
4548
- return createHash("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
4549
- }
4550
- function projectIndexServerEndpoint(projectRoot, indexDir) {
4551
- const key = projectIndexServerKey(projectRoot, indexDir);
4552
- if (process.platform === "win32") {
4553
- return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
4554
- }
4555
- return path12.join(
4556
- os3.tmpdir(),
4557
- `wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`,
4558
- `${key}.sock`
4559
- );
4560
- }
4561
- function projectIndexServerMetadataPath(projectRoot, indexDir) {
4562
- return path12.join(
4563
- path12.resolve(resolveIndexDir(projectRoot, indexDir)),
4564
- PROJECT_INDEX_SERVER_METADATA_FILE
4565
- );
4566
- }
4567
-
4568
- // src/codebase-index/project-server-protocol.ts
4569
- var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
4570
- function encodeProjectServerMessage(message) {
4571
- return `${JSON.stringify(message)}
4572
- `;
4573
- }
4574
-
4575
- // src/codebase-index/project-server-client.ts
4576
- var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
4577
- var SERVER_START_TIMEOUT_MS = 1e4;
4578
- var SERVER_CONTROL_TIMEOUT_MS = 5e3;
4579
- var SERVER_HEALTH_TIMEOUT_MS = 3e3;
4580
- var SERVER_HEARTBEAT_INTERVAL_MS = 1e4;
4581
- var StaleProjectIndexServerError = class extends Error {
4582
- constructor(message, pid) {
4583
- super(message);
4584
- this.pid = pid;
4585
- }
4586
- pid;
4587
- name = "StaleProjectIndexServerError";
4588
- };
4589
- var connectionStates = /* @__PURE__ */ new Map();
4590
- var connectionStateListeners = /* @__PURE__ */ new Set();
4591
- var latestConnectionState = {
4592
- status: "offline",
4593
- connected: false
4594
- };
4595
- function resolveProjectIndexDaemonAvailability() {
4596
- if (process.env["WRONGSTACK_INDEX_INLINE"] || process.env["WRONGSTACK_INDEX_SERVER"] === "0") {
4597
- return { kind: "inline-requested" };
4598
- }
4599
- for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
4600
- try {
4601
- const url = new URL(rel, import.meta.url);
4602
- if (url.protocol === "file:" && fs10.existsSync(fileURLToPath2(url))) {
4603
- return { kind: "available", url };
4604
- }
4605
- } catch {
4606
- }
4607
- }
4608
- return { kind: "missing-build" };
4609
- }
4610
- function resolveProjectServerUrl() {
4611
- const availability = resolveProjectIndexDaemonAvailability();
4612
- return availability.kind === "available" ? availability.url : null;
4613
- }
4614
- function projectIndexServerExpectedBuildId() {
4615
- const override = process.env["WRONGSTACK_INDEX_SERVER_BUILD_ID"]?.trim();
4616
- if (override) return override;
4617
- const url = resolveProjectServerUrl();
4618
- return url ? projectIndexServerBuildId(url) : null;
4619
- }
4620
- function isProjectIndexServerAvailable() {
4621
- return resolveProjectServerUrl() !== null;
4622
- }
4623
- function publishConnectionState(endpoint, state) {
4624
- connectionStates.set(endpoint, state);
4625
- latestConnectionState = state;
4626
- for (const listener of connectionStateListeners) listener(state);
4627
- }
4628
- function getProjectIndexServerConnectionState(projectRoot, indexDir) {
4629
- if (projectRoot) {
4630
- const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
4631
- const existing = connectionStates.get(endpoint);
4632
- if (existing) return existing;
4633
- if (!isProjectIndexServerAvailable()) {
4634
- return { status: "unavailable", connected: false };
4635
- }
4636
- return {
4637
- status: "offline",
4638
- connected: false,
4639
- projectRoot,
4640
- indexDir,
4641
- endpoint
4642
- };
4643
- }
4644
- if (latestConnectionState.endpoint) return latestConnectionState;
4645
- if (!isProjectIndexServerAvailable()) return { status: "unavailable", connected: false };
4646
- return latestConnectionState;
4647
- }
4648
- function onProjectIndexServerConnectionStateChange(listener) {
4649
- connectionStateListeners.add(listener);
4650
- return () => connectionStateListeners.delete(listener);
4651
- }
4652
- function remoteError(message, name) {
4653
- if (name === "LockError") return new LockError(message);
4654
- if (name === "IndexTimeoutError") return new IndexTimeoutError(message);
4655
- const error = new Error(message);
4656
- if (name && name !== "Error") error.name = name;
4657
- return error;
4658
- }
4659
- function isProjectIndexServerHealth(value) {
4660
- if (!value || typeof value !== "object") return false;
4661
- const health = value;
4662
- const memory = health.memory && typeof health.memory === "object" ? health.memory : void 0;
4663
- const activity = health.activity && typeof health.activity === "object" ? health.activity : void 0;
4664
- return typeof health.checkedAt === "number" && typeof health.uptimeMs === "number" && typeof memory?.rss === "number" && typeof memory.heapUsed === "number" && typeof memory.heapTotal === "number" && typeof memory.external === "number" && typeof health.clients === "number" && typeof health.activeRequests === "number" && typeof health.activeWrites === "number" && typeof health.queuedWrites === "number" && typeof health.pendingExternalFiles === "number" && typeof health.watchingExternal === "boolean" && typeof activity?.indexing === "boolean" && typeof activity.currentFile === "number" && typeof activity.totalFiles === "number" && typeof activity.generation === "number";
4665
- }
4666
- function delay(ms) {
4667
- return new Promise((resolve4) => {
4668
- const timer = setTimeout(resolve4, ms);
4669
- timer.unref?.();
4670
- });
4671
- }
4672
- function cancellationError(signal) {
4673
- return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
4674
- }
4675
- var ProjectServerConnection = class {
4676
- constructor(projectRoot, indexDir, endpoint) {
4677
- this.projectRoot = projectRoot;
4678
- this.indexDir = indexDir;
4679
- this.endpoint = endpoint;
4680
- this.transition("offline");
4681
- }
4682
- projectRoot;
4683
- indexDir;
4684
- endpoint;
4685
- socket = null;
4686
- buffer = "";
4687
- info = null;
4688
- activity = null;
4689
- health = null;
4690
- healthCheck = null;
4691
- connecting = null;
4692
- connectResolve = null;
4693
- connectReject = null;
4694
- nextId = 1;
4695
- pending = /* @__PURE__ */ new Map();
4696
- transition(status, options = {}) {
4697
- const previous = connectionStates.get(this.endpoint);
4698
- const pid = options.pid ?? (status === "connected" ? this.info?.pid : void 0);
4699
- const lastError = options.error === void 0 ? status === "error" || status === "degraded" || status === "unresponsive" ? previous?.lastError : void 0 : options.error instanceof Error ? options.error.message : String(options.error);
4700
- publishConnectionState(this.endpoint, {
4701
- status,
4702
- connected: status === "connected" || status === "degraded" || status === "unresponsive",
4703
- projectRoot: this.projectRoot,
4704
- indexDir: this.indexDir,
4705
- endpoint: this.endpoint,
4706
- pid,
4707
- lastError,
4708
- ...this.activity ? { activity: this.activity } : {},
4709
- ...this.health ? { health: this.health } : {}
4710
- });
4711
- }
4712
- isConnected() {
4713
- return this.socket !== null && !this.socket.destroyed && this.info !== null;
4105
+ /** Safe LRU candidate: no request, connect, or health probe is in flight. */
4106
+ isEvictable() {
4107
+ return this.pending.size === 0 && this.connecting === null && this.healthCheck === null;
4714
4108
  }
4715
4109
  async checkHealth(spawnIfMissing = false, timeoutMs = SERVER_HEALTH_TIMEOUT_MS) {
4716
4110
  await this.ensureConnected(spawnIfMissing);
@@ -4810,371 +4204,1018 @@ var ProjectServerConnection = class {
4810
4204
  this.transition("connected", { pid: this.info?.pid });
4811
4205
  }
4812
4206
  }
4813
- close() {
4814
- const socket = this.socket;
4207
+ close() {
4208
+ const socket = this.socket;
4209
+ this.socket = null;
4210
+ this.info = null;
4211
+ this.activity = null;
4212
+ this.health = null;
4213
+ this.connectReject?.(new Error("codebase-index client disconnected"));
4214
+ this.connectResolve = null;
4215
+ this.connectReject = null;
4216
+ if (socket && !socket.destroyed) socket.destroy();
4217
+ this.rejectPending(new Error("codebase-index client disconnected"));
4218
+ this.transition("offline");
4219
+ maybeStopHeartbeatLoop();
4220
+ }
4221
+ request(message, options) {
4222
+ const socket = this.socket;
4223
+ if (!socket || socket.destroyed) {
4224
+ return Promise.reject(new Error("codebase-index server connection is not available"));
4225
+ }
4226
+ const id = this.nextId++;
4227
+ return new Promise((resolve4, reject) => {
4228
+ const timer = setTimeout(() => {
4229
+ const entry = this.pending.get(id);
4230
+ if (!entry) return;
4231
+ this.pending.delete(id);
4232
+ this.write({ type: "cancel", id });
4233
+ const error = new IndexTimeoutError(
4234
+ `Index ${message.type === "request" ? message.op : message.type} exceeded its ${options.timeoutMs}ms watchdog timeout`
4235
+ );
4236
+ this.cleanupPending(entry);
4237
+ entry.reject(error);
4238
+ }, options.timeoutMs);
4239
+ timer.unref?.();
4240
+ const signal = options.signal;
4241
+ const onAbort = signal ? () => {
4242
+ const entry = this.pending.get(id);
4243
+ if (!entry) return;
4244
+ this.pending.delete(id);
4245
+ this.write({ type: "cancel", id });
4246
+ this.cleanupPending(entry);
4247
+ entry.reject(cancellationError(signal));
4248
+ } : void 0;
4249
+ this.pending.set(id, {
4250
+ resolve: resolve4,
4251
+ reject,
4252
+ timer,
4253
+ signal,
4254
+ onAbort,
4255
+ onProgress: options.onProgress
4256
+ });
4257
+ if (signal && onAbort) {
4258
+ signal.addEventListener("abort", onAbort, { once: true });
4259
+ if (signal.aborted) {
4260
+ onAbort();
4261
+ return;
4262
+ }
4263
+ }
4264
+ this.write({ ...message, id });
4265
+ });
4266
+ }
4267
+ async ensureConnected(spawnIfMissing) {
4268
+ if (this.socket && !this.socket.destroyed && this.info) return;
4269
+ if (this.connecting) return this.connecting;
4270
+ this.transition("connecting");
4271
+ this.connecting = this.connectWithElection(spawnIfMissing).catch((error) => {
4272
+ this.transition("error", { error });
4273
+ throw error;
4274
+ }).finally(() => {
4275
+ this.connecting = null;
4276
+ });
4277
+ return this.connecting;
4278
+ }
4279
+ async connectWithElection(spawnIfMissing) {
4280
+ const deadline = Date.now() + (spawnIfMissing ? SERVER_START_TIMEOUT_MS : CONNECT_ATTEMPT_TIMEOUT_MS);
4281
+ let spawned = false;
4282
+ let staleAttempts = 0;
4283
+ let lastError = new Error("codebase-index server unavailable");
4284
+ while (Date.now() < deadline) {
4285
+ try {
4286
+ await this.connectOnce();
4287
+ return;
4288
+ } catch (error) {
4289
+ lastError = error;
4290
+ if (error instanceof StaleProjectIndexServerError) {
4291
+ staleAttempts++;
4292
+ if (!spawnIfMissing) break;
4293
+ if (staleAttempts >= 3) this.forceKillServer(error.pid);
4294
+ spawned = false;
4295
+ await delay(100);
4296
+ continue;
4297
+ }
4298
+ }
4299
+ if (!spawnIfMissing) break;
4300
+ if (!spawned) {
4301
+ this.spawnDetachedServer();
4302
+ spawned = true;
4303
+ }
4304
+ await delay(75);
4305
+ }
4306
+ throw lastError;
4307
+ }
4308
+ connectOnce() {
4309
+ this.socket?.destroy();
4310
+ this.socket = null;
4311
+ this.info = null;
4312
+ this.activity = null;
4313
+ this.health = null;
4314
+ this.buffer = "";
4315
+ return new Promise((resolve4, reject) => {
4316
+ const socket = net.createConnection(this.endpoint);
4317
+ this.socket = socket;
4318
+ socket.setEncoding("utf8");
4319
+ const timer = setTimeout(() => {
4320
+ reject(new Error("codebase-index server handshake timed out"));
4321
+ socket.destroy();
4322
+ }, CONNECT_ATTEMPT_TIMEOUT_MS);
4323
+ timer.unref?.();
4324
+ const finishResolve = () => {
4325
+ clearTimeout(timer);
4326
+ this.connectResolve = null;
4327
+ this.connectReject = null;
4328
+ resolve4();
4329
+ };
4330
+ const finishReject = (error) => {
4331
+ clearTimeout(timer);
4332
+ this.connectResolve = null;
4333
+ this.connectReject = null;
4334
+ reject(error);
4335
+ };
4336
+ this.connectResolve = finishResolve;
4337
+ this.connectReject = finishReject;
4338
+ socket.on("data", (chunk) => this.onData(socket, chunk));
4339
+ socket.on("error", (error) => {
4340
+ if (!this.info) finishReject(error);
4341
+ });
4342
+ socket.on("close", () => this.onClose(socket));
4343
+ });
4344
+ }
4345
+ onData(socket, chunk) {
4346
+ if (socket !== this.socket) return;
4347
+ this.buffer += chunk;
4348
+ while (true) {
4349
+ const newline = this.buffer.indexOf("\n");
4350
+ if (newline < 0) {
4351
+ if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
4352
+ socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
4353
+ }
4354
+ return;
4355
+ }
4356
+ if (newline > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
4357
+ socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
4358
+ return;
4359
+ }
4360
+ const line = this.buffer.slice(0, newline);
4361
+ this.buffer = this.buffer.slice(newline + 1);
4362
+ if (!line) continue;
4363
+ let message;
4364
+ try {
4365
+ message = JSON.parse(line);
4366
+ } catch {
4367
+ socket.destroy(new Error("invalid codebase-index server response"));
4368
+ return;
4369
+ }
4370
+ this.onMessage(message);
4371
+ }
4372
+ }
4373
+ onMessage(message) {
4374
+ if (message.type === "hello") {
4375
+ if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
4376
+ this.rejectStaleServer(
4377
+ message,
4378
+ `codebase-index protocol mismatch: client=${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}, server=${message.protocolVersion}`
4379
+ );
4380
+ return;
4381
+ }
4382
+ const expectedBuildId = projectIndexServerExpectedBuildId();
4383
+ if (expectedBuildId && message.buildId !== expectedBuildId) {
4384
+ this.rejectStaleServer(
4385
+ message,
4386
+ `codebase-index build mismatch: client=${expectedBuildId}, server=${message.buildId ?? "legacy"}`
4387
+ );
4388
+ return;
4389
+ }
4390
+ this.info = message;
4391
+ this.markResponsive();
4392
+ this.transition("connected", { pid: message.pid });
4393
+ ensureHeartbeatLoop();
4394
+ this.connectResolve?.();
4395
+ return;
4396
+ }
4397
+ if (message.type === "index-state") {
4398
+ this.activity = message.state;
4399
+ this.markResponsive();
4400
+ this.transition("connected", { pid: this.info?.pid });
4401
+ return;
4402
+ }
4403
+ const entry = this.pending.get(message.id);
4404
+ if (!entry) return;
4405
+ this.markResponsive();
4406
+ const status = connectionStates.get(this.endpoint)?.status;
4407
+ if (status === "degraded" || status === "unresponsive") {
4408
+ this.transition("connected", { pid: this.info?.pid });
4409
+ }
4410
+ if (message.type === "progress") {
4411
+ entry.onProgress?.(message.current, message.total);
4412
+ return;
4413
+ }
4414
+ this.pending.delete(message.id);
4415
+ this.cleanupPending(entry);
4416
+ if (message.ok) entry.resolve(message.result);
4417
+ else entry.reject(remoteError(message.error, message.errorName));
4418
+ }
4419
+ onClose(socket) {
4420
+ if (socket !== this.socket) return;
4421
+ const wasConnected = this.info !== null;
4815
4422
  this.socket = null;
4816
4423
  this.info = null;
4817
4424
  this.activity = null;
4818
4425
  this.health = null;
4819
- this.connectReject?.(new Error("codebase-index client disconnected"));
4426
+ const error = new Error("codebase-index server connection closed");
4427
+ this.connectReject?.(error);
4820
4428
  this.connectResolve = null;
4821
4429
  this.connectReject = null;
4822
- if (socket && !socket.destroyed) socket.destroy();
4823
- this.rejectPending(new Error("codebase-index client disconnected"));
4824
- this.transition("offline");
4430
+ this.rejectPending(error);
4431
+ if (wasConnected) this.transition("error", { error });
4825
4432
  maybeStopHeartbeatLoop();
4826
4433
  }
4827
- request(message, options) {
4828
- const socket = this.socket;
4829
- if (!socket || socket.destroyed) {
4830
- return Promise.reject(new Error("codebase-index server connection is not available"));
4434
+ cleanupPending(entry) {
4435
+ clearTimeout(entry.timer);
4436
+ if (entry.signal && entry.onAbort) {
4437
+ entry.signal.removeEventListener("abort", entry.onAbort);
4831
4438
  }
4832
- const id = this.nextId++;
4833
- return new Promise((resolve4, reject) => {
4834
- const timer = setTimeout(() => {
4835
- const entry = this.pending.get(id);
4836
- if (!entry) return;
4837
- this.pending.delete(id);
4838
- this.write({ type: "cancel", id });
4839
- const error = new IndexTimeoutError(
4840
- `Index ${message.type === "request" ? message.op : message.type} exceeded its ${options.timeoutMs}ms watchdog timeout`
4841
- );
4842
- this.cleanupPending(entry);
4843
- entry.reject(error);
4844
- }, options.timeoutMs);
4439
+ }
4440
+ rejectPending(error) {
4441
+ const entries = [...this.pending.values()];
4442
+ this.pending.clear();
4443
+ for (const entry of entries) {
4444
+ this.cleanupPending(entry);
4445
+ entry.reject(error);
4446
+ }
4447
+ }
4448
+ write(message) {
4449
+ const socket = this.socket;
4450
+ if (socket && !socket.destroyed) socket.write(encodeProjectServerMessage(message));
4451
+ }
4452
+ rejectStaleServer(message, reason) {
4453
+ const socket = this.socket;
4454
+ if (socket && !socket.destroyed) {
4455
+ socket.write(
4456
+ encodeProjectServerMessage({
4457
+ type: "shutdown",
4458
+ id: 0,
4459
+ reason: "stale-build-replacement"
4460
+ })
4461
+ );
4462
+ const timer = setTimeout(() => socket.destroy(), 25);
4845
4463
  timer.unref?.();
4846
- const signal = options.signal;
4847
- const onAbort = signal ? () => {
4848
- const entry = this.pending.get(id);
4849
- if (!entry) return;
4850
- this.pending.delete(id);
4851
- this.write({ type: "cancel", id });
4852
- this.cleanupPending(entry);
4853
- entry.reject(cancellationError(signal));
4854
- } : void 0;
4855
- this.pending.set(id, {
4856
- resolve: resolve4,
4857
- reject,
4858
- timer,
4859
- signal,
4860
- onAbort,
4861
- onProgress: options.onProgress
4862
- });
4863
- if (signal && onAbort) {
4864
- signal.addEventListener("abort", onAbort, { once: true });
4865
- if (signal.aborted) {
4866
- onAbort();
4867
- return;
4868
- }
4464
+ }
4465
+ this.connectReject?.(new StaleProjectIndexServerError(reason, message.pid));
4466
+ }
4467
+ spawnDetachedServer() {
4468
+ const url = resolveProjectServerUrl();
4469
+ if (!url) throw new Error("built codebase-index project server is unavailable");
4470
+ if (process.platform !== "win32") {
4471
+ try {
4472
+ fs4.rmSync(this.endpoint, { force: true });
4473
+ } catch {
4869
4474
  }
4870
- this.write({ ...message, id });
4475
+ }
4476
+ const args = [fileURLToPath2(url), "--project-root", this.projectRoot];
4477
+ if (this.indexDir) args.push("--index-dir", this.indexDir);
4478
+ const child = spawn(process.execPath, args, {
4479
+ detached: true,
4480
+ stdio: "ignore",
4481
+ windowsHide: true,
4482
+ env: process.env
4871
4483
  });
4484
+ child.unref();
4872
4485
  }
4873
- async ensureConnected(spawnIfMissing) {
4874
- if (this.socket && !this.socket.destroyed && this.info) return;
4875
- if (this.connecting) return this.connecting;
4876
- this.transition("connecting");
4877
- this.connecting = this.connectWithElection(spawnIfMissing).catch((error) => {
4878
- this.transition("error", { error });
4879
- throw error;
4880
- }).finally(() => {
4881
- this.connecting = null;
4882
- });
4883
- return this.connecting;
4486
+ forceKillKnownServer() {
4487
+ const pid = this.info?.pid;
4488
+ return pid ? this.forceKillServer(pid) : false;
4884
4489
  }
4885
- async connectWithElection(spawnIfMissing) {
4886
- const deadline = Date.now() + (spawnIfMissing ? SERVER_START_TIMEOUT_MS : CONNECT_ATTEMPT_TIMEOUT_MS);
4887
- let spawned = false;
4888
- let staleAttempts = 0;
4889
- let lastError = new Error("codebase-index server unavailable");
4890
- while (Date.now() < deadline) {
4490
+ forceKillServer(pid) {
4491
+ if (pid === process.pid) return false;
4492
+ try {
4493
+ process.kill(pid);
4494
+ const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
4891
4495
  try {
4892
- await this.connectOnce();
4893
- return;
4894
- } catch (error) {
4895
- lastError = error;
4896
- if (error instanceof StaleProjectIndexServerError) {
4897
- staleAttempts++;
4898
- if (!spawnIfMissing) break;
4899
- if (staleAttempts >= 3) this.forceKillServer(error.pid);
4900
- spawned = false;
4901
- await delay(100);
4902
- continue;
4903
- }
4904
- }
4905
- if (!spawnIfMissing) break;
4906
- if (!spawned) {
4907
- this.spawnDetachedServer();
4908
- spawned = true;
4496
+ const metadata = JSON.parse(fs4.readFileSync(metadataPath, "utf8"));
4497
+ if (metadata.pid === pid) fs4.rmSync(metadataPath, { force: true });
4498
+ } catch {
4909
4499
  }
4910
- await delay(75);
4500
+ return true;
4501
+ } catch {
4502
+ return false;
4503
+ }
4504
+ }
4505
+ };
4506
+ var connections = /* @__PURE__ */ new Map();
4507
+ var MAX_CACHED_CONNECTIONS = 8;
4508
+ var heartbeatTimer;
4509
+ function forgetConnection(endpoint, connection) {
4510
+ if (connections.get(endpoint) === connection) connections.delete(endpoint);
4511
+ connection.close();
4512
+ connectionStates.delete(endpoint);
4513
+ if (latestConnectionState.endpoint !== endpoint) return;
4514
+ latestConnectionState = [...connectionStates.values()].at(-1) ?? {
4515
+ status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
4516
+ connected: false
4517
+ };
4518
+ }
4519
+ function trimConnectionCache(protectedConnection) {
4520
+ if (connections.size <= MAX_CACHED_CONNECTIONS) return;
4521
+ for (const [endpoint, connection] of connections) {
4522
+ if (connections.size <= MAX_CACHED_CONNECTIONS) break;
4523
+ if (connection === protectedConnection || !connection.isEvictable()) continue;
4524
+ forgetConnection(endpoint, connection);
4525
+ }
4526
+ }
4527
+ function ensureHeartbeatLoop() {
4528
+ if (heartbeatTimer) return;
4529
+ heartbeatTimer = setInterval(() => {
4530
+ for (const connection of connections.values()) {
4531
+ if (connection.isConnected()) void connection.checkHealth(false).catch(() => {
4532
+ });
4533
+ }
4534
+ }, SERVER_HEARTBEAT_INTERVAL_MS);
4535
+ heartbeatTimer.unref?.();
4536
+ }
4537
+ function maybeStopHeartbeatLoop() {
4538
+ if (!heartbeatTimer) return;
4539
+ if ([...connections.values()].some((connection) => connection.isConnected())) return;
4540
+ clearInterval(heartbeatTimer);
4541
+ heartbeatTimer = void 0;
4542
+ }
4543
+ function connectionFor(projectRoot, indexDir) {
4544
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
4545
+ let connection = connections.get(endpoint);
4546
+ if (!connection) {
4547
+ connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
4548
+ connections.set(endpoint, connection);
4549
+ } else {
4550
+ connections.delete(endpoint);
4551
+ connections.set(endpoint, connection);
4552
+ }
4553
+ trimConnectionCache(connection);
4554
+ return connection;
4555
+ }
4556
+ function callProjectIndexServer(op, args, options) {
4557
+ return connectionFor(args.projectRoot, args.indexDir).call(op, args, options);
4558
+ }
4559
+ function ensureProjectIndexServer(options) {
4560
+ return connectionFor(options.projectRoot, options.indexDir).configure(
4561
+ options.watchExternal,
4562
+ options.debounceMs
4563
+ );
4564
+ }
4565
+ function checkProjectIndexServerHealth(projectRoot, indexDir, options = {}) {
4566
+ return connectionFor(projectRoot, indexDir).checkHealth(
4567
+ false,
4568
+ options.timeoutMs ?? SERVER_HEALTH_TIMEOUT_MS
4569
+ );
4570
+ }
4571
+ async function shutdownProjectIndexServer(projectRoot, indexDir, reason) {
4572
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
4573
+ const connection = connectionFor(projectRoot, indexDir);
4574
+ try {
4575
+ return await connection.shutdownRemote(reason);
4576
+ } finally {
4577
+ connection.close();
4578
+ connections.delete(endpoint);
4579
+ connectionStates.delete(endpoint);
4580
+ }
4581
+ }
4582
+ function closeProjectIndexServerClients() {
4583
+ for (const connection of connections.values()) connection.close();
4584
+ connections.clear();
4585
+ connectionStates.clear();
4586
+ latestConnectionState = {
4587
+ status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
4588
+ connected: false
4589
+ };
4590
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
4591
+ heartbeatTimer = void 0;
4592
+ }
4593
+
4594
+ // src/codebase-index/background-indexer.ts
4595
+ import * as fs11 from "node:fs";
4596
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
4597
+ import { Worker } from "node:worker_threads";
4598
+
4599
+ // src/codebase-index/indexer.ts
4600
+ import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
4601
+ import { execFile as execFile2 } from "node:child_process";
4602
+ import * as fs10 from "node:fs/promises";
4603
+ import { availableParallelism } from "node:os";
4604
+ import * as path12 from "node:path";
4605
+ import {
4606
+ DEFAULT_WALK_IGNORE_DIRS,
4607
+ indexParallelBatchSize,
4608
+ isFrugalPerf
4609
+ } from "@wrongstack/core/utils";
4610
+
4611
+ // src/codebase-index/gitignore.ts
4612
+ import * as fs5 from "node:fs/promises";
4613
+ import * as path5 from "node:path";
4614
+ import { compileGlob } from "@wrongstack/core/utils";
4615
+ function globBody(glob) {
4616
+ return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
4617
+ }
4618
+ function compileGitignore(lines) {
4619
+ const rules = [];
4620
+ for (const raw of lines) {
4621
+ let line = raw.replace(/\r$/, "");
4622
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
4623
+ line = line.trim();
4624
+ let negated = false;
4625
+ if (line.startsWith("!")) {
4626
+ negated = true;
4627
+ line = line.slice(1);
4911
4628
  }
4912
- throw lastError;
4913
- }
4914
- connectOnce() {
4915
- this.socket?.destroy();
4916
- this.socket = null;
4917
- this.info = null;
4918
- this.activity = null;
4919
- this.health = null;
4920
- this.buffer = "";
4921
- return new Promise((resolve4, reject) => {
4922
- const socket = net.createConnection(this.endpoint);
4923
- this.socket = socket;
4924
- socket.setEncoding("utf8");
4925
- const timer = setTimeout(() => {
4926
- reject(new Error("codebase-index server handshake timed out"));
4927
- socket.destroy();
4928
- }, CONNECT_ATTEMPT_TIMEOUT_MS);
4929
- timer.unref?.();
4930
- const finishResolve = () => {
4931
- clearTimeout(timer);
4932
- this.connectResolve = null;
4933
- this.connectReject = null;
4934
- resolve4();
4935
- };
4936
- const finishReject = (error) => {
4937
- clearTimeout(timer);
4938
- this.connectResolve = null;
4939
- this.connectReject = null;
4940
- reject(error);
4941
- };
4942
- this.connectResolve = finishResolve;
4943
- this.connectReject = finishReject;
4944
- socket.on("data", (chunk) => this.onData(socket, chunk));
4945
- socket.on("error", (error) => {
4946
- if (!this.info) finishReject(error);
4947
- });
4948
- socket.on("close", () => this.onClose(socket));
4629
+ let dirOnly = false;
4630
+ if (line.endsWith("/")) {
4631
+ dirOnly = true;
4632
+ line = line.slice(0, -1);
4633
+ }
4634
+ if (!line) continue;
4635
+ const anchored = line.startsWith("/") || line.includes("/");
4636
+ if (line.startsWith("/")) line = line.slice(1);
4637
+ const body = globBody(line);
4638
+ const prefix = anchored ? "^" : "(?:^|.*/)";
4639
+ rules.push({
4640
+ eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
4641
+ under: new RegExp(`${prefix}${body}/.*$`),
4642
+ negated,
4643
+ dirOnly
4949
4644
  });
4950
4645
  }
4951
- onData(socket, chunk) {
4952
- if (socket !== this.socket) return;
4953
- this.buffer += chunk;
4954
- while (true) {
4955
- const newline = this.buffer.indexOf("\n");
4956
- if (newline < 0) {
4957
- if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
4958
- socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
4959
- }
4960
- return;
4646
+ return (relPath, isDir) => {
4647
+ const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
4648
+ let ignored = false;
4649
+ for (const r of rules) {
4650
+ const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
4651
+ if (re.test(p)) ignored = !r.negated;
4652
+ }
4653
+ return ignored;
4654
+ };
4655
+ }
4656
+ async function loadGitignoreMatcher(projectRoot) {
4657
+ let lines = [];
4658
+ try {
4659
+ const raw = await fs5.readFile(path5.join(projectRoot, ".gitignore"), "utf8");
4660
+ lines = raw.split("\n");
4661
+ } catch {
4662
+ }
4663
+ return compileGitignore(lines);
4664
+ }
4665
+
4666
+ // src/codebase-index/indexer.ts
4667
+ init_languages();
4668
+
4669
+ // src/codebase-index/parser-dispatch.ts
4670
+ async function parseFileContent(file, content, lang) {
4671
+ switch (lang) {
4672
+ case "ts":
4673
+ case "tsx":
4674
+ case "js":
4675
+ case "jsx": {
4676
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
4677
+ return parseSymbols8({ file, content, lang });
4678
+ }
4679
+ case "go": {
4680
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
4681
+ return parseSymbols8({ file, content, lang: "go" });
4682
+ }
4683
+ case "py": {
4684
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
4685
+ return parseSymbols8({ file, content, lang: "py" });
4686
+ }
4687
+ case "rs": {
4688
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
4689
+ return parseSymbols8({ file, content, lang: "rs" });
4690
+ }
4691
+ case "json": {
4692
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
4693
+ return parseSymbols8({ file, content, lang: "json" });
4694
+ }
4695
+ case "yaml": {
4696
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
4697
+ return parseSymbols8({ file, content, lang: "yaml" });
4698
+ }
4699
+ default: {
4700
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
4701
+ return parseSymbols8({ file, content, lang });
4702
+ }
4703
+ }
4704
+ }
4705
+
4706
+ // src/codebase-index/indexer.ts
4707
+ var YIELD_EVERY_N = 50;
4708
+ function resolveParallelBatch() {
4709
+ return indexParallelBatchSize(availableParallelism());
4710
+ }
4711
+ function yieldEventLoop() {
4712
+ return new Promise((resolve4) => setImmediate(resolve4));
4713
+ }
4714
+ function throwIfAborted(signal) {
4715
+ if (!signal?.aborted) return;
4716
+ if (signal.reason instanceof Error) throw signal.reason;
4717
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
4718
+ }
4719
+ function isAbortError(err) {
4720
+ return err instanceof DOMException && err.name === "AbortError";
4721
+ }
4722
+ var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
4723
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
4724
+ var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
4725
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
4726
+ var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
4727
+ function isWithinProject(projectRoot, file) {
4728
+ const rel = path12.relative(projectRoot, file);
4729
+ return rel !== "" && !rel.startsWith(`..${path12.sep}`) && rel !== ".." && !path12.isAbsolute(rel);
4730
+ }
4731
+ function isMissingPathError(err) {
4732
+ const code = err?.code;
4733
+ return code === "ENOENT" || code === "ENOTDIR";
4734
+ }
4735
+ function normalizeComparablePath(value) {
4736
+ const resolved = path12.resolve(value);
4737
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
4738
+ }
4739
+ function gitOutput(projectRoot, args) {
4740
+ return new Promise((resolve4, reject) => {
4741
+ execFile2(
4742
+ "git",
4743
+ ["-C", projectRoot, ...args],
4744
+ {
4745
+ encoding: "buffer",
4746
+ maxBuffer: MAX_GIT_FILE_LIST_BYTES,
4747
+ windowsHide: true
4748
+ },
4749
+ (error, stdout) => {
4750
+ if (error) reject(error);
4751
+ else resolve4(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
4961
4752
  }
4962
- if (newline > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
4963
- socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
4964
- return;
4753
+ );
4754
+ });
4755
+ }
4756
+ async function findGitSourceFiles(projectRoot, ignore, signal) {
4757
+ try {
4758
+ throwIfAborted(signal);
4759
+ const topLevel = (await gitOutput(projectRoot, ["rev-parse", "--show-toplevel"])).toString("utf8").trim();
4760
+ if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
4761
+ throwIfAborted(signal);
4762
+ const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
4763
+ const [output, statusOutput] = await Promise.all([
4764
+ gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
4765
+ gitOutput(projectRoot, [
4766
+ "status",
4767
+ "--porcelain=v1",
4768
+ "-z",
4769
+ "--untracked-files=all",
4770
+ "--ignored=no"
4771
+ ])
4772
+ ]);
4773
+ throwIfAborted(signal);
4774
+ const dirty = /* @__PURE__ */ new Set();
4775
+ const deleted = /* @__PURE__ */ new Set();
4776
+ const statusRecords = statusOutput.toString("utf8").split("\0");
4777
+ for (let i = 0; i < statusRecords.length; i++) {
4778
+ const record = statusRecords[i];
4779
+ if (!record) continue;
4780
+ const status = record.slice(0, 2);
4781
+ const changedPath = path12.resolve(projectRoot, record.slice(3));
4782
+ dirty.add(changedPath);
4783
+ if (status.includes("D")) deleted.add(changedPath);
4784
+ if (status.includes("R") || status.includes("C")) {
4785
+ const source = statusRecords[++i];
4786
+ if (source) dirty.add(path12.resolve(projectRoot, source));
4965
4787
  }
4966
- const line = this.buffer.slice(0, newline);
4967
- this.buffer = this.buffer.slice(newline + 1);
4968
- if (!line) continue;
4969
- let message;
4970
- try {
4971
- message = JSON.parse(line);
4972
- } catch {
4973
- socket.destroy(new Error("invalid codebase-index server response"));
4974
- return;
4788
+ }
4789
+ const files = [];
4790
+ for (const relative2 of output.toString("utf8").split("\0")) {
4791
+ if (!relative2) continue;
4792
+ const portable = relative2.replace(/\\/g, "/");
4793
+ if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path12.posix.basename(portable))) {
4794
+ continue;
4975
4795
  }
4976
- this.onMessage(message);
4796
+ const full = path12.resolve(projectRoot, relative2);
4797
+ if (deleted.has(full)) continue;
4798
+ const ext = path12.extname(relative2).toLowerCase();
4799
+ if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
4977
4800
  }
4801
+ return {
4802
+ files,
4803
+ trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
4804
+ };
4805
+ } catch {
4806
+ return null;
4978
4807
  }
4979
- onMessage(message) {
4980
- if (message.type === "hello") {
4981
- if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
4982
- this.rejectStaleServer(
4983
- message,
4984
- `codebase-index protocol mismatch: client=${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}, server=${message.protocolVersion}`
4985
- );
4986
- return;
4987
- }
4988
- const expectedBuildId = projectIndexServerExpectedBuildId();
4989
- if (expectedBuildId && message.buildId !== expectedBuildId) {
4990
- this.rejectStaleServer(
4991
- message,
4992
- `codebase-index build mismatch: client=${expectedBuildId}, server=${message.buildId ?? "legacy"}`
4993
- );
4994
- return;
4995
- }
4996
- this.info = message;
4997
- this.markResponsive();
4998
- this.transition("connected", { pid: message.pid });
4999
- ensureHeartbeatLoop();
5000
- this.connectResolve?.();
5001
- return;
4808
+ }
4809
+ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
4810
+ const gitFiles = await findGitSourceFiles(projectRoot, ignore, signal);
4811
+ if (gitFiles) {
4812
+ return {
4813
+ files: gitFiles.files,
4814
+ complete: true,
4815
+ errors: [],
4816
+ trustedUnchanged: gitFiles.trustedUnchanged
4817
+ };
4818
+ }
4819
+ const results = [];
4820
+ const errors = [];
4821
+ let complete = true;
4822
+ const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
4823
+ const indexableExts = new Set(INDEXABLE_EXTENSIONS);
4824
+ let dirCount = 0;
4825
+ const walk = async (dir) => {
4826
+ throwIfAborted(signal);
4827
+ if (dirCount > 0 && dirCount % YIELD_EVERY_N === 0) {
4828
+ await yieldEventLoop();
4829
+ throwIfAborted(signal);
5002
4830
  }
5003
- if (message.type === "index-state") {
5004
- this.activity = message.state;
5005
- this.markResponsive();
5006
- this.transition("connected", { pid: this.info?.pid });
4831
+ let entries;
4832
+ try {
4833
+ entries = await fs10.readdir(dir, { withFileTypes: true });
4834
+ } catch (err) {
4835
+ complete = false;
4836
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
5007
4837
  return;
5008
4838
  }
5009
- const entry = this.pending.get(message.id);
5010
- if (!entry) return;
5011
- this.markResponsive();
5012
- const status = connectionStates.get(this.endpoint)?.status;
5013
- if (status === "degraded" || status === "unresponsive") {
5014
- this.transition("connected", { pid: this.info?.pid });
4839
+ dirCount++;
4840
+ for (const e of entries) {
4841
+ if (ignoreSet.has(e.name)) continue;
4842
+ const full = path12.join(dir, e.name);
4843
+ const rel = path12.relative(projectRoot, full).replace(/\\/g, "/");
4844
+ if (e.isDirectory()) {
4845
+ if (isGitIgnored(rel, true)) continue;
4846
+ await walk(full);
4847
+ } else if (e.isFile()) {
4848
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
4849
+ const ext = path12.extname(e.name).toLowerCase();
4850
+ if (indexableExts.has(ext) || detectLang(full) !== null) {
4851
+ results.push(full);
4852
+ }
4853
+ }
5015
4854
  }
5016
- if (message.type === "progress") {
5017
- entry.onProgress?.(message.current, message.total);
5018
- return;
4855
+ };
4856
+ await walk(projectRoot);
4857
+ return { files: results, complete, errors };
4858
+ }
4859
+ function assignRefsToSymbols2(refs, symbols) {
4860
+ if (refs.length === 0 || symbols.length === 0) return [];
4861
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
4862
+ const seen = /* @__PURE__ */ new Set();
4863
+ const assigned = [];
4864
+ for (const ref of refs) {
4865
+ let owner;
4866
+ for (const symbol of ordered) {
4867
+ if (symbol.line > ref.line) break;
4868
+ owner = symbol;
5019
4869
  }
5020
- this.pending.delete(message.id);
5021
- this.cleanupPending(entry);
5022
- if (message.ok) entry.resolve(message.result);
5023
- else entry.reject(remoteError(message.error, message.errorName));
5024
- }
5025
- onClose(socket) {
5026
- if (socket !== this.socket) return;
5027
- const wasConnected = this.info !== null;
5028
- this.socket = null;
5029
- this.info = null;
5030
- this.activity = null;
5031
- this.health = null;
5032
- const error = new Error("codebase-index server connection closed");
5033
- this.connectReject?.(error);
5034
- this.connectResolve = null;
5035
- this.connectReject = null;
5036
- this.rejectPending(error);
5037
- if (wasConnected) this.transition("error", { error });
5038
- maybeStopHeartbeatLoop();
4870
+ if (!owner && ref.callType === "import") owner = ordered[0];
4871
+ if (!owner || owner.id <= 0) continue;
4872
+ const key = `${owner.id}:${ref.toName}:${ref.callType}`;
4873
+ if (seen.has(key)) continue;
4874
+ seen.add(key);
4875
+ assigned.push({ ...ref, fromId: owner.id });
5039
4876
  }
5040
- cleanupPending(entry) {
5041
- clearTimeout(entry.timer);
5042
- if (entry.signal && entry.onAbort) {
5043
- entry.signal.removeEventListener("abort", entry.onAbort);
4877
+ return assigned;
4878
+ }
4879
+ async function runIndexer(_ctx, opts) {
4880
+ const store = new IndexStore(opts.projectRoot, { indexDir: opts.indexDir });
4881
+ try {
4882
+ return await runIndexerWithStore(store, opts);
4883
+ } finally {
4884
+ try {
4885
+ store.close();
4886
+ } catch {
5044
4887
  }
5045
4888
  }
5046
- rejectPending(error) {
5047
- const entries = [...this.pending.values()];
5048
- this.pending.clear();
5049
- for (const entry of entries) {
5050
- this.cleanupPending(entry);
5051
- entry.reject(error);
5052
- }
4889
+ }
4890
+ async function runIndexerWithStore(store, opts) {
4891
+ const { projectRoot, langs, ignore = [], signal } = opts;
4892
+ const relationGraphVersion = "2";
4893
+ const refResolutionVersion = "2";
4894
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
4895
+ const needsFullRefResolution = force || store.getMetadata("ref_resolution_version") !== refResolutionVersion;
4896
+ const startMs = Date.now();
4897
+ const errors = [];
4898
+ const langStats = {};
4899
+ let filesIndexed = 0;
4900
+ let symbolsIndexed = 0;
4901
+ const isGitIgnored = await loadGitignoreMatcher(projectRoot);
4902
+ let files;
4903
+ let discoveredFiles = null;
4904
+ let discoveryComplete = true;
4905
+ let trustedUnchanged;
4906
+ if (opts.files && opts.files.length > 0) {
4907
+ files = opts.files.map((f) => path12.resolve(projectRoot, f)).filter((f) => {
4908
+ if (!isWithinProject(projectRoot, f)) return false;
4909
+ const rel = path12.relative(projectRoot, f).replace(/\\/g, "/");
4910
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path12.basename(f)) && !isGitIgnored(rel, false);
4911
+ });
4912
+ } else {
4913
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
4914
+ files = discovery.files;
4915
+ errors.push(...discovery.errors);
4916
+ discoveryComplete = discovery.complete;
4917
+ discoveredFiles = new Set(files);
4918
+ trustedUnchanged = discovery.trustedUnchanged;
5053
4919
  }
5054
- write(message) {
5055
- const socket = this.socket;
5056
- if (socket && !socket.destroyed) socket.write(encodeProjectServerMessage(message));
4920
+ if (langs && langs.length > 0) {
4921
+ const langSet = new Set(langs);
4922
+ files = files.filter((f) => {
4923
+ const lang = detectLang(f);
4924
+ return lang ? langSet.has(lang) : false;
4925
+ });
5057
4926
  }
5058
- rejectStaleServer(message, reason) {
5059
- const socket = this.socket;
5060
- if (socket && !socket.destroyed) {
5061
- socket.write(
5062
- encodeProjectServerMessage({
5063
- type: "shutdown",
5064
- id: 0,
5065
- reason: "stale-build-replacement"
5066
- })
5067
- );
5068
- const timer = setTimeout(() => socket.destroy(), 25);
5069
- timer.unref?.();
4927
+ if (force) store.clearAll();
4928
+ const existingMeta = /* @__PURE__ */ new Map();
4929
+ if (!force) {
4930
+ for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
4931
+ }
4932
+ const totalFilesForProgress = files.length;
4933
+ let filesPreSkipped = 0;
4934
+ if (!force && trustedUnchanged) {
4935
+ files = files.filter((file) => {
4936
+ const meta = existingMeta.get(file);
4937
+ if (!meta || !trustedUnchanged.has(file)) return true;
4938
+ langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
4939
+ symbolsIndexed += meta.symbolCount;
4940
+ filesIndexed++;
4941
+ filesPreSkipped++;
4942
+ return false;
4943
+ });
4944
+ if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
4945
+ }
4946
+ const parallelBatch = resolveParallelBatch();
4947
+ let filesSinceLastYield = 0;
4948
+ for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
4949
+ const batchEnd = Math.min(batchStart + parallelBatch, files.length);
4950
+ const batchFiles = files.slice(batchStart, batchEnd);
4951
+ opts.onProgress?.(filesPreSkipped + batchEnd, totalFilesForProgress);
4952
+ filesSinceLastYield += batchFiles.length;
4953
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
4954
+ filesSinceLastYield = 0;
4955
+ await yieldEventLoop();
4956
+ if (isFrugalPerf()) {
4957
+ await new Promise((r) => setTimeout(r, 8));
4958
+ }
4959
+ throwIfAborted(signal);
4960
+ }
4961
+ const statOpts = signal ? { signal } : {};
4962
+ const statReadParse = await Promise.allSettled(
4963
+ batchFiles.map(
4964
+ async (file) => {
4965
+ let stat2;
4966
+ try {
4967
+ stat2 = await fs10.stat(file, statOpts);
4968
+ } catch (e) {
4969
+ if (isAbortError(e)) throw e;
4970
+ return {
4971
+ file,
4972
+ stat: null,
4973
+ lang: "",
4974
+ parsed: null,
4975
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
4976
+ missing: isMissingPathError(e)
4977
+ };
4978
+ }
4979
+ if (!stat2.isFile()) return { file, stat: stat2, lang: "", parsed: null };
4980
+ const lang = detectLang(file);
4981
+ if (!lang) return { file, stat: stat2, lang: "", parsed: null };
4982
+ if (stat2.size > MAX_INDEX_FILE_BYTES) {
4983
+ return {
4984
+ file,
4985
+ stat: stat2,
4986
+ lang,
4987
+ parsed: null,
4988
+ error: `file too large (${stat2.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
4989
+ };
4990
+ }
4991
+ const meta = existingMeta.get(file);
4992
+ if (!force && meta && meta.mtimeMs === Math.floor(stat2.mtimeMs)) {
4993
+ return { file, stat: stat2, lang, parsed: null, skippedMeta: meta };
4994
+ }
4995
+ let content;
4996
+ try {
4997
+ content = await fs10.readFile(file, { encoding: "utf8", signal });
4998
+ } catch (e) {
4999
+ if (isAbortError(e)) throw e;
5000
+ return {
5001
+ file,
5002
+ stat: stat2,
5003
+ lang,
5004
+ parsed: null,
5005
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
5006
+ };
5007
+ }
5008
+ let parsed;
5009
+ try {
5010
+ parsed = await parseFileContent(file, content, lang);
5011
+ } catch (e) {
5012
+ return {
5013
+ file,
5014
+ stat: stat2,
5015
+ lang,
5016
+ parsed: null,
5017
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
5018
+ };
5019
+ }
5020
+ return { file, stat: stat2, lang, parsed, content };
5021
+ }
5022
+ )
5023
+ );
5024
+ const batchEntries = [];
5025
+ const deleteForFiles = [];
5026
+ for (let fi = 0; fi < statReadParse.length; fi++) {
5027
+ const settled = statReadParse[fi];
5028
+ const file = expectDefined5(batchFiles[fi]);
5029
+ if (settled.status === "rejected") {
5030
+ const err = settled.reason;
5031
+ if (err instanceof Error && isAbortError(err)) throw err;
5032
+ errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
5033
+ continue;
5034
+ }
5035
+ const result = settled.value;
5036
+ if (result.error) {
5037
+ if (result.missing) store.deleteFile(file);
5038
+ errors.push(`${file}: ${result.error}`);
5039
+ continue;
5040
+ }
5041
+ const { stat: stat2, lang, parsed } = result;
5042
+ if (result.skippedMeta) {
5043
+ langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
5044
+ symbolsIndexed += result.skippedMeta.symbolCount;
5045
+ filesIndexed++;
5046
+ continue;
5047
+ }
5048
+ if (!lang || !parsed) {
5049
+ if (lang) {
5050
+ store.upsertFile({
5051
+ file,
5052
+ lang,
5053
+ mtimeMs: Math.floor(stat2.mtimeMs),
5054
+ symbolCount: 0,
5055
+ lastIndexed: Date.now()
5056
+ });
5057
+ filesIndexed++;
5058
+ }
5059
+ continue;
5060
+ }
5061
+ if (parsed.symbols.length === 0) {
5062
+ store.replaceEmptyFile({
5063
+ file,
5064
+ lang,
5065
+ mtimeMs: Math.floor(stat2.mtimeMs),
5066
+ symbolCount: 0,
5067
+ lastIndexed: Date.now()
5068
+ });
5069
+ filesIndexed++;
5070
+ continue;
5071
+ }
5072
+ batchEntries.push({
5073
+ file,
5074
+ lang,
5075
+ symbols: parsed.symbols,
5076
+ refs: parsed.refs ?? [],
5077
+ mtimeMs: Math.floor(stat2.mtimeMs),
5078
+ symbolCount: parsed.symbols.length
5079
+ });
5080
+ deleteForFiles.push(file);
5070
5081
  }
5071
- this.connectReject?.(new StaleProjectIndexServerError(reason, message.pid));
5072
- }
5073
- spawnDetachedServer() {
5074
- const url = resolveProjectServerUrl();
5075
- if (!url) throw new Error("built codebase-index project server is unavailable");
5076
- if (process.platform !== "win32") {
5082
+ if (batchEntries.length > 0) {
5077
5083
  try {
5078
- fs10.rmSync(this.endpoint, { force: true });
5079
- } catch {
5084
+ store.commitBatch(batchEntries, { deleteForFiles });
5085
+ for (const entry of batchEntries) {
5086
+ const count = entry.symbols.length;
5087
+ symbolsIndexed += count;
5088
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
5089
+ filesIndexed++;
5090
+ }
5091
+ } catch (err) {
5092
+ const message = err instanceof Error ? err.message : String(err);
5093
+ errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
5094
+ for (const entry of batchEntries) {
5095
+ try {
5096
+ store.deleteRefsForFile(entry.file);
5097
+ store.deleteSymbolsForFile(entry.file);
5098
+ const symbolsWithIds = store.insertSymbols(entry.symbols);
5099
+ symbolsIndexed += symbolsWithIds.length;
5100
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
5101
+ filesIndexed++;
5102
+ if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
5103
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
5104
+ if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
5105
+ }
5106
+ store.resolveRefsForNames([
5107
+ ...entry.symbols.map((symbol) => symbol.name),
5108
+ ...entry.refs.map((ref) => ref.toName)
5109
+ ]);
5110
+ store.upsertFile({
5111
+ file: entry.file,
5112
+ lang: entry.lang,
5113
+ mtimeMs: entry.mtimeMs,
5114
+ symbolCount: entry.symbolCount,
5115
+ lastIndexed: Date.now()
5116
+ });
5117
+ } catch (innerErr) {
5118
+ errors.push(
5119
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
5120
+ );
5121
+ }
5122
+ }
5080
5123
  }
5081
5124
  }
5082
- const args = [fileURLToPath2(url), "--project-root", this.projectRoot];
5083
- if (this.indexDir) args.push("--index-dir", this.indexDir);
5084
- const child = spawn4(process.execPath, args, {
5085
- detached: true,
5086
- stdio: "ignore",
5087
- windowsHide: true,
5088
- env: process.env
5089
- });
5090
- child.unref();
5091
- }
5092
- forceKillKnownServer() {
5093
- const pid = this.info?.pid;
5094
- return pid ? this.forceKillServer(pid) : false;
5095
5125
  }
5096
- forceKillServer(pid) {
5097
- if (pid === process.pid) return false;
5098
- try {
5099
- process.kill(pid);
5100
- const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
5101
- try {
5102
- const metadata = JSON.parse(fs10.readFileSync(metadataPath, "utf8"));
5103
- if (metadata.pid === pid) fs10.rmSync(metadataPath, { force: true });
5104
- } catch {
5126
+ if (discoveredFiles && discoveryComplete) {
5127
+ for (const [file_] of existingMeta) {
5128
+ if (!discoveredFiles.has(file_)) {
5129
+ store.deleteFile(file_);
5105
5130
  }
5106
- return true;
5107
- } catch {
5108
- return false;
5109
5131
  }
5110
5132
  }
5111
- };
5112
- var connections = /* @__PURE__ */ new Map();
5113
- var heartbeatTimer;
5114
- function ensureHeartbeatLoop() {
5115
- if (heartbeatTimer) return;
5116
- heartbeatTimer = setInterval(() => {
5117
- for (const connection of connections.values()) {
5118
- if (connection.isConnected()) void connection.checkHealth(false).catch(() => {
5119
- });
5120
- }
5121
- }, SERVER_HEARTBEAT_INTERVAL_MS);
5122
- heartbeatTimer.unref?.();
5123
- }
5124
- function maybeStopHeartbeatLoop() {
5125
- if (!heartbeatTimer) return;
5126
- if ([...connections.values()].some((connection) => connection.isConnected())) return;
5127
- clearInterval(heartbeatTimer);
5128
- heartbeatTimer = void 0;
5133
+ if (needsFullRefResolution) store.resolveRefs();
5134
+ store.setMetadata("ref_resolution_version", refResolutionVersion);
5135
+ store.setMetadata("relation_graph_version", relationGraphVersion);
5136
+ if (!opts.files || filesIndexed >= 50) store.optimize();
5137
+ store.setLastIndexed(Date.now());
5138
+ if (!opts.files) store.compactIfNeeded();
5139
+ const durationMs = Date.now() - startMs;
5140
+ return {
5141
+ filesIndexed,
5142
+ symbolsIndexed,
5143
+ langStats,
5144
+ durationMs,
5145
+ errors
5146
+ };
5129
5147
  }
5130
- function connectionFor(projectRoot, indexDir) {
5131
- const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
5132
- let connection = connections.get(endpoint);
5133
- if (!connection) {
5134
- connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
5135
- connections.set(endpoint, connection);
5148
+
5149
+ // src/codebase-index/index-service.ts
5150
+ async function indexService(args, hooks = {}) {
5151
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
5152
+ try {
5153
+ return await runIndexerWithStore(store, {
5154
+ projectRoot: args.projectRoot,
5155
+ indexDir: args.indexDir,
5156
+ files: args.files,
5157
+ force: args.force,
5158
+ langs: args.langs,
5159
+ ignore: args.ignore,
5160
+ signal: hooks.signal,
5161
+ onProgress: hooks.onProgress
5162
+ });
5163
+ } finally {
5164
+ indexStorePool.release(store);
5136
5165
  }
5137
- return connection;
5138
5166
  }
5139
- function callProjectIndexServer(op, args, options) {
5140
- return connectionFor(args.projectRoot, args.indexDir).call(op, args, options);
5167
+ function searchService(args) {
5168
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
5169
+ try {
5170
+ return store.searchRanked(
5171
+ args.query,
5172
+ {
5173
+ kind: args.kind,
5174
+ lang: args.lang,
5175
+ file: args.file,
5176
+ lspKind: args.lspKind
5177
+ },
5178
+ args.limit
5179
+ );
5180
+ } finally {
5181
+ indexStorePool.release(store);
5182
+ }
5141
5183
  }
5142
- function ensureProjectIndexServer(options) {
5143
- return connectionFor(options.projectRoot, options.indexDir).configure(
5144
- options.watchExternal,
5145
- options.debounceMs
5146
- );
5184
+ function statsService(args) {
5185
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
5186
+ try {
5187
+ return store.getStats();
5188
+ } finally {
5189
+ indexStorePool.release(store);
5190
+ }
5147
5191
  }
5148
- function checkProjectIndexServerHealth(projectRoot, indexDir, options = {}) {
5149
- return connectionFor(projectRoot, indexDir).checkHealth(
5150
- false,
5151
- options.timeoutMs ?? SERVER_HEALTH_TIMEOUT_MS
5152
- );
5192
+ function packageGraphService(args) {
5193
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
5194
+ try {
5195
+ return store.getPackageGraph();
5196
+ } finally {
5197
+ indexStorePool.release(store);
5198
+ }
5153
5199
  }
5154
- async function shutdownProjectIndexServer(projectRoot, indexDir, reason) {
5155
- const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
5156
- const connection = connectionFor(projectRoot, indexDir);
5200
+ function fileGraphService(args) {
5201
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
5157
5202
  try {
5158
- return await connection.shutdownRemote(reason);
5203
+ return store.getFileGraph(args.packageFilter);
5159
5204
  } finally {
5160
- connection.close();
5161
- connections.delete(endpoint);
5162
- connectionStates.delete(endpoint);
5205
+ indexStorePool.release(store);
5163
5206
  }
5164
5207
  }
5165
- function closeProjectIndexServerClients() {
5166
- for (const connection of connections.values()) connection.close();
5167
- connections.clear();
5168
- connectionStates.clear();
5169
- latestConnectionState = {
5170
- status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
5171
- connected: false
5172
- };
5173
- if (heartbeatTimer) clearInterval(heartbeatTimer);
5174
- heartbeatTimer = void 0;
5208
+ function symbolGraphService(args) {
5209
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
5210
+ try {
5211
+ return store.getSymbolGraph(args.fileFilter);
5212
+ } finally {
5213
+ indexStorePool.release(store);
5214
+ }
5175
5215
  }
5176
5216
 
5177
5217
  // src/codebase-index/background-indexer.ts
5218
+ init_languages();
5178
5219
  var DEFAULT_FULL_INDEX_TIMEOUT_MS = 24e4;
5179
5220
  var DEFAULT_INCREMENTAL_TIMEOUT_MS = 6e4;
5180
5221
  var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
@@ -5307,8 +5348,17 @@ async function shutdownCodebaseIndexHost() {
5307
5348
  }
5308
5349
  }
5309
5350
  }
5351
+ var warnedInvalidEndpoints = /* @__PURE__ */ new Set();
5352
+ function warnEndpointInvalidOnce(availability) {
5353
+ if (warnedInvalidEndpoints.has(availability.endpoint)) return;
5354
+ warnedInvalidEndpoints.add(availability.endpoint);
5355
+ process.stderr.write(
5356
+ `codebase-index: socket path is ${availability.byteLength} bytes, over this platform's ${availability.maxBytes}-byte sun_path limit (${availability.endpoint}). Subsequent calls will reject until TMPDIR is shortened to restore the shared daemon.
5357
+ `
5358
+ );
5359
+ }
5310
5360
  function callIndexOp(op, args, opts) {
5311
- const availability = resolveProjectIndexDaemonAvailability();
5361
+ const availability = resolveProjectIndexDaemonAvailability(args.projectRoot, args.indexDir);
5312
5362
  if (availability.kind === "available") {
5313
5363
  return callProjectIndexServer(op, args, opts);
5314
5364
  }
@@ -5319,6 +5369,14 @@ function callIndexOp(op, args, opts) {
5319
5369
  )
5320
5370
  );
5321
5371
  }
5372
+ if (availability.kind === "endpoint-invalid") {
5373
+ warnEndpointInvalidOnce(availability);
5374
+ return Promise.reject(
5375
+ new Error(
5376
+ `codebase-index: socket path is ${availability.byteLength} bytes, over this platform's ${availability.maxBytes}-byte sun_path limit (${availability.endpoint}). Set a shorter TMPDIR to relocate the shared daemon, or set WRONGSTACK_INDEX_INLINE=1 to explicitly opt into a process-local index.`
5377
+ )
5378
+ );
5379
+ }
5322
5380
  const w = ensureWorker();
5323
5381
  if (!w) return callInline(op, args, opts);
5324
5382
  if (opts.signal?.aborted) {
@@ -5592,7 +5650,11 @@ function checkCodebaseIndexServerHealth(projectRoot, indexDir, options = {}) {
5592
5650
  return checkProjectIndexServerHealth(projectRoot, indexDir, options);
5593
5651
  }
5594
5652
  function ensureCodebaseIndexServer(options) {
5595
- if (!isProjectIndexServerAvailable()) return Promise.resolve();
5653
+ const availability = resolveProjectIndexDaemonAvailability(options.projectRoot, options.indexDir);
5654
+ if (availability.kind !== "available") {
5655
+ if (availability.kind === "endpoint-invalid") warnEndpointInvalidOnce(availability);
5656
+ return Promise.resolve();
5657
+ }
5596
5658
  return ensureProjectIndexServer({
5597
5659
  projectRoot: options.projectRoot,
5598
5660
  indexDir: options.indexDir,
@@ -6400,6 +6462,7 @@ export {
6400
6462
  packageGraphService2 as packageGraphService,
6401
6463
  resetIndexCircuitBreaker,
6402
6464
  resolveIndexDir,
6465
+ resolveProjectIndexDaemonAvailability,
6403
6466
  runDeadCodeScan,
6404
6467
  runIndexer,
6405
6468
  runStartupIndex,