@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
package/dist/pack.js CHANGED
@@ -479,6 +479,7 @@ var init_process_registry = __esm({
479
479
  }
480
480
  /** Get all tracked processes. */
481
481
  list() {
482
+ this._pruneAllStale();
482
483
  return Array.from(this.processes.values());
483
484
  }
484
485
  /** Get processes filtered by name (e.g. 'bash', 'exec'). */
@@ -509,6 +510,7 @@ var init_process_registry = __esm({
509
510
  * Combined stats for observability — used by /ps and the TUI status bar.
510
511
  */
511
512
  stats() {
513
+ this._pruneAllStale();
512
514
  return {
513
515
  activeCount: this.activeCount,
514
516
  backgroundCount: this.activeBackgroundCount,
@@ -748,6 +750,19 @@ var init_process_registry = __esm({
748
750
  this.processes.delete(pid);
749
751
  }
750
752
  }
753
+ /**
754
+ * Remove every stale entry, not just one PID. `list()`/`stats()` — the
755
+ * surfaces the TUI status bar and `/ps` poll — must prune too: a child
756
+ * whose 'close' event never fires (e.g. Windows grandchildren holding stdio
757
+ * open) would otherwise linger in the registry until someone looks up its
758
+ * exact PID, and PID reuse meanwhile makes `get()`/`kill()` target the
759
+ * wrong process. RAM-leak audit 2026-07-31, MEDIUM.
760
+ */
761
+ _pruneAllStale() {
762
+ for (const [pid, entry] of this.processes) {
763
+ if (this._isStaleEntry(entry)) this.processes.delete(pid);
764
+ }
765
+ }
751
766
  };
752
767
  }
753
768
  });
@@ -4893,16 +4908,16 @@ var init_legacy_bridge = __esm({
4893
4908
  });
4894
4909
 
4895
4910
  // src/codebase-index/languages.ts
4896
- import * as path14 from "node:path";
4911
+ import * as path18 from "node:path";
4897
4912
  function detectLang(file) {
4898
- const base = path14.basename(file);
4913
+ const base = path18.basename(file);
4899
4914
  const lowerBase = base.toLowerCase();
4900
4915
  if (lowerBase.endsWith(".d.ts") || lowerBase.endsWith(".d.mts") || lowerBase.endsWith(".d.cts")) {
4901
4916
  return "ts";
4902
4917
  }
4903
4918
  const special = SPECIAL_FILENAMES[lowerBase];
4904
4919
  if (special) return special;
4905
- const ext = path14.extname(base).toLowerCase();
4920
+ const ext = path18.extname(base).toLowerCase();
4906
4921
  if (!ext) return null;
4907
4922
  return EXT_TO_LANG[ext] ?? null;
4908
4923
  }
@@ -5238,10 +5253,10 @@ __export(go_parser_exports, {
5238
5253
  detectLang: () => detectLang,
5239
5254
  parseSymbols: () => parseSymbols2
5240
5255
  });
5241
- import { spawn as spawn4 } from "node:child_process";
5242
- import * as os5 from "node:os";
5243
- import * as path15 from "node:path";
5244
- import * as fs10 from "node:fs/promises";
5256
+ import { spawn as spawn5 } from "node:child_process";
5257
+ import * as os6 from "node:os";
5258
+ import * as path19 from "node:path";
5259
+ import * as fs14 from "node:fs/promises";
5245
5260
  async function parseSymbols2(opts) {
5246
5261
  const { file, content, lang } = opts;
5247
5262
  try {
@@ -5313,16 +5328,16 @@ async function syncGoParse(filePath, content, lang) {
5313
5328
  try {
5314
5329
  let scriptPath = _cachedGoScriptPath;
5315
5330
  if (!scriptPath) {
5316
- const tmpDir = await fs10.mkdtemp(path15.join(os5.tmpdir(), "ws-go-parse-"));
5317
- scriptPath = path15.join(tmpDir, "parse.go");
5318
- await fs10.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5331
+ const tmpDir = await fs14.mkdtemp(path19.join(os6.tmpdir(), "ws-go-parse-"));
5332
+ scriptPath = path19.join(tmpDir, "parse.go");
5333
+ await fs14.writeFile(scriptPath, GO_PARSE_SCRIPT, "utf8");
5319
5334
  _cachedGoScriptPath = scriptPath;
5320
5335
  }
5321
5336
  const goBinary = resolveWin32Command("go");
5322
5337
  const goResult = await new Promise(
5323
5338
  (resolve16, reject) => {
5324
5339
  let settled = false;
5325
- const proc = spawn4(goBinary, ["run", scriptPath], {
5340
+ const proc = spawn5(goBinary, ["run", scriptPath], {
5326
5341
  stdio: ["pipe", "pipe", "pipe"],
5327
5342
  windowsHide: true
5328
5343
  });
@@ -5925,10 +5940,10 @@ __export(py_parser_exports, {
5925
5940
  detectLang: () => detectLang,
5926
5941
  parseSymbols: () => parseSymbols4
5927
5942
  });
5928
- import { spawn as spawn5 } from "node:child_process";
5929
- import * as fs11 from "node:fs/promises";
5930
- import * as os6 from "node:os";
5931
- import * as path16 from "node:path";
5943
+ import { spawn as spawn6 } from "node:child_process";
5944
+ import * as fs15 from "node:fs/promises";
5945
+ import * as os7 from "node:os";
5946
+ import * as path20 from "node:path";
5932
5947
  async function parseSymbols4(opts) {
5933
5948
  const { file, content, lang } = opts;
5934
5949
  try {
@@ -5950,7 +5965,7 @@ async function resolvePython() {
5950
5965
  function commandIsAvailable(command) {
5951
5966
  return new Promise((resolve16) => {
5952
5967
  let settled = false;
5953
- const proc = spawn5(command, ["--version"], {
5968
+ const proc = spawn6(command, ["--version"], {
5954
5969
  stdio: "ignore",
5955
5970
  windowsHide: true
5956
5971
  });
@@ -5972,7 +5987,7 @@ function commandIsAvailable(command) {
5972
5987
  function spawnPyParser(pyBinary, scriptPath, filePath, content) {
5973
5988
  return new Promise((resolve16, reject) => {
5974
5989
  let settled = false;
5975
- const proc = spawn5(pyBinary, [scriptPath, filePath], {
5990
+ const proc = spawn6(pyBinary, [scriptPath, filePath], {
5976
5991
  stdio: ["pipe", "pipe", "pipe"],
5977
5992
  windowsHide: true
5978
5993
  });
@@ -6006,10 +6021,10 @@ function spawnPyParser(pyBinary, scriptPath, filePath, content) {
6006
6021
  async function syncPyParse(filePath, content, lang) {
6007
6022
  try {
6008
6023
  if (!_cachedScriptPath) {
6009
- const tmpDir = path16.join(os6.tmpdir(), "ws-py-parse");
6010
- await fs11.mkdir(tmpDir, { recursive: true });
6011
- _cachedScriptPath = path16.join(tmpDir, "parse.py");
6012
- await fs11.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
6024
+ const tmpDir = path20.join(os7.tmpdir(), "ws-py-parse");
6025
+ await fs15.mkdir(tmpDir, { recursive: true });
6026
+ _cachedScriptPath = path20.join(tmpDir, "parse.py");
6027
+ await fs15.writeFile(_cachedScriptPath, PY_PARSE_SCRIPT, "utf8");
6013
6028
  }
6014
6029
  cachedPyBinary ??= resolvePython();
6015
6030
  const pyBinary = await cachedPyBinary;
@@ -6263,10 +6278,10 @@ __export(rs_parser_exports, {
6263
6278
  detectLang: () => detectLang,
6264
6279
  parseSymbols: () => parseSymbols5
6265
6280
  });
6266
- import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
6267
- import { execFile, spawn as spawn6 } from "node:child_process";
6268
- import * as fs12 from "node:fs/promises";
6269
- import * as path17 from "node:path";
6281
+ import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
6282
+ import { execFile, spawn as spawn7 } from "node:child_process";
6283
+ import * as fs16 from "node:fs/promises";
6284
+ import * as path21 from "node:path";
6270
6285
  async function parseSymbols5(opts) {
6271
6286
  const { file, content, lang } = opts;
6272
6287
  const nativeAvailable = await checkNativeParser();
@@ -6288,7 +6303,7 @@ function checkNativeParser() {
6288
6303
  nativeParserAvailability ??= (async () => {
6289
6304
  try {
6290
6305
  await probe("rustc", ["--version"]);
6291
- const toolsDir = path17.join(process.cwd(), "tools");
6306
+ const toolsDir = path21.join(process.cwd(), "tools");
6292
6307
  await probe(
6293
6308
  "cargo",
6294
6309
  [
@@ -6297,7 +6312,7 @@ function checkNativeParser() {
6297
6312
  "--format-version",
6298
6313
  "1",
6299
6314
  "--manifest-path",
6300
- path17.join(toolsDir, "Cargo.toml")
6315
+ path21.join(toolsDir, "Cargo.toml")
6301
6316
  ]
6302
6317
  );
6303
6318
  return true;
@@ -6309,17 +6324,17 @@ function checkNativeParser() {
6309
6324
  }
6310
6325
  async function tryNativeParse(file, content) {
6311
6326
  try {
6312
- const toolsDir = path17.join(process.cwd(), "tools");
6313
- const crateDir = path17.join(toolsDir, "syn-parser");
6314
- const tmpFile = path17.join(crateDir, "src", "input.rs");
6315
- await fs12.writeFile(tmpFile, content, "utf8");
6327
+ const toolsDir = path21.join(process.cwd(), "tools");
6328
+ const crateDir = path21.join(toolsDir, "syn-parser");
6329
+ const tmpFile = path21.join(crateDir, "src", "input.rs");
6330
+ await fs16.writeFile(tmpFile, content, "utf8");
6316
6331
  const cargoBinary = resolveWin32Command("cargo");
6317
6332
  const result = await new Promise(
6318
6333
  (resolve16, reject) => {
6319
6334
  let settled = false;
6320
- const proc = spawn6(
6335
+ const proc = spawn7(
6321
6336
  cargoBinary,
6322
- ["run", "--manifest-path", path17.join(toolsDir, "Cargo.toml")],
6337
+ ["run", "--manifest-path", path21.join(toolsDir, "Cargo.toml")],
6323
6338
  {
6324
6339
  cwd: process.cwd(),
6325
6340
  stdio: ["pipe", "pipe", "pipe"],
@@ -6378,7 +6393,7 @@ function regexParse(opts) {
6378
6393
  let hi = lineOffsets2.length - 1;
6379
6394
  while (lo < hi) {
6380
6395
  const mid = lo + hi + 1 >>> 1;
6381
- if (expectDefined2(lineOffsets2[mid]) <= offset) lo = mid;
6396
+ if (expectDefined3(lineOffsets2[mid]) <= offset) lo = mid;
6382
6397
  else hi = mid - 1;
6383
6398
  }
6384
6399
  return lo + 1;
@@ -6390,7 +6405,7 @@ function regexParse(opts) {
6390
6405
  for (const pattern of RS_PATTERNS) {
6391
6406
  pattern.regex.lastIndex = 0;
6392
6407
  for (let match = pattern.regex.exec(content); match !== null; match = pattern.regex.exec(content)) {
6393
- const name = expectDefined2(match[1]);
6408
+ const name = expectDefined3(match[1]);
6394
6409
  const offset = match.index ?? 0;
6395
6410
  const line = lineFromOffset(offset);
6396
6411
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6447,8 +6462,8 @@ __export(json_parser_exports, {
6447
6462
  detectLang: () => detectLang,
6448
6463
  parseSymbols: () => parseSymbols6
6449
6464
  });
6450
- import { expectDefined as expectDefined3 } from "@wrongstack/core/utils";
6451
- import * as path18 from "node:path";
6465
+ import { expectDefined as expectDefined4 } from "@wrongstack/core/utils";
6466
+ import * as path22 from "node:path";
6452
6467
  function parseSymbols6(opts) {
6453
6468
  const { file, content, lang } = opts;
6454
6469
  try {
@@ -6460,7 +6475,7 @@ function parseSymbols6(opts) {
6460
6475
  function regexParse2(opts) {
6461
6476
  const { file, content, lang } = opts;
6462
6477
  const symbols = [];
6463
- const basename12 = path18.basename(file).toLowerCase();
6478
+ const basename12 = path22.basename(file).toLowerCase();
6464
6479
  const isPackageJson = basename12 === "package.json";
6465
6480
  const isTsconfig = basename12 === "tsconfig.json" || basename12 === "tsconfig.build.json";
6466
6481
  const isJsonSchema = content.includes("$schema") || content.includes("$id") || content.includes("$ref");
@@ -6475,22 +6490,22 @@ function regexParse2(opts) {
6475
6490
  let hi = lineOffsets2.length - 1;
6476
6491
  while (lo < hi) {
6477
6492
  const mid = lo + hi + 1 >>> 1;
6478
- if (expectDefined3(lineOffsets2[mid]) <= offset) lo = mid;
6493
+ if (expectDefined4(lineOffsets2[mid]) <= offset) lo = mid;
6479
6494
  else hi = mid - 1;
6480
6495
  }
6481
6496
  return lo + 1;
6482
6497
  }
6483
6498
  const rootMatch = content.match(/^\s*\{/m);
6484
6499
  if (rootMatch) {
6485
- const offset = expectDefined3(rootMatch.index);
6500
+ const offset = expectDefined4(rootMatch.index);
6486
6501
  const line = lineFromOffset(offset);
6487
6502
  symbols.push(
6488
6503
  makeSymbol({
6489
- name: path18.basename(file),
6504
+ name: path22.basename(file),
6490
6505
  kind: "object",
6491
6506
  line,
6492
6507
  col: 0,
6493
- signature: `"${path18.basename(file)}" = { ... }`,
6508
+ signature: `"${path22.basename(file)}" = { ... }`,
6494
6509
  file,
6495
6510
  lang
6496
6511
  })
@@ -6498,7 +6513,7 @@ function regexParse2(opts) {
6498
6513
  }
6499
6514
  const topLevelKeyRegex = /^\s*"([^"]+)"\s*:/gm;
6500
6515
  for (let match = topLevelKeyRegex.exec(content); match !== null; match = topLevelKeyRegex.exec(content)) {
6501
- const key = expectDefined3(match[1]);
6516
+ const key = expectDefined4(match[1]);
6502
6517
  const offset = match.index ?? 0;
6503
6518
  const line = lineFromOffset(offset);
6504
6519
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6545,7 +6560,7 @@ function regexParse2(opts) {
6545
6560
  const defsRegex = /"\$defs"\s*:|"\$defs"\s*:/g;
6546
6561
  const defsMatch = defsRegex.exec(content);
6547
6562
  if (defsMatch !== null) {
6548
- const offset = expectDefined3(defsMatch.index);
6563
+ const offset = expectDefined4(defsMatch.index);
6549
6564
  const line = lineFromOffset(offset);
6550
6565
  symbols.push(
6551
6566
  makeSymbol({
@@ -6570,7 +6585,7 @@ function regexParse2(opts) {
6570
6585
  for (let match = pat.exec(content); match !== null; match = pat.exec(content)) {
6571
6586
  const offset = match.index ?? 0;
6572
6587
  const line = lineFromOffset(offset);
6573
- const key = match[0]?.match(/"([^"]+)"/)?.[1] ?? expectDefined3(match[0]);
6588
+ const key = match[0]?.match(/"([^"]+)"/)?.[1] ?? expectDefined4(match[0]);
6574
6589
  symbols.push(
6575
6590
  makeSymbol({
6576
6591
  name: key,
@@ -6589,12 +6604,12 @@ function regexParse2(opts) {
6589
6604
  function extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineFromOffset) {
6590
6605
  const scriptsBlockRegex = /"scripts"\s*:\s*\{([^}]+)\}/g;
6591
6606
  for (let match = scriptsBlockRegex.exec(content); match !== null; match = scriptsBlockRegex.exec(content)) {
6592
- const blockContent = expectDefined3(match[0]);
6607
+ const blockContent = expectDefined4(match[0]);
6593
6608
  const blockOffset = match.index ?? 0;
6594
6609
  const scriptKeyRegex = /"(\w[\w-]*)"\s*:/g;
6595
6610
  for (let scriptMatch = scriptKeyRegex.exec(blockContent); scriptMatch !== null; scriptMatch = scriptKeyRegex.exec(blockContent)) {
6596
- const key = expectDefined3(scriptMatch[1]);
6597
- const keyOffset = blockOffset + expectDefined3(scriptMatch.index);
6611
+ const key = expectDefined4(scriptMatch[1]);
6612
+ const keyOffset = blockOffset + expectDefined4(scriptMatch.index);
6598
6613
  const line = lineFromOffset(keyOffset);
6599
6614
  symbols.push(
6600
6615
  makeSymbol({
@@ -6613,12 +6628,12 @@ function extractPackageScripts(content, symbols, file, lang, lineOffsets2, lineF
6613
6628
  function extractCompilerOptions(content, symbols, file, lang, lineOffsets2, parentLine, lineFromOffset) {
6614
6629
  const optsBlockRegex = /"compilerOptions"\s*:\s*\{([^}]+)\}/g;
6615
6630
  for (let match = optsBlockRegex.exec(content); match !== null; match = optsBlockRegex.exec(content)) {
6616
- const blockContent = expectDefined3(match[0]);
6631
+ const blockContent = expectDefined4(match[0]);
6617
6632
  const blockOffset = match.index ?? 0;
6618
6633
  const optKeyRegex = /"(\w[\w]*)"\s*:/g;
6619
6634
  for (let optMatch = optKeyRegex.exec(blockContent); optMatch !== null; optMatch = optKeyRegex.exec(blockContent)) {
6620
- const key = expectDefined3(optMatch[1]);
6621
- const keyOffset = blockOffset + expectDefined3(optMatch.index);
6635
+ const key = expectDefined4(optMatch[1]);
6636
+ const keyOffset = blockOffset + expectDefined4(optMatch.index);
6622
6637
  const line = lineFromOffset(keyOffset);
6623
6638
  if (line <= parentLine) continue;
6624
6639
  symbols.push(
@@ -6663,7 +6678,7 @@ __export(yaml_parser_exports, {
6663
6678
  detectLang: () => detectLang,
6664
6679
  parseSymbols: () => parseSymbols7
6665
6680
  });
6666
- import { expectDefined as expectDefined4, truncate } from "@wrongstack/core/utils";
6681
+ import { expectDefined as expectDefined5, truncate } from "@wrongstack/core/utils";
6667
6682
  function parseSymbols7(opts) {
6668
6683
  const { file, content, lang } = opts;
6669
6684
  try {
@@ -6685,14 +6700,14 @@ function regexParse3(opts) {
6685
6700
  let hi = lineOffsets2.length - 1;
6686
6701
  while (lo < hi) {
6687
6702
  const mid = lo + hi + 1 >>> 1;
6688
- if (expectDefined4(lineOffsets2[mid]) <= offset) lo = mid;
6703
+ if (expectDefined5(lineOffsets2[mid]) <= offset) lo = mid;
6689
6704
  else hi = mid - 1;
6690
6705
  }
6691
6706
  return lo + 1;
6692
6707
  }
6693
6708
  const anchorRegex = /&(\w[\w-]*)/g;
6694
6709
  for (let match = anchorRegex.exec(content); match !== null; match = anchorRegex.exec(content)) {
6695
- const name = expectDefined4(match[1]);
6710
+ const name = expectDefined5(match[1]);
6696
6711
  const offset = match.index ?? 0;
6697
6712
  const line = lineFromOffset(offset);
6698
6713
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6710,7 +6725,7 @@ function regexParse3(opts) {
6710
6725
  }
6711
6726
  const aliasRegex = /\*(\w[\w-]*)/g;
6712
6727
  for (let match = aliasRegex.exec(content); match !== null; match = aliasRegex.exec(content)) {
6713
- const name = expectDefined4(match[1]);
6728
+ const name = expectDefined5(match[1]);
6714
6729
  const offset = match.index ?? 0;
6715
6730
  const line = lineFromOffset(offset);
6716
6731
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6745,7 +6760,7 @@ function regexParse3(opts) {
6745
6760
  }
6746
6761
  const listItemRegex = /^-(\s+)([^:#\s][^:#\s]*)\s*:/gm;
6747
6762
  for (let match = listItemRegex.exec(content); match !== null; match = listItemRegex.exec(content)) {
6748
- const key = expectDefined4(match[2]);
6763
+ const key = expectDefined5(match[2]);
6749
6764
  const offset = match.index ?? 0;
6750
6765
  const line = lineFromOffset(offset);
6751
6766
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -6765,7 +6780,7 @@ function regexParse3(opts) {
6765
6780
  }
6766
6781
  const blockScalarRegex = /^(\s*)([^:#\s][^:#\s]*)\s*:\s*[|>](\s|$)/gm;
6767
6782
  for (let match = blockScalarRegex.exec(content); match !== null; match = blockScalarRegex.exec(content)) {
6768
- const key = expectDefined4(match[2]);
6783
+ const key = expectDefined5(match[2]);
6769
6784
  const offset = match.index ?? 0;
6770
6785
  const line = lineFromOffset(offset);
6771
6786
  const col = offset - (lineOffsets2[line - 1] ?? 0);
@@ -9770,10 +9785,12 @@ var browserTools = [
9770
9785
  for (const tool of browserTools) tool.icon = "web";
9771
9786
  for (const tool of browserTools) tool.timeoutMs ??= 6e4;
9772
9787
 
9773
- // src/codebase-index/background-indexer.ts
9774
- import * as fs18 from "node:fs";
9775
- import { fileURLToPath as fileURLToPath3 } from "node:url";
9776
- import { Worker } from "node:worker_threads";
9788
+ // src/codebase-index/project-server-client.ts
9789
+ import { spawn as spawn4 } from "node:child_process";
9790
+ import * as fs12 from "node:fs";
9791
+ import * as net3 from "node:net";
9792
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
9793
+ import { checkUnixSocketPath } from "@wrongstack/core/utils";
9777
9794
 
9778
9795
  // src/codebase-index/circuit-breaker.ts
9779
9796
  var CircuitOpenError = class extends Error {
@@ -9855,117 +9872,18 @@ var IndexCircuitBreaker = class {
9855
9872
  };
9856
9873
  var indexCircuitBreaker = new IndexCircuitBreaker();
9857
9874
 
9858
- // src/codebase-index/indexer.ts
9859
- import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
9860
- import { execFile as execFile2 } from "node:child_process";
9861
- import * as fs15 from "node:fs/promises";
9862
- import { availableParallelism } from "node:os";
9863
- import * as path22 from "node:path";
9864
- import {
9865
- DEFAULT_WALK_IGNORE_DIRS,
9866
- indexParallelBatchSize,
9867
- isFrugalPerf
9868
- } from "@wrongstack/core/utils";
9869
-
9870
- // src/codebase-index/gitignore.ts
9871
- import * as fs9 from "node:fs/promises";
9872
- import * as path13 from "node:path";
9873
- import { compileGlob } from "@wrongstack/core/utils";
9874
- function globBody(glob) {
9875
- return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
9876
- }
9877
- function compileGitignore(lines) {
9878
- const rules = [];
9879
- for (const raw of lines) {
9880
- let line = raw.replace(/\r$/, "");
9881
- if (!line.trim() || line.trimStart().startsWith("#")) continue;
9882
- line = line.trim();
9883
- let negated = false;
9884
- if (line.startsWith("!")) {
9885
- negated = true;
9886
- line = line.slice(1);
9887
- }
9888
- let dirOnly = false;
9889
- if (line.endsWith("/")) {
9890
- dirOnly = true;
9891
- line = line.slice(0, -1);
9892
- }
9893
- if (!line) continue;
9894
- const anchored = line.startsWith("/") || line.includes("/");
9895
- if (line.startsWith("/")) line = line.slice(1);
9896
- const body = globBody(line);
9897
- const prefix = anchored ? "^" : "(?:^|.*/)";
9898
- rules.push({
9899
- eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
9900
- under: new RegExp(`${prefix}${body}/.*$`),
9901
- negated,
9902
- dirOnly
9903
- });
9904
- }
9905
- return (relPath, isDir) => {
9906
- const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
9907
- let ignored = false;
9908
- for (const r of rules) {
9909
- const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
9910
- if (re.test(p)) ignored = !r.negated;
9911
- }
9912
- return ignored;
9913
- };
9914
- }
9915
- async function loadGitignoreMatcher(projectRoot) {
9916
- let lines = [];
9917
- try {
9918
- const raw = await fs9.readFile(path13.join(projectRoot, ".gitignore"), "utf8");
9919
- lines = raw.split("\n");
9920
- } catch {
9921
- }
9922
- return compileGitignore(lines);
9923
- }
9924
-
9925
- // src/codebase-index/indexer.ts
9926
- init_languages2();
9927
-
9928
- // src/codebase-index/parser-dispatch.ts
9929
- async function parseFileContent(file, content, lang) {
9930
- switch (lang) {
9931
- case "ts":
9932
- case "tsx":
9933
- case "js":
9934
- case "jsx": {
9935
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
9936
- return parseSymbols8({ file, content, lang });
9937
- }
9938
- case "go": {
9939
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
9940
- return parseSymbols8({ file, content, lang: "go" });
9941
- }
9942
- case "py": {
9943
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
9944
- return parseSymbols8({ file, content, lang: "py" });
9945
- }
9946
- case "rs": {
9947
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
9948
- return parseSymbols8({ file, content, lang: "rs" });
9949
- }
9950
- case "json": {
9951
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
9952
- return parseSymbols8({ file, content, lang: "json" });
9953
- }
9954
- case "yaml": {
9955
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
9956
- return parseSymbols8({ file, content, lang: "yaml" });
9957
- }
9958
- default: {
9959
- const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
9960
- return parseSymbols8({ file, content, lang });
9961
- }
9962
- }
9963
- }
9875
+ // src/codebase-index/project-server-endpoint.ts
9876
+ import { createHash as createHash4 } from "node:crypto";
9877
+ import * as fs11 from "node:fs";
9878
+ import * as os5 from "node:os";
9879
+ import * as path16 from "node:path";
9880
+ import { fileURLToPath } from "node:url";
9881
+ import { assertUnixSocketPathWithinLimit } from "@wrongstack/core/utils";
9964
9882
 
9965
9883
  // src/codebase-index/writer.ts
9966
- import { expectDefined as expectDefined5 } from "@wrongstack/core/utils";
9967
- import * as fs14 from "node:fs";
9968
- import * as path21 from "node:path";
9884
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
9885
+ import * as fs10 from "node:fs";
9886
+ import * as path15 from "node:path";
9969
9887
 
9970
9888
  // src/codebase-index/bm25.ts
9971
9889
  var K1 = 1.5;
@@ -10160,8 +10078,8 @@ function runSqliteWithRetry(fn) {
10160
10078
  }
10161
10079
 
10162
10080
  // src/codebase-index/writer-admin.ts
10163
- import * as fs13 from "node:fs";
10164
- import * as path19 from "node:path";
10081
+ import * as fs9 from "node:fs";
10082
+ import * as path13 from "node:path";
10165
10083
  var DB_FILE = "index.db";
10166
10084
  function getAllIndexableWithStatement(stmt) {
10167
10085
  return stmt("SELECT id, text FROM symbols").all().map(({ id, text }) => ({ id, text }));
@@ -10220,7 +10138,7 @@ function getAllFileMetasWithStatement(stmt) {
10220
10138
  }
10221
10139
  function getIndexDbSizeBytes(indexDir) {
10222
10140
  try {
10223
- return fs13.statSync(path19.join(indexDir, DB_FILE)).size;
10141
+ return fs9.statSync(path13.join(indexDir, DB_FILE)).size;
10224
10142
  } catch {
10225
10143
  return 0;
10226
10144
  }
@@ -10287,7 +10205,7 @@ function bulkInsertRefsWithStatement(stmt, maxSqlVars, refs) {
10287
10205
  }
10288
10206
 
10289
10207
  // src/codebase-index/writer-graph-helpers.ts
10290
- import * as path20 from "node:path";
10208
+ import * as path14 from "node:path";
10291
10209
  function derivePackage(filePath) {
10292
10210
  const f = filePath.replace(/\\/g, "/");
10293
10211
  const pkgsIdx = f.indexOf("/packages/");
@@ -10402,16 +10320,16 @@ function buildSymbolGraphNodes(symById, relatedIds, fileFilter) {
10402
10320
  function resolveRelativeImport(fromFile, moduleName, indexedFiles) {
10403
10321
  if (!moduleName.startsWith(".")) return void 0;
10404
10322
  const normalizedFrom = fromFile.replace(/\\/g, "/");
10405
- const absolute = path20.posix.normalize(
10406
- path20.posix.join(path20.posix.dirname(normalizedFrom), moduleName)
10323
+ const absolute = path14.posix.normalize(
10324
+ path14.posix.join(path14.posix.dirname(normalizedFrom), moduleName)
10407
10325
  );
10408
- const extension = path20.posix.extname(absolute);
10326
+ const extension = path14.posix.extname(absolute);
10409
10327
  const base = extension ? absolute.slice(0, -extension.length) : absolute;
10410
10328
  const candidates = [
10411
10329
  absolute,
10412
10330
  ...[".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"].map((ext) => `${base}${ext}`),
10413
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path20.posix.join(absolute, `index${ext}`)),
10414
- ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path20.posix.join(base, `index${ext}`))
10331
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(absolute, `index${ext}`)),
10332
+ ...[".ts", ".tsx", ".js", ".jsx"].map((ext) => path14.posix.join(base, `index${ext}`))
10415
10333
  ];
10416
10334
  const indexedByPortablePath = new Map(
10417
10335
  [...indexedFiles].map((file) => [file.replace(/\\/g, "/").toLocaleLowerCase(), file])
@@ -10927,9 +10845,9 @@ var IndexStore = class _IndexStore {
10927
10845
  }
10928
10846
  constructor(projectRoot, opts = {}) {
10929
10847
  this.indexDir = resolveIndexDir(projectRoot, opts.indexDir);
10930
- fs14.mkdirSync(this.indexDir, { recursive: true });
10848
+ fs10.mkdirSync(this.indexDir, { recursive: true });
10931
10849
  const Database = loadDatabaseSync();
10932
- this.db = new Database(path21.join(this.indexDir, DB_FILE2));
10850
+ this.db = new Database(path15.join(this.indexDir, DB_FILE2));
10933
10851
  applyIndexStorePragmas(this.db);
10934
10852
  this.initSchema();
10935
10853
  }
@@ -11344,13 +11262,13 @@ var IndexStore = class _IndexStore {
11344
11262
  if (rankDiff !== 0) return rankDiff;
11345
11263
  const scoreDiff = b.score - a.score;
11346
11264
  if (scoreDiff !== 0) return scoreDiff;
11347
- const left = expectDefined5(candidateById.get(a.id));
11348
- const right = expectDefined5(candidateById.get(b.id));
11265
+ const left = expectDefined2(candidateById.get(a.id));
11266
+ const right = expectDefined2(candidateById.get(b.id));
11349
11267
  return left.name.localeCompare(right.name) || left.file.localeCompare(right.file) || left.line - right.line || left.col - right.col || left.id - right.id;
11350
11268
  });
11351
11269
  const qTokens = tokenise(query);
11352
11270
  const results = scored.slice(0, limit).map(({ id, score }) => {
11353
- const c = expectDefined5(candidateById.get(id));
11271
+ const c = expectDefined2(candidateById.get(id));
11354
11272
  return { ...c, score, snippet: bm25.extractSnippet(id, qTokens) };
11355
11273
  });
11356
11274
  return { results, total: candidates.length };
@@ -11764,1134 +11682,1274 @@ var indexStorePool = new StorePool(
11764
11682
  (projectRoot, opts) => new IndexStore(projectRoot, opts)
11765
11683
  );
11766
11684
 
11767
- // src/codebase-index/indexer.ts
11768
- var YIELD_EVERY_N = 50;
11769
- function resolveParallelBatch() {
11770
- return indexParallelBatchSize(availableParallelism());
11771
- }
11772
- function yieldEventLoop() {
11773
- return new Promise((resolve16) => setImmediate(resolve16));
11685
+ // src/codebase-index/project-server-endpoint.ts
11686
+ var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
11687
+ var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
11688
+ var PROJECT_INDEX_SERVER_SOCKET_DIR = `wsci-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`;
11689
+ var buildIdCache;
11690
+ function projectIndexServerBuildId(entrypoint) {
11691
+ const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path16.resolve(entrypoint);
11692
+ try {
11693
+ const stat17 = fs11.statSync(file);
11694
+ if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat17.mtimeMs && buildIdCache.size === stat17.size) {
11695
+ return buildIdCache.buildId;
11696
+ }
11697
+ const buildId = createHash4("sha256").update(fs11.readFileSync(file)).digest("hex").slice(0, 24);
11698
+ buildIdCache = { file, mtimeMs: stat17.mtimeMs, size: stat17.size, buildId };
11699
+ return buildId;
11700
+ } catch {
11701
+ return `unreadable:${path16.basename(file)}`;
11702
+ }
11774
11703
  }
11775
- function throwIfAborted(signal) {
11776
- if (!signal?.aborted) return;
11777
- if (signal.reason instanceof Error) throw signal.reason;
11778
- throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
11704
+ function normalizeLocalPath(value) {
11705
+ const resolved = path16.resolve(value);
11706
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
11779
11707
  }
11780
- function isAbortError(err) {
11781
- return err instanceof DOMException && err.name === "AbortError";
11708
+ function projectIndexServerKey(projectRoot, indexDir) {
11709
+ const resolvedIndexDir = normalizeLocalPath(resolveIndexDir(projectRoot, indexDir));
11710
+ return createHash4("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
11782
11711
  }
11783
- var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
11784
- var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
11785
- var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
11786
- var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
11787
- var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
11788
- function isWithinProject(projectRoot, file) {
11789
- const rel = path22.relative(projectRoot, file);
11790
- return rel !== "" && !rel.startsWith(`..${path22.sep}`) && rel !== ".." && !path22.isAbsolute(rel);
11712
+ function projectIndexServerEndpoint(projectRoot, indexDir) {
11713
+ const key = projectIndexServerKey(projectRoot, indexDir);
11714
+ if (process.platform === "win32") {
11715
+ return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
11716
+ }
11717
+ return path16.join(os5.tmpdir(), PROJECT_INDEX_SERVER_SOCKET_DIR, `${key}.sock`);
11791
11718
  }
11792
- function isMissingPathError(err) {
11793
- const code = err?.code;
11794
- return code === "ENOENT" || code === "ENOTDIR";
11719
+ function projectIndexServerMetadataPath(projectRoot, indexDir) {
11720
+ return path16.join(
11721
+ path16.resolve(resolveIndexDir(projectRoot, indexDir)),
11722
+ PROJECT_INDEX_SERVER_METADATA_FILE
11723
+ );
11795
11724
  }
11796
- function normalizeComparablePath(value) {
11797
- const resolved = path22.resolve(value);
11798
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
11725
+
11726
+ // src/codebase-index/project-server-protocol.ts
11727
+ var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
11728
+ function encodeProjectServerMessage(message) {
11729
+ return `${JSON.stringify(message)}
11730
+ `;
11799
11731
  }
11800
- function gitOutput(projectRoot, args) {
11801
- return new Promise((resolve16, reject) => {
11802
- execFile2(
11803
- "git",
11804
- ["-C", projectRoot, ...args],
11805
- {
11806
- encoding: "buffer",
11807
- maxBuffer: MAX_GIT_FILE_LIST_BYTES,
11808
- windowsHide: true
11809
- },
11810
- (error, stdout) => {
11811
- if (error) reject(error);
11812
- else resolve16(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
11813
- }
11814
- );
11815
- });
11816
- }
11817
- async function findGitSourceFiles(projectRoot, ignore, signal) {
11818
- try {
11819
- throwIfAborted(signal);
11820
- const topLevel = (await gitOutput(projectRoot, ["rev-parse", "--show-toplevel"])).toString("utf8").trim();
11821
- if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
11822
- throwIfAborted(signal);
11823
- const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
11824
- const [output, statusOutput] = await Promise.all([
11825
- gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
11826
- gitOutput(projectRoot, [
11827
- "status",
11828
- "--porcelain=v1",
11829
- "-z",
11830
- "--untracked-files=all",
11831
- "--ignored=no"
11832
- ])
11833
- ]);
11834
- throwIfAborted(signal);
11835
- const dirty = /* @__PURE__ */ new Set();
11836
- const deleted = /* @__PURE__ */ new Set();
11837
- const statusRecords = statusOutput.toString("utf8").split("\0");
11838
- for (let i = 0; i < statusRecords.length; i++) {
11839
- const record = statusRecords[i];
11840
- if (!record) continue;
11841
- const status = record.slice(0, 2);
11842
- const changedPath = path22.resolve(projectRoot, record.slice(3));
11843
- dirty.add(changedPath);
11844
- if (status.includes("D")) deleted.add(changedPath);
11845
- if (status.includes("R") || status.includes("C")) {
11846
- const source = statusRecords[++i];
11847
- if (source) dirty.add(path22.resolve(projectRoot, source));
11732
+
11733
+ // src/codebase-index/project-server-client.ts
11734
+ var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
11735
+ var SERVER_START_TIMEOUT_MS = 1e4;
11736
+ var SERVER_CONTROL_TIMEOUT_MS = 5e3;
11737
+ var SERVER_HEALTH_TIMEOUT_MS = 3e3;
11738
+ var SERVER_HEARTBEAT_INTERVAL_MS = 1e4;
11739
+ var StaleProjectIndexServerError = class extends Error {
11740
+ constructor(message, pid) {
11741
+ super(message);
11742
+ this.pid = pid;
11743
+ }
11744
+ pid;
11745
+ name = "StaleProjectIndexServerError";
11746
+ };
11747
+ var connectionStates = /* @__PURE__ */ new Map();
11748
+ var connectionStateListeners = /* @__PURE__ */ new Set();
11749
+ var latestConnectionState = {
11750
+ status: "offline",
11751
+ connected: false
11752
+ };
11753
+ function resolveProjectIndexDaemonAvailability(projectRoot, indexDir) {
11754
+ if (process.env["WRONGSTACK_INDEX_INLINE"] || process.env["WRONGSTACK_INDEX_SERVER"] === "0") {
11755
+ return { kind: "inline-requested" };
11756
+ }
11757
+ let builtUrl = null;
11758
+ for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
11759
+ try {
11760
+ const url = new URL(rel, import.meta.url);
11761
+ if (url.protocol === "file:" && fs12.existsSync(fileURLToPath2(url))) {
11762
+ builtUrl = url;
11763
+ break;
11848
11764
  }
11765
+ } catch {
11849
11766
  }
11850
- const files = [];
11851
- for (const relative12 of output.toString("utf8").split("\0")) {
11852
- if (!relative12) continue;
11853
- const portable = relative12.replace(/\\/g, "/");
11854
- if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path22.posix.basename(portable))) {
11855
- continue;
11856
- }
11857
- const full = path22.resolve(projectRoot, relative12);
11858
- if (deleted.has(full)) continue;
11859
- const ext = path22.extname(relative12).toLowerCase();
11860
- if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
11767
+ }
11768
+ if (builtUrl === null) return { kind: "missing-build" };
11769
+ if (projectRoot !== void 0) {
11770
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
11771
+ const check = checkUnixSocketPath(endpoint);
11772
+ if (!check.ok) {
11773
+ return {
11774
+ kind: "endpoint-invalid",
11775
+ endpoint,
11776
+ byteLength: check.byteLength,
11777
+ maxBytes: check.maxBytes
11778
+ };
11861
11779
  }
11862
- return {
11863
- files,
11864
- trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
11865
- };
11866
- } catch {
11867
- return null;
11868
11780
  }
11781
+ return { kind: "available", url: builtUrl };
11869
11782
  }
11870
- async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
11871
- const gitFiles = await findGitSourceFiles(projectRoot, ignore, signal);
11872
- if (gitFiles) {
11783
+ function resolveProjectServerUrl() {
11784
+ const availability = resolveProjectIndexDaemonAvailability();
11785
+ return availability.kind === "available" ? availability.url : null;
11786
+ }
11787
+ function projectIndexServerExpectedBuildId() {
11788
+ const override = process.env["WRONGSTACK_INDEX_SERVER_BUILD_ID"]?.trim();
11789
+ if (override) return override;
11790
+ const url = resolveProjectServerUrl();
11791
+ return url ? projectIndexServerBuildId(url) : null;
11792
+ }
11793
+ function isProjectIndexServerAvailable() {
11794
+ return resolveProjectServerUrl() !== null;
11795
+ }
11796
+ function publishConnectionState(endpoint, state) {
11797
+ connectionStates.set(endpoint, state);
11798
+ latestConnectionState = state;
11799
+ for (const listener of connectionStateListeners) listener(state);
11800
+ }
11801
+ function getProjectIndexServerConnectionState(projectRoot, indexDir) {
11802
+ if (projectRoot) {
11803
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
11804
+ const existing = connectionStates.get(endpoint);
11805
+ if (existing) return existing;
11806
+ if (!isProjectIndexServerAvailable()) {
11807
+ return { status: "unavailable", connected: false };
11808
+ }
11873
11809
  return {
11874
- files: gitFiles.files,
11875
- complete: true,
11876
- errors: [],
11877
- trustedUnchanged: gitFiles.trustedUnchanged
11810
+ status: "offline",
11811
+ connected: false,
11812
+ projectRoot,
11813
+ indexDir,
11814
+ endpoint
11878
11815
  };
11879
11816
  }
11880
- const results = [];
11881
- const errors = [];
11882
- let complete = true;
11883
- const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
11884
- const indexableExts = new Set(INDEXABLE_EXTENSIONS);
11885
- let dirCount = 0;
11886
- const walk = async (dir) => {
11887
- throwIfAborted(signal);
11888
- if (dirCount > 0 && dirCount % YIELD_EVERY_N === 0) {
11889
- await yieldEventLoop();
11890
- throwIfAborted(signal);
11891
- }
11892
- let entries;
11893
- try {
11894
- entries = await fs15.readdir(dir, { withFileTypes: true });
11895
- } catch (err) {
11896
- complete = false;
11897
- errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
11898
- return;
11899
- }
11900
- dirCount++;
11901
- for (const e of entries) {
11902
- if (ignoreSet.has(e.name)) continue;
11903
- const full = path22.join(dir, e.name);
11904
- const rel = path22.relative(projectRoot, full).replace(/\\/g, "/");
11905
- if (e.isDirectory()) {
11906
- if (isGitIgnored(rel, true)) continue;
11907
- await walk(full);
11908
- } else if (e.isFile()) {
11909
- if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
11910
- const ext = path22.extname(e.name).toLowerCase();
11911
- if (indexableExts.has(ext) || detectLang(full) !== null) {
11912
- results.push(full);
11913
- }
11914
- }
11915
- }
11916
- };
11917
- await walk(projectRoot);
11918
- return { files: results, complete, errors };
11817
+ if (latestConnectionState.endpoint) return latestConnectionState;
11818
+ if (!isProjectIndexServerAvailable()) return { status: "unavailable", connected: false };
11819
+ return latestConnectionState;
11919
11820
  }
11920
- function assignRefsToSymbols2(refs, symbols) {
11921
- if (refs.length === 0 || symbols.length === 0) return [];
11922
- const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
11923
- const seen = /* @__PURE__ */ new Set();
11924
- const assigned = [];
11925
- for (const ref of refs) {
11926
- let owner2;
11927
- for (const symbol of ordered) {
11928
- if (symbol.line > ref.line) break;
11929
- owner2 = symbol;
11930
- }
11931
- if (!owner2 && ref.callType === "import") owner2 = ordered[0];
11932
- if (!owner2 || owner2.id <= 0) continue;
11933
- const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
11934
- if (seen.has(key)) continue;
11935
- seen.add(key);
11936
- assigned.push({ ...ref, fromId: owner2.id });
11937
- }
11938
- return assigned;
11821
+ function onProjectIndexServerConnectionStateChange(listener) {
11822
+ connectionStateListeners.add(listener);
11823
+ return () => connectionStateListeners.delete(listener);
11939
11824
  }
11940
- async function runIndexerWithStore(store, opts) {
11941
- const { projectRoot, langs, ignore = [], signal } = opts;
11942
- const relationGraphVersion = "2";
11943
- const refResolutionVersion = "2";
11944
- const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
11945
- const needsFullRefResolution = force || store.getMetadata("ref_resolution_version") !== refResolutionVersion;
11946
- const startMs = Date.now();
11947
- const errors = [];
11948
- const langStats = {};
11949
- let filesIndexed = 0;
11950
- let symbolsIndexed = 0;
11951
- const isGitIgnored = await loadGitignoreMatcher(projectRoot);
11952
- let files;
11953
- let discoveredFiles = null;
11954
- let discoveryComplete = true;
11955
- let trustedUnchanged;
11956
- if (opts.files && opts.files.length > 0) {
11957
- files = opts.files.map((f) => path22.resolve(projectRoot, f)).filter((f) => {
11958
- if (!isWithinProject(projectRoot, f)) return false;
11959
- const rel = path22.relative(projectRoot, f).replace(/\\/g, "/");
11960
- return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path22.basename(f)) && !isGitIgnored(rel, false);
11961
- });
11962
- } else {
11963
- const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
11964
- files = discovery.files;
11965
- errors.push(...discovery.errors);
11966
- discoveryComplete = discovery.complete;
11967
- discoveredFiles = new Set(files);
11968
- trustedUnchanged = discovery.trustedUnchanged;
11825
+ function remoteError(message, name) {
11826
+ if (name === "LockError") return new LockError(message);
11827
+ if (name === "IndexTimeoutError") return new IndexTimeoutError(message);
11828
+ const error = new Error(message);
11829
+ if (name && name !== "Error") error.name = name;
11830
+ return error;
11831
+ }
11832
+ function isProjectIndexServerHealth(value) {
11833
+ if (!value || typeof value !== "object") return false;
11834
+ const health = value;
11835
+ const memory = health.memory && typeof health.memory === "object" ? health.memory : void 0;
11836
+ const activity = health.activity && typeof health.activity === "object" ? health.activity : void 0;
11837
+ 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";
11838
+ }
11839
+ function delay(ms) {
11840
+ return new Promise((resolve16) => {
11841
+ const timer = setTimeout(resolve16, ms);
11842
+ timer.unref?.();
11843
+ });
11844
+ }
11845
+ function cancellationError(signal) {
11846
+ return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
11847
+ }
11848
+ var ProjectServerConnection = class {
11849
+ constructor(projectRoot, indexDir, endpoint) {
11850
+ this.projectRoot = projectRoot;
11851
+ this.indexDir = indexDir;
11852
+ this.endpoint = endpoint;
11853
+ this.transition("offline");
11969
11854
  }
11970
- if (langs && langs.length > 0) {
11971
- const langSet = new Set(langs);
11972
- files = files.filter((f) => {
11973
- const lang = detectLang(f);
11974
- return lang ? langSet.has(lang) : false;
11855
+ projectRoot;
11856
+ indexDir;
11857
+ endpoint;
11858
+ socket = null;
11859
+ buffer = "";
11860
+ info = null;
11861
+ activity = null;
11862
+ health = null;
11863
+ healthCheck = null;
11864
+ connecting = null;
11865
+ connectResolve = null;
11866
+ connectReject = null;
11867
+ nextId = 1;
11868
+ pending = /* @__PURE__ */ new Map();
11869
+ transition(status, options = {}) {
11870
+ const previous = connectionStates.get(this.endpoint);
11871
+ const pid = options.pid ?? (status === "connected" ? this.info?.pid : void 0);
11872
+ 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);
11873
+ publishConnectionState(this.endpoint, {
11874
+ status,
11875
+ connected: status === "connected" || status === "degraded" || status === "unresponsive",
11876
+ projectRoot: this.projectRoot,
11877
+ indexDir: this.indexDir,
11878
+ endpoint: this.endpoint,
11879
+ pid,
11880
+ lastError,
11881
+ ...this.activity ? { activity: this.activity } : {},
11882
+ ...this.health ? { health: this.health } : {}
11975
11883
  });
11976
11884
  }
11977
- if (force) store.clearAll();
11978
- const existingMeta = /* @__PURE__ */ new Map();
11979
- if (!force) {
11980
- for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
11885
+ isConnected() {
11886
+ return this.socket !== null && !this.socket.destroyed && this.info !== null;
11981
11887
  }
11982
- const totalFilesForProgress = files.length;
11983
- let filesPreSkipped = 0;
11984
- if (!force && trustedUnchanged) {
11985
- files = files.filter((file) => {
11986
- const meta = existingMeta.get(file);
11987
- if (!meta || !trustedUnchanged.has(file)) return true;
11988
- langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
11989
- symbolsIndexed += meta.symbolCount;
11990
- filesIndexed++;
11991
- filesPreSkipped++;
11992
- return false;
11993
- });
11994
- if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
11888
+ /** Safe LRU candidate: no request, connect, or health probe is in flight. */
11889
+ isEvictable() {
11890
+ return this.pending.size === 0 && this.connecting === null && this.healthCheck === null;
11995
11891
  }
11996
- const parallelBatch = resolveParallelBatch();
11997
- let filesSinceLastYield = 0;
11998
- for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
11999
- const batchEnd = Math.min(batchStart + parallelBatch, files.length);
12000
- const batchFiles = files.slice(batchStart, batchEnd);
12001
- opts.onProgress?.(filesPreSkipped + batchEnd, totalFilesForProgress);
12002
- filesSinceLastYield += batchFiles.length;
12003
- if (filesSinceLastYield >= YIELD_EVERY_N) {
12004
- filesSinceLastYield = 0;
12005
- await yieldEventLoop();
12006
- if (isFrugalPerf()) {
12007
- await new Promise((r) => setTimeout(r, 8));
12008
- }
12009
- throwIfAborted(signal);
11892
+ async checkHealth(spawnIfMissing = false, timeoutMs = SERVER_HEALTH_TIMEOUT_MS) {
11893
+ await this.ensureConnected(spawnIfMissing);
11894
+ if (this.healthCheck) return this.healthCheck;
11895
+ const startedAt = Date.now();
11896
+ this.healthCheck = this.request({ type: "ping" }, { timeoutMs }).then((server) => {
11897
+ const now = Date.now();
11898
+ this.health = {
11899
+ status: "healthy",
11900
+ checkedAt: now,
11901
+ lastHealthyAt: now,
11902
+ latencyMs: Math.max(0, now - startedAt),
11903
+ missedHeartbeats: 0,
11904
+ ...isProjectIndexServerHealth(server) ? { server } : {}
11905
+ };
11906
+ this.transition("connected", { pid: this.info?.pid });
11907
+ return this.health;
11908
+ }).catch((error) => {
11909
+ if (!this.isConnected()) throw error;
11910
+ if ((this.health?.lastHealthyAt ?? 0) > startedAt) return this.health;
11911
+ const missedHeartbeats = (this.health?.missedHeartbeats ?? 0) + 1;
11912
+ const status = missedHeartbeats >= 3 ? "unresponsive" : "degraded";
11913
+ this.health = {
11914
+ status,
11915
+ checkedAt: Date.now(),
11916
+ lastHealthyAt: this.health?.lastHealthyAt ?? null,
11917
+ latencyMs: null,
11918
+ missedHeartbeats,
11919
+ ...this.health?.server ? { server: this.health.server } : {}
11920
+ };
11921
+ this.transition(status, { pid: this.info?.pid, error });
11922
+ return this.health;
11923
+ }).finally(() => {
11924
+ this.healthCheck = null;
11925
+ });
11926
+ return this.healthCheck;
11927
+ }
11928
+ markResponsive() {
11929
+ const now = Date.now();
11930
+ this.health = {
11931
+ status: "healthy",
11932
+ checkedAt: now,
11933
+ lastHealthyAt: now,
11934
+ latencyMs: this.health?.latencyMs ?? null,
11935
+ missedHeartbeats: 0,
11936
+ ...this.health?.server ? { server: this.health.server } : {}
11937
+ };
11938
+ }
11939
+ async call(op, args, options) {
11940
+ if (options.signal?.aborted) throw cancellationError(options.signal);
11941
+ await this.ensureConnected(true);
11942
+ if (options.signal?.aborted) throw cancellationError(options.signal);
11943
+ return this.request({ type: "request", op, args }, options);
11944
+ }
11945
+ async shutdownRemote(reason) {
11946
+ try {
11947
+ await this.ensureConnected(false);
11948
+ } catch {
11949
+ return { stopped: false, reason: "not-running" };
12010
11950
  }
12011
- const statOpts = signal ? { signal } : {};
12012
- const statReadParse = await Promise.allSettled(
12013
- batchFiles.map(
12014
- async (file) => {
12015
- let stat17;
12016
- try {
12017
- stat17 = await fs15.stat(file, statOpts);
12018
- } catch (e) {
12019
- if (isAbortError(e)) throw e;
12020
- return {
12021
- file,
12022
- stat: null,
12023
- lang: "",
12024
- parsed: null,
12025
- error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
12026
- missing: isMissingPathError(e)
12027
- };
12028
- }
12029
- if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
12030
- const lang = detectLang(file);
12031
- if (!lang) return { file, stat: stat17, lang: "", parsed: null };
12032
- if (stat17.size > MAX_INDEX_FILE_BYTES) {
12033
- return {
12034
- file,
12035
- stat: stat17,
12036
- lang,
12037
- parsed: null,
12038
- error: `file too large (${stat17.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
12039
- };
12040
- }
12041
- const meta = existingMeta.get(file);
12042
- if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
12043
- return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
12044
- }
12045
- let content;
12046
- try {
12047
- content = await fs15.readFile(file, { encoding: "utf8", signal });
12048
- } catch (e) {
12049
- if (isAbortError(e)) throw e;
12050
- return {
12051
- file,
12052
- stat: stat17,
12053
- lang,
12054
- parsed: null,
12055
- error: `read error: ${e instanceof Error ? e.message : String(e)}`
12056
- };
12057
- }
12058
- let parsed;
12059
- try {
12060
- parsed = await parseFileContent(file, content, lang);
12061
- } catch (e) {
12062
- return {
12063
- file,
12064
- stat: stat17,
12065
- lang,
12066
- parsed: null,
12067
- error: `parse error: ${e instanceof Error ? e.message : String(e)}`
12068
- };
12069
- }
12070
- return { file, stat: stat17, lang, parsed, content };
12071
- }
12072
- )
11951
+ const pid = this.info?.pid;
11952
+ try {
11953
+ this.transition("stopping", { pid });
11954
+ await this.request(
11955
+ { type: "shutdown", reason },
11956
+ { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
11957
+ );
11958
+ return { stopped: true, pid };
11959
+ } catch (error) {
11960
+ const forceKilled = this.forceKillKnownServer();
11961
+ return {
11962
+ stopped: forceKilled,
11963
+ pid,
11964
+ reason: forceKilled ? `force-killed after graceful shutdown failed: ${error instanceof Error ? error.message : String(error)}` : error instanceof Error ? error.message : String(error)
11965
+ };
11966
+ } finally {
11967
+ this.close();
11968
+ }
11969
+ }
11970
+ async configure(watchExternal, debounceMs) {
11971
+ await this.ensureConnected(true);
11972
+ const startedAt = Date.now();
11973
+ const result = await this.request(
11974
+ { type: "configure", watchExternal, debounceMs },
11975
+ { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
12073
11976
  );
12074
- const batchEntries = [];
12075
- const deleteForFiles = [];
12076
- for (let fi = 0; fi < statReadParse.length; fi++) {
12077
- const settled = statReadParse[fi];
12078
- const file = expectDefined6(batchFiles[fi]);
12079
- if (settled.status === "rejected") {
12080
- const err = settled.reason;
12081
- if (err instanceof Error && isAbortError(err)) throw err;
12082
- errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
12083
- continue;
11977
+ if (isProjectIndexServerHealth(result.health)) {
11978
+ const now = Date.now();
11979
+ this.health = {
11980
+ status: "healthy",
11981
+ checkedAt: now,
11982
+ lastHealthyAt: now,
11983
+ latencyMs: Math.max(0, now - startedAt),
11984
+ missedHeartbeats: 0,
11985
+ server: result.health
11986
+ };
11987
+ this.transition("connected", { pid: this.info?.pid });
11988
+ }
11989
+ }
11990
+ close() {
11991
+ const socket = this.socket;
11992
+ this.socket = null;
11993
+ this.info = null;
11994
+ this.activity = null;
11995
+ this.health = null;
11996
+ this.connectReject?.(new Error("codebase-index client disconnected"));
11997
+ this.connectResolve = null;
11998
+ this.connectReject = null;
11999
+ if (socket && !socket.destroyed) socket.destroy();
12000
+ this.rejectPending(new Error("codebase-index client disconnected"));
12001
+ this.transition("offline");
12002
+ maybeStopHeartbeatLoop();
12003
+ }
12004
+ request(message, options) {
12005
+ const socket = this.socket;
12006
+ if (!socket || socket.destroyed) {
12007
+ return Promise.reject(new Error("codebase-index server connection is not available"));
12008
+ }
12009
+ const id = this.nextId++;
12010
+ return new Promise((resolve16, reject) => {
12011
+ const timer = setTimeout(() => {
12012
+ const entry = this.pending.get(id);
12013
+ if (!entry) return;
12014
+ this.pending.delete(id);
12015
+ this.write({ type: "cancel", id });
12016
+ const error = new IndexTimeoutError(
12017
+ `Index ${message.type === "request" ? message.op : message.type} exceeded its ${options.timeoutMs}ms watchdog timeout`
12018
+ );
12019
+ this.cleanupPending(entry);
12020
+ entry.reject(error);
12021
+ }, options.timeoutMs);
12022
+ timer.unref?.();
12023
+ const signal = options.signal;
12024
+ const onAbort = signal ? () => {
12025
+ const entry = this.pending.get(id);
12026
+ if (!entry) return;
12027
+ this.pending.delete(id);
12028
+ this.write({ type: "cancel", id });
12029
+ this.cleanupPending(entry);
12030
+ entry.reject(cancellationError(signal));
12031
+ } : void 0;
12032
+ this.pending.set(id, {
12033
+ resolve: resolve16,
12034
+ reject,
12035
+ timer,
12036
+ signal,
12037
+ onAbort,
12038
+ onProgress: options.onProgress
12039
+ });
12040
+ if (signal && onAbort) {
12041
+ signal.addEventListener("abort", onAbort, { once: true });
12042
+ if (signal.aborted) {
12043
+ onAbort();
12044
+ return;
12045
+ }
12084
12046
  }
12085
- const result = settled.value;
12086
- if (result.error) {
12087
- if (result.missing) store.deleteFile(file);
12088
- errors.push(`${file}: ${result.error}`);
12089
- continue;
12047
+ this.write({ ...message, id });
12048
+ });
12049
+ }
12050
+ async ensureConnected(spawnIfMissing) {
12051
+ if (this.socket && !this.socket.destroyed && this.info) return;
12052
+ if (this.connecting) return this.connecting;
12053
+ this.transition("connecting");
12054
+ this.connecting = this.connectWithElection(spawnIfMissing).catch((error) => {
12055
+ this.transition("error", { error });
12056
+ throw error;
12057
+ }).finally(() => {
12058
+ this.connecting = null;
12059
+ });
12060
+ return this.connecting;
12061
+ }
12062
+ async connectWithElection(spawnIfMissing) {
12063
+ const deadline = Date.now() + (spawnIfMissing ? SERVER_START_TIMEOUT_MS : CONNECT_ATTEMPT_TIMEOUT_MS);
12064
+ let spawned = false;
12065
+ let staleAttempts = 0;
12066
+ let lastError = new Error("codebase-index server unavailable");
12067
+ while (Date.now() < deadline) {
12068
+ try {
12069
+ await this.connectOnce();
12070
+ return;
12071
+ } catch (error) {
12072
+ lastError = error;
12073
+ if (error instanceof StaleProjectIndexServerError) {
12074
+ staleAttempts++;
12075
+ if (!spawnIfMissing) break;
12076
+ if (staleAttempts >= 3) this.forceKillServer(error.pid);
12077
+ spawned = false;
12078
+ await delay(100);
12079
+ continue;
12080
+ }
12090
12081
  }
12091
- const { stat: stat17, lang, parsed } = result;
12092
- if (result.skippedMeta) {
12093
- langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
12094
- symbolsIndexed += result.skippedMeta.symbolCount;
12095
- filesIndexed++;
12096
- continue;
12082
+ if (!spawnIfMissing) break;
12083
+ if (!spawned) {
12084
+ this.spawnDetachedServer();
12085
+ spawned = true;
12097
12086
  }
12098
- if (!lang || !parsed) {
12099
- if (lang) {
12100
- store.upsertFile({
12101
- file,
12102
- lang,
12103
- mtimeMs: Math.floor(stat17.mtimeMs),
12104
- symbolCount: 0,
12105
- lastIndexed: Date.now()
12106
- });
12107
- filesIndexed++;
12087
+ await delay(75);
12088
+ }
12089
+ throw lastError;
12090
+ }
12091
+ connectOnce() {
12092
+ this.socket?.destroy();
12093
+ this.socket = null;
12094
+ this.info = null;
12095
+ this.activity = null;
12096
+ this.health = null;
12097
+ this.buffer = "";
12098
+ return new Promise((resolve16, reject) => {
12099
+ const socket = net3.createConnection(this.endpoint);
12100
+ this.socket = socket;
12101
+ socket.setEncoding("utf8");
12102
+ const timer = setTimeout(() => {
12103
+ reject(new Error("codebase-index server handshake timed out"));
12104
+ socket.destroy();
12105
+ }, CONNECT_ATTEMPT_TIMEOUT_MS);
12106
+ timer.unref?.();
12107
+ const finishResolve = () => {
12108
+ clearTimeout(timer);
12109
+ this.connectResolve = null;
12110
+ this.connectReject = null;
12111
+ resolve16();
12112
+ };
12113
+ const finishReject = (error) => {
12114
+ clearTimeout(timer);
12115
+ this.connectResolve = null;
12116
+ this.connectReject = null;
12117
+ reject(error);
12118
+ };
12119
+ this.connectResolve = finishResolve;
12120
+ this.connectReject = finishReject;
12121
+ socket.on("data", (chunk) => this.onData(socket, chunk));
12122
+ socket.on("error", (error) => {
12123
+ if (!this.info) finishReject(error);
12124
+ });
12125
+ socket.on("close", () => this.onClose(socket));
12126
+ });
12127
+ }
12128
+ onData(socket, chunk) {
12129
+ if (socket !== this.socket) return;
12130
+ this.buffer += chunk;
12131
+ while (true) {
12132
+ const newline = this.buffer.indexOf("\n");
12133
+ if (newline < 0) {
12134
+ if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12135
+ socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
12108
12136
  }
12109
- continue;
12137
+ return;
12110
12138
  }
12111
- if (parsed.symbols.length === 0) {
12112
- store.replaceEmptyFile({
12113
- file,
12114
- lang,
12115
- mtimeMs: Math.floor(stat17.mtimeMs),
12116
- symbolCount: 0,
12117
- lastIndexed: Date.now()
12118
- });
12119
- filesIndexed++;
12120
- continue;
12139
+ if (newline > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12140
+ socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
12141
+ return;
12121
12142
  }
12122
- batchEntries.push({
12123
- file,
12124
- lang,
12125
- symbols: parsed.symbols,
12126
- refs: parsed.refs ?? [],
12127
- mtimeMs: Math.floor(stat17.mtimeMs),
12128
- symbolCount: parsed.symbols.length
12129
- });
12130
- deleteForFiles.push(file);
12143
+ const line = this.buffer.slice(0, newline);
12144
+ this.buffer = this.buffer.slice(newline + 1);
12145
+ if (!line) continue;
12146
+ let message;
12147
+ try {
12148
+ message = JSON.parse(line);
12149
+ } catch {
12150
+ socket.destroy(new Error("invalid codebase-index server response"));
12151
+ return;
12152
+ }
12153
+ this.onMessage(message);
12131
12154
  }
12132
- if (batchEntries.length > 0) {
12155
+ }
12156
+ onMessage(message) {
12157
+ if (message.type === "hello") {
12158
+ if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
12159
+ this.rejectStaleServer(
12160
+ message,
12161
+ `codebase-index protocol mismatch: client=${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}, server=${message.protocolVersion}`
12162
+ );
12163
+ return;
12164
+ }
12165
+ const expectedBuildId = projectIndexServerExpectedBuildId();
12166
+ if (expectedBuildId && message.buildId !== expectedBuildId) {
12167
+ this.rejectStaleServer(
12168
+ message,
12169
+ `codebase-index build mismatch: client=${expectedBuildId}, server=${message.buildId ?? "legacy"}`
12170
+ );
12171
+ return;
12172
+ }
12173
+ this.info = message;
12174
+ this.markResponsive();
12175
+ this.transition("connected", { pid: message.pid });
12176
+ ensureHeartbeatLoop();
12177
+ this.connectResolve?.();
12178
+ return;
12179
+ }
12180
+ if (message.type === "index-state") {
12181
+ this.activity = message.state;
12182
+ this.markResponsive();
12183
+ this.transition("connected", { pid: this.info?.pid });
12184
+ return;
12185
+ }
12186
+ const entry = this.pending.get(message.id);
12187
+ if (!entry) return;
12188
+ this.markResponsive();
12189
+ const status = connectionStates.get(this.endpoint)?.status;
12190
+ if (status === "degraded" || status === "unresponsive") {
12191
+ this.transition("connected", { pid: this.info?.pid });
12192
+ }
12193
+ if (message.type === "progress") {
12194
+ entry.onProgress?.(message.current, message.total);
12195
+ return;
12196
+ }
12197
+ this.pending.delete(message.id);
12198
+ this.cleanupPending(entry);
12199
+ if (message.ok) entry.resolve(message.result);
12200
+ else entry.reject(remoteError(message.error, message.errorName));
12201
+ }
12202
+ onClose(socket) {
12203
+ if (socket !== this.socket) return;
12204
+ const wasConnected = this.info !== null;
12205
+ this.socket = null;
12206
+ this.info = null;
12207
+ this.activity = null;
12208
+ this.health = null;
12209
+ const error = new Error("codebase-index server connection closed");
12210
+ this.connectReject?.(error);
12211
+ this.connectResolve = null;
12212
+ this.connectReject = null;
12213
+ this.rejectPending(error);
12214
+ if (wasConnected) this.transition("error", { error });
12215
+ maybeStopHeartbeatLoop();
12216
+ }
12217
+ cleanupPending(entry) {
12218
+ clearTimeout(entry.timer);
12219
+ if (entry.signal && entry.onAbort) {
12220
+ entry.signal.removeEventListener("abort", entry.onAbort);
12221
+ }
12222
+ }
12223
+ rejectPending(error) {
12224
+ const entries = [...this.pending.values()];
12225
+ this.pending.clear();
12226
+ for (const entry of entries) {
12227
+ this.cleanupPending(entry);
12228
+ entry.reject(error);
12229
+ }
12230
+ }
12231
+ write(message) {
12232
+ const socket = this.socket;
12233
+ if (socket && !socket.destroyed) socket.write(encodeProjectServerMessage(message));
12234
+ }
12235
+ rejectStaleServer(message, reason) {
12236
+ const socket = this.socket;
12237
+ if (socket && !socket.destroyed) {
12238
+ socket.write(
12239
+ encodeProjectServerMessage({
12240
+ type: "shutdown",
12241
+ id: 0,
12242
+ reason: "stale-build-replacement"
12243
+ })
12244
+ );
12245
+ const timer = setTimeout(() => socket.destroy(), 25);
12246
+ timer.unref?.();
12247
+ }
12248
+ this.connectReject?.(new StaleProjectIndexServerError(reason, message.pid));
12249
+ }
12250
+ spawnDetachedServer() {
12251
+ const url = resolveProjectServerUrl();
12252
+ if (!url) throw new Error("built codebase-index project server is unavailable");
12253
+ if (process.platform !== "win32") {
12133
12254
  try {
12134
- store.commitBatch(batchEntries, { deleteForFiles });
12135
- for (const entry of batchEntries) {
12136
- const count = entry.symbols.length;
12137
- symbolsIndexed += count;
12138
- langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
12139
- filesIndexed++;
12140
- }
12141
- } catch (err) {
12142
- const message = err instanceof Error ? err.message : String(err);
12143
- errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
12144
- for (const entry of batchEntries) {
12145
- try {
12146
- store.deleteRefsForFile(entry.file);
12147
- store.deleteSymbolsForFile(entry.file);
12148
- const symbolsWithIds = store.insertSymbols(entry.symbols);
12149
- symbolsIndexed += symbolsWithIds.length;
12150
- langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
12151
- filesIndexed++;
12152
- if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
12153
- const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
12154
- if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
12155
- }
12156
- store.resolveRefsForNames([
12157
- ...entry.symbols.map((symbol) => symbol.name),
12158
- ...entry.refs.map((ref) => ref.toName)
12159
- ]);
12160
- store.upsertFile({
12161
- file: entry.file,
12162
- lang: entry.lang,
12163
- mtimeMs: entry.mtimeMs,
12164
- symbolCount: entry.symbolCount,
12165
- lastIndexed: Date.now()
12166
- });
12167
- } catch (innerErr) {
12168
- errors.push(
12169
- `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
12170
- );
12171
- }
12172
- }
12255
+ fs12.rmSync(this.endpoint, { force: true });
12256
+ } catch {
12173
12257
  }
12174
12258
  }
12259
+ const args = [fileURLToPath2(url), "--project-root", this.projectRoot];
12260
+ if (this.indexDir) args.push("--index-dir", this.indexDir);
12261
+ const child = spawn4(process.execPath, args, {
12262
+ detached: true,
12263
+ stdio: "ignore",
12264
+ windowsHide: true,
12265
+ env: process.env
12266
+ });
12267
+ child.unref();
12175
12268
  }
12176
- if (discoveredFiles && discoveryComplete) {
12177
- for (const [file_] of existingMeta) {
12178
- if (!discoveredFiles.has(file_)) {
12179
- store.deleteFile(file_);
12269
+ forceKillKnownServer() {
12270
+ const pid = this.info?.pid;
12271
+ return pid ? this.forceKillServer(pid) : false;
12272
+ }
12273
+ forceKillServer(pid) {
12274
+ if (pid === process.pid) return false;
12275
+ try {
12276
+ process.kill(pid);
12277
+ const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
12278
+ try {
12279
+ const metadata = JSON.parse(fs12.readFileSync(metadataPath, "utf8"));
12280
+ if (metadata.pid === pid) fs12.rmSync(metadataPath, { force: true });
12281
+ } catch {
12180
12282
  }
12283
+ return true;
12284
+ } catch {
12285
+ return false;
12181
12286
  }
12182
12287
  }
12183
- if (needsFullRefResolution) store.resolveRefs();
12184
- store.setMetadata("ref_resolution_version", refResolutionVersion);
12185
- store.setMetadata("relation_graph_version", relationGraphVersion);
12186
- if (!opts.files || filesIndexed >= 50) store.optimize();
12187
- store.setLastIndexed(Date.now());
12188
- if (!opts.files) store.compactIfNeeded();
12189
- const durationMs = Date.now() - startMs;
12190
- return {
12191
- filesIndexed,
12192
- symbolsIndexed,
12193
- langStats,
12194
- durationMs,
12195
- errors
12288
+ };
12289
+ var connections = /* @__PURE__ */ new Map();
12290
+ var MAX_CACHED_CONNECTIONS = 8;
12291
+ var heartbeatTimer;
12292
+ function forgetConnection(endpoint, connection) {
12293
+ if (connections.get(endpoint) === connection) connections.delete(endpoint);
12294
+ connection.close();
12295
+ connectionStates.delete(endpoint);
12296
+ if (latestConnectionState.endpoint !== endpoint) return;
12297
+ latestConnectionState = [...connectionStates.values()].at(-1) ?? {
12298
+ status: isProjectIndexServerAvailable() ? "offline" : "unavailable",
12299
+ connected: false
12196
12300
  };
12197
12301
  }
12198
-
12199
- // src/codebase-index/index-service.ts
12200
- async function indexService(args, hooks = {}) {
12201
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12202
- try {
12203
- return await runIndexerWithStore(store, {
12204
- projectRoot: args.projectRoot,
12205
- indexDir: args.indexDir,
12206
- files: args.files,
12207
- force: args.force,
12208
- langs: args.langs,
12209
- ignore: args.ignore,
12210
- signal: hooks.signal,
12211
- onProgress: hooks.onProgress
12212
- });
12213
- } finally {
12214
- indexStorePool.release(store);
12215
- }
12216
- }
12217
- function searchService(args) {
12218
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12219
- try {
12220
- return store.searchRanked(
12221
- args.query,
12222
- {
12223
- kind: args.kind,
12224
- lang: args.lang,
12225
- file: args.file,
12226
- lspKind: args.lspKind
12227
- },
12228
- args.limit
12229
- );
12230
- } finally {
12231
- indexStorePool.release(store);
12302
+ function trimConnectionCache(protectedConnection) {
12303
+ if (connections.size <= MAX_CACHED_CONNECTIONS) return;
12304
+ for (const [endpoint, connection] of connections) {
12305
+ if (connections.size <= MAX_CACHED_CONNECTIONS) break;
12306
+ if (connection === protectedConnection || !connection.isEvictable()) continue;
12307
+ forgetConnection(endpoint, connection);
12232
12308
  }
12233
12309
  }
12234
- function statsService(args) {
12235
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12236
- try {
12237
- return store.getStats();
12238
- } finally {
12239
- indexStorePool.release(store);
12240
- }
12310
+ function ensureHeartbeatLoop() {
12311
+ if (heartbeatTimer) return;
12312
+ heartbeatTimer = setInterval(() => {
12313
+ for (const connection of connections.values()) {
12314
+ if (connection.isConnected()) void connection.checkHealth(false).catch(() => {
12315
+ });
12316
+ }
12317
+ }, SERVER_HEARTBEAT_INTERVAL_MS);
12318
+ heartbeatTimer.unref?.();
12241
12319
  }
12242
- function packageGraphService(args) {
12243
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12244
- try {
12245
- return store.getPackageGraph();
12246
- } finally {
12247
- indexStorePool.release(store);
12248
- }
12320
+ function maybeStopHeartbeatLoop() {
12321
+ if (!heartbeatTimer) return;
12322
+ if ([...connections.values()].some((connection) => connection.isConnected())) return;
12323
+ clearInterval(heartbeatTimer);
12324
+ heartbeatTimer = void 0;
12249
12325
  }
12250
- function fileGraphService(args) {
12251
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12252
- try {
12253
- return store.getFileGraph(args.packageFilter);
12254
- } finally {
12255
- indexStorePool.release(store);
12326
+ function connectionFor(projectRoot, indexDir) {
12327
+ const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
12328
+ let connection = connections.get(endpoint);
12329
+ if (!connection) {
12330
+ connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
12331
+ connections.set(endpoint, connection);
12332
+ } else {
12333
+ connections.delete(endpoint);
12334
+ connections.set(endpoint, connection);
12256
12335
  }
12336
+ trimConnectionCache(connection);
12337
+ return connection;
12257
12338
  }
12258
- function symbolGraphService(args) {
12259
- const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12260
- try {
12261
- return store.getSymbolGraph(args.fileFilter);
12262
- } finally {
12263
- indexStorePool.release(store);
12264
- }
12339
+ function callProjectIndexServer(op, args, options) {
12340
+ return connectionFor(args.projectRoot, args.indexDir).call(op, args, options);
12265
12341
  }
12266
12342
 
12267
- // src/codebase-index/project-server-client.ts
12268
- import { spawn as spawn7 } from "node:child_process";
12269
- import * as fs17 from "node:fs";
12270
- import * as net3 from "node:net";
12271
- import { fileURLToPath as fileURLToPath2 } from "node:url";
12343
+ // src/codebase-index/background-indexer.ts
12344
+ import * as fs18 from "node:fs";
12345
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
12346
+ import { Worker } from "node:worker_threads";
12272
12347
 
12273
- // src/codebase-index/project-server-endpoint.ts
12274
- import { createHash as createHash4 } from "node:crypto";
12275
- import * as fs16 from "node:fs";
12276
- import * as os7 from "node:os";
12348
+ // src/codebase-index/indexer.ts
12349
+ import { expectDefined as expectDefined6 } from "@wrongstack/core/utils";
12350
+ import { execFile as execFile2 } from "node:child_process";
12351
+ import * as fs17 from "node:fs/promises";
12352
+ import { availableParallelism } from "node:os";
12277
12353
  import * as path23 from "node:path";
12278
- import { fileURLToPath } from "node:url";
12279
- var PROJECT_INDEX_SERVER_PROTOCOL_VERSION = 1;
12280
- var PROJECT_INDEX_SERVER_METADATA_FILE = "server.json";
12281
- var buildIdCache;
12282
- function projectIndexServerBuildId(entrypoint) {
12283
- const file = entrypoint instanceof URL || entrypoint.startsWith("file:") ? fileURLToPath(entrypoint) : path23.resolve(entrypoint);
12284
- try {
12285
- const stat17 = fs16.statSync(file);
12286
- if (buildIdCache?.file === file && buildIdCache.mtimeMs === stat17.mtimeMs && buildIdCache.size === stat17.size) {
12287
- return buildIdCache.buildId;
12288
- }
12289
- const buildId = createHash4("sha256").update(fs16.readFileSync(file)).digest("hex").slice(0, 24);
12290
- buildIdCache = { file, mtimeMs: stat17.mtimeMs, size: stat17.size, buildId };
12291
- return buildId;
12292
- } catch {
12293
- return `unreadable:${path23.basename(file)}`;
12294
- }
12295
- }
12296
- function normalizeLocalPath(value) {
12297
- const resolved = path23.resolve(value);
12298
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12299
- }
12300
- function projectIndexServerKey(projectRoot, indexDir) {
12301
- const resolvedIndexDir = normalizeLocalPath(resolveIndexDir(projectRoot, indexDir));
12302
- return createHash4("sha256").update(resolvedIndexDir).digest("hex").slice(0, 24);
12354
+ import {
12355
+ DEFAULT_WALK_IGNORE_DIRS,
12356
+ indexParallelBatchSize,
12357
+ isFrugalPerf
12358
+ } from "@wrongstack/core/utils";
12359
+
12360
+ // src/codebase-index/gitignore.ts
12361
+ import * as fs13 from "node:fs/promises";
12362
+ import * as path17 from "node:path";
12363
+ import { compileGlob } from "@wrongstack/core/utils";
12364
+ function globBody(glob) {
12365
+ return compileGlob(glob).source.replace(/^\^/, "").replace(/\$$/, "");
12303
12366
  }
12304
- function projectIndexServerEndpoint(projectRoot, indexDir) {
12305
- const key = projectIndexServerKey(projectRoot, indexDir);
12306
- if (process.platform === "win32") {
12307
- return `\\\\.\\pipe\\wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}-${key}`;
12367
+ function compileGitignore(lines) {
12368
+ const rules = [];
12369
+ for (const raw of lines) {
12370
+ let line = raw.replace(/\r$/, "");
12371
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
12372
+ line = line.trim();
12373
+ let negated = false;
12374
+ if (line.startsWith("!")) {
12375
+ negated = true;
12376
+ line = line.slice(1);
12377
+ }
12378
+ let dirOnly = false;
12379
+ if (line.endsWith("/")) {
12380
+ dirOnly = true;
12381
+ line = line.slice(0, -1);
12382
+ }
12383
+ if (!line) continue;
12384
+ const anchored = line.startsWith("/") || line.includes("/");
12385
+ if (line.startsWith("/")) line = line.slice(1);
12386
+ const body = globBody(line);
12387
+ const prefix = anchored ? "^" : "(?:^|.*/)";
12388
+ rules.push({
12389
+ eqOrUnder: new RegExp(`${prefix}${body}(?:/.*)?$`),
12390
+ under: new RegExp(`${prefix}${body}/.*$`),
12391
+ negated,
12392
+ dirOnly
12393
+ });
12308
12394
  }
12309
- return path23.join(
12310
- os7.tmpdir(),
12311
- `wrongstack-codebase-index-v${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}`,
12312
- `${key}.sock`
12313
- );
12395
+ return (relPath, isDir) => {
12396
+ const p = relPath.replace(/\\/g, "/").replace(/^\/+/, "");
12397
+ let ignored = false;
12398
+ for (const r of rules) {
12399
+ const re = r.dirOnly && !isDir ? r.under : r.eqOrUnder;
12400
+ if (re.test(p)) ignored = !r.negated;
12401
+ }
12402
+ return ignored;
12403
+ };
12314
12404
  }
12315
- function projectIndexServerMetadataPath(projectRoot, indexDir) {
12316
- return path23.join(
12317
- path23.resolve(resolveIndexDir(projectRoot, indexDir)),
12318
- PROJECT_INDEX_SERVER_METADATA_FILE
12319
- );
12405
+ async function loadGitignoreMatcher(projectRoot) {
12406
+ let lines = [];
12407
+ try {
12408
+ const raw = await fs13.readFile(path17.join(projectRoot, ".gitignore"), "utf8");
12409
+ lines = raw.split("\n");
12410
+ } catch {
12411
+ }
12412
+ return compileGitignore(lines);
12320
12413
  }
12321
12414
 
12322
- // src/codebase-index/project-server-protocol.ts
12323
- var PROJECT_INDEX_SERVER_MAX_FRAME_CHARS = 64 * 1024 * 1024;
12324
- function encodeProjectServerMessage(message) {
12325
- return `${JSON.stringify(message)}
12326
- `;
12327
- }
12415
+ // src/codebase-index/indexer.ts
12416
+ init_languages2();
12328
12417
 
12329
- // src/codebase-index/project-server-client.ts
12330
- var CONNECT_ATTEMPT_TIMEOUT_MS = 750;
12331
- var SERVER_START_TIMEOUT_MS = 1e4;
12332
- var SERVER_CONTROL_TIMEOUT_MS = 5e3;
12333
- var SERVER_HEALTH_TIMEOUT_MS = 3e3;
12334
- var SERVER_HEARTBEAT_INTERVAL_MS = 1e4;
12335
- var StaleProjectIndexServerError = class extends Error {
12336
- constructor(message, pid) {
12337
- super(message);
12338
- this.pid = pid;
12339
- }
12340
- pid;
12341
- name = "StaleProjectIndexServerError";
12342
- };
12343
- var connectionStates = /* @__PURE__ */ new Map();
12344
- var connectionStateListeners = /* @__PURE__ */ new Set();
12345
- var latestConnectionState = {
12346
- status: "offline",
12347
- connected: false
12348
- };
12349
- function resolveProjectIndexDaemonAvailability() {
12350
- if (process.env["WRONGSTACK_INDEX_INLINE"] || process.env["WRONGSTACK_INDEX_SERVER"] === "0") {
12351
- return { kind: "inline-requested" };
12352
- }
12353
- for (const rel of ["./project-server.js", "./codebase-index/project-server.js"]) {
12354
- try {
12355
- const url = new URL(rel, import.meta.url);
12356
- if (url.protocol === "file:" && fs17.existsSync(fileURLToPath2(url))) {
12357
- return { kind: "available", url };
12358
- }
12359
- } catch {
12418
+ // src/codebase-index/parser-dispatch.ts
12419
+ async function parseFileContent(file, content, lang) {
12420
+ switch (lang) {
12421
+ case "ts":
12422
+ case "tsx":
12423
+ case "js":
12424
+ case "jsx": {
12425
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_ts_parser(), ts_parser_exports));
12426
+ return parseSymbols8({ file, content, lang });
12427
+ }
12428
+ case "go": {
12429
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_go_parser(), go_parser_exports));
12430
+ return parseSymbols8({ file, content, lang: "go" });
12431
+ }
12432
+ case "py": {
12433
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_py_parser(), py_parser_exports));
12434
+ return parseSymbols8({ file, content, lang: "py" });
12435
+ }
12436
+ case "rs": {
12437
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_rs_parser(), rs_parser_exports));
12438
+ return parseSymbols8({ file, content, lang: "rs" });
12439
+ }
12440
+ case "json": {
12441
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_json_parser(), json_parser_exports));
12442
+ return parseSymbols8({ file, content, lang: "json" });
12443
+ }
12444
+ case "yaml": {
12445
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_yaml_parser(), yaml_parser_exports));
12446
+ return parseSymbols8({ file, content, lang: "yaml" });
12447
+ }
12448
+ default: {
12449
+ const { parseSymbols: parseSymbols8 } = await Promise.resolve().then(() => (init_generic_parser(), generic_parser_exports));
12450
+ return parseSymbols8({ file, content, lang });
12360
12451
  }
12361
12452
  }
12362
- return { kind: "missing-build" };
12363
- }
12364
- function resolveProjectServerUrl() {
12365
- const availability = resolveProjectIndexDaemonAvailability();
12366
- return availability.kind === "available" ? availability.url : null;
12367
- }
12368
- function projectIndexServerExpectedBuildId() {
12369
- const override = process.env["WRONGSTACK_INDEX_SERVER_BUILD_ID"]?.trim();
12370
- if (override) return override;
12371
- const url = resolveProjectServerUrl();
12372
- return url ? projectIndexServerBuildId(url) : null;
12373
- }
12374
- function isProjectIndexServerAvailable() {
12375
- return resolveProjectServerUrl() !== null;
12376
12453
  }
12377
- function publishConnectionState(endpoint, state) {
12378
- connectionStates.set(endpoint, state);
12379
- latestConnectionState = state;
12380
- for (const listener of connectionStateListeners) listener(state);
12454
+
12455
+ // src/codebase-index/indexer.ts
12456
+ var YIELD_EVERY_N = 50;
12457
+ function resolveParallelBatch() {
12458
+ return indexParallelBatchSize(availableParallelism());
12381
12459
  }
12382
- function getProjectIndexServerConnectionState(projectRoot, indexDir) {
12383
- if (projectRoot) {
12384
- const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
12385
- const existing = connectionStates.get(endpoint);
12386
- if (existing) return existing;
12387
- if (!isProjectIndexServerAvailable()) {
12388
- return { status: "unavailable", connected: false };
12389
- }
12390
- return {
12391
- status: "offline",
12392
- connected: false,
12393
- projectRoot,
12394
- indexDir,
12395
- endpoint
12396
- };
12397
- }
12398
- if (latestConnectionState.endpoint) return latestConnectionState;
12399
- if (!isProjectIndexServerAvailable()) return { status: "unavailable", connected: false };
12400
- return latestConnectionState;
12460
+ function yieldEventLoop() {
12461
+ return new Promise((resolve16) => setImmediate(resolve16));
12401
12462
  }
12402
- function onProjectIndexServerConnectionStateChange(listener) {
12403
- connectionStateListeners.add(listener);
12404
- return () => connectionStateListeners.delete(listener);
12463
+ function throwIfAborted(signal) {
12464
+ if (!signal?.aborted) return;
12465
+ if (signal.reason instanceof Error) throw signal.reason;
12466
+ throw new Error(typeof signal.reason === "string" ? signal.reason : "Indexing cancelled");
12405
12467
  }
12406
- function remoteError(message, name) {
12407
- if (name === "LockError") return new LockError(message);
12408
- if (name === "IndexTimeoutError") return new IndexTimeoutError(message);
12409
- const error = new Error(message);
12410
- if (name && name !== "Error") error.name = name;
12411
- return error;
12468
+ function isAbortError(err) {
12469
+ return err instanceof DOMException && err.name === "AbortError";
12412
12470
  }
12413
- function isProjectIndexServerHealth(value) {
12414
- if (!value || typeof value !== "object") return false;
12415
- const health = value;
12416
- const memory = health.memory && typeof health.memory === "object" ? health.memory : void 0;
12417
- const activity = health.activity && typeof health.activity === "object" ? health.activity : void 0;
12418
- 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";
12471
+ var DEFAULT_IGNORE = DEFAULT_WALK_IGNORE_DIRS;
12472
+ var DEFAULT_IGNORE_FILES = /* @__PURE__ */ new Set(["package-lock.json", "pnpm-lock.yaml", "pnpm-lock.yml"]);
12473
+ var INDEXABLE_EXTENSION_SET = new Set(INDEXABLE_EXTENSIONS);
12474
+ var MAX_INDEX_FILE_BYTES = 5 * 1024 * 1024;
12475
+ var MAX_GIT_FILE_LIST_BYTES = 64 * 1024 * 1024;
12476
+ function isWithinProject(projectRoot, file) {
12477
+ const rel = path23.relative(projectRoot, file);
12478
+ return rel !== "" && !rel.startsWith(`..${path23.sep}`) && rel !== ".." && !path23.isAbsolute(rel);
12419
12479
  }
12420
- function delay(ms) {
12421
- return new Promise((resolve16) => {
12422
- const timer = setTimeout(resolve16, ms);
12423
- timer.unref?.();
12424
- });
12480
+ function isMissingPathError(err) {
12481
+ const code = err?.code;
12482
+ return code === "ENOENT" || code === "ENOTDIR";
12425
12483
  }
12426
- function cancellationError(signal) {
12427
- return signal.reason instanceof Error ? signal.reason : new Error("Indexing cancelled");
12484
+ function normalizeComparablePath(value) {
12485
+ const resolved = path23.resolve(value);
12486
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
12428
12487
  }
12429
- var ProjectServerConnection = class {
12430
- constructor(projectRoot, indexDir, endpoint) {
12431
- this.projectRoot = projectRoot;
12432
- this.indexDir = indexDir;
12433
- this.endpoint = endpoint;
12434
- this.transition("offline");
12435
- }
12436
- projectRoot;
12437
- indexDir;
12438
- endpoint;
12439
- socket = null;
12440
- buffer = "";
12441
- info = null;
12442
- activity = null;
12443
- health = null;
12444
- healthCheck = null;
12445
- connecting = null;
12446
- connectResolve = null;
12447
- connectReject = null;
12448
- nextId = 1;
12449
- pending = /* @__PURE__ */ new Map();
12450
- transition(status, options = {}) {
12451
- const previous = connectionStates.get(this.endpoint);
12452
- const pid = options.pid ?? (status === "connected" ? this.info?.pid : void 0);
12453
- 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);
12454
- publishConnectionState(this.endpoint, {
12455
- status,
12456
- connected: status === "connected" || status === "degraded" || status === "unresponsive",
12457
- projectRoot: this.projectRoot,
12458
- indexDir: this.indexDir,
12459
- endpoint: this.endpoint,
12460
- pid,
12461
- lastError,
12462
- ...this.activity ? { activity: this.activity } : {},
12463
- ...this.health ? { health: this.health } : {}
12464
- });
12465
- }
12466
- isConnected() {
12467
- return this.socket !== null && !this.socket.destroyed && this.info !== null;
12468
- }
12469
- async checkHealth(spawnIfMissing = false, timeoutMs = SERVER_HEALTH_TIMEOUT_MS) {
12470
- await this.ensureConnected(spawnIfMissing);
12471
- if (this.healthCheck) return this.healthCheck;
12472
- const startedAt = Date.now();
12473
- this.healthCheck = this.request({ type: "ping" }, { timeoutMs }).then((server) => {
12474
- const now = Date.now();
12475
- this.health = {
12476
- status: "healthy",
12477
- checkedAt: now,
12478
- lastHealthyAt: now,
12479
- latencyMs: Math.max(0, now - startedAt),
12480
- missedHeartbeats: 0,
12481
- ...isProjectIndexServerHealth(server) ? { server } : {}
12482
- };
12483
- this.transition("connected", { pid: this.info?.pid });
12484
- return this.health;
12485
- }).catch((error) => {
12486
- if (!this.isConnected()) throw error;
12487
- if ((this.health?.lastHealthyAt ?? 0) > startedAt) return this.health;
12488
- const missedHeartbeats = (this.health?.missedHeartbeats ?? 0) + 1;
12489
- const status = missedHeartbeats >= 3 ? "unresponsive" : "degraded";
12490
- this.health = {
12491
- status,
12492
- checkedAt: Date.now(),
12493
- lastHealthyAt: this.health?.lastHealthyAt ?? null,
12494
- latencyMs: null,
12495
- missedHeartbeats,
12496
- ...this.health?.server ? { server: this.health.server } : {}
12497
- };
12498
- this.transition(status, { pid: this.info?.pid, error });
12499
- return this.health;
12500
- }).finally(() => {
12501
- this.healthCheck = null;
12502
- });
12503
- return this.healthCheck;
12504
- }
12505
- markResponsive() {
12506
- const now = Date.now();
12507
- this.health = {
12508
- status: "healthy",
12509
- checkedAt: now,
12510
- lastHealthyAt: now,
12511
- latencyMs: this.health?.latencyMs ?? null,
12512
- missedHeartbeats: 0,
12513
- ...this.health?.server ? { server: this.health.server } : {}
12488
+ function gitOutput(projectRoot, args) {
12489
+ return new Promise((resolve16, reject) => {
12490
+ execFile2(
12491
+ "git",
12492
+ ["-C", projectRoot, ...args],
12493
+ {
12494
+ encoding: "buffer",
12495
+ maxBuffer: MAX_GIT_FILE_LIST_BYTES,
12496
+ windowsHide: true
12497
+ },
12498
+ (error, stdout) => {
12499
+ if (error) reject(error);
12500
+ else resolve16(Buffer.isBuffer(stdout) ? stdout : Buffer.from(stdout));
12501
+ }
12502
+ );
12503
+ });
12504
+ }
12505
+ async function findGitSourceFiles(projectRoot, ignore, signal) {
12506
+ try {
12507
+ throwIfAborted(signal);
12508
+ const topLevel = (await gitOutput(projectRoot, ["rev-parse", "--show-toplevel"])).toString("utf8").trim();
12509
+ if (normalizeComparablePath(topLevel) !== normalizeComparablePath(projectRoot)) return null;
12510
+ throwIfAborted(signal);
12511
+ const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
12512
+ const [output, statusOutput] = await Promise.all([
12513
+ gitOutput(projectRoot, ["ls-files", "--cached", "--others", "--exclude-standard", "-z"]),
12514
+ gitOutput(projectRoot, [
12515
+ "status",
12516
+ "--porcelain=v1",
12517
+ "-z",
12518
+ "--untracked-files=all",
12519
+ "--ignored=no"
12520
+ ])
12521
+ ]);
12522
+ throwIfAborted(signal);
12523
+ const dirty = /* @__PURE__ */ new Set();
12524
+ const deleted = /* @__PURE__ */ new Set();
12525
+ const statusRecords = statusOutput.toString("utf8").split("\0");
12526
+ for (let i = 0; i < statusRecords.length; i++) {
12527
+ const record = statusRecords[i];
12528
+ if (!record) continue;
12529
+ const status = record.slice(0, 2);
12530
+ const changedPath = path23.resolve(projectRoot, record.slice(3));
12531
+ dirty.add(changedPath);
12532
+ if (status.includes("D")) deleted.add(changedPath);
12533
+ if (status.includes("R") || status.includes("C")) {
12534
+ const source = statusRecords[++i];
12535
+ if (source) dirty.add(path23.resolve(projectRoot, source));
12536
+ }
12537
+ }
12538
+ const files = [];
12539
+ for (const relative12 of output.toString("utf8").split("\0")) {
12540
+ if (!relative12) continue;
12541
+ const portable = relative12.replace(/\\/g, "/");
12542
+ if (portable.split("/").some((segment) => ignoreSet.has(segment)) || DEFAULT_IGNORE_FILES.has(path23.posix.basename(portable))) {
12543
+ continue;
12544
+ }
12545
+ const full = path23.resolve(projectRoot, relative12);
12546
+ if (deleted.has(full)) continue;
12547
+ const ext = path23.extname(relative12).toLowerCase();
12548
+ if (INDEXABLE_EXTENSION_SET.has(ext) || detectLang(full) !== null) files.push(full);
12549
+ }
12550
+ return {
12551
+ files,
12552
+ trustedUnchanged: new Set(files.filter((file) => !dirty.has(file)))
12514
12553
  };
12554
+ } catch {
12555
+ return null;
12515
12556
  }
12516
- async call(op, args, options) {
12517
- if (options.signal?.aborted) throw cancellationError(options.signal);
12518
- await this.ensureConnected(true);
12519
- if (options.signal?.aborted) throw cancellationError(options.signal);
12520
- return this.request({ type: "request", op, args }, options);
12557
+ }
12558
+ async function findSourceFiles(projectRoot, ignore, isGitIgnored, signal) {
12559
+ const gitFiles = await findGitSourceFiles(projectRoot, ignore, signal);
12560
+ if (gitFiles) {
12561
+ return {
12562
+ files: gitFiles.files,
12563
+ complete: true,
12564
+ errors: [],
12565
+ trustedUnchanged: gitFiles.trustedUnchanged
12566
+ };
12521
12567
  }
12522
- async shutdownRemote(reason) {
12523
- try {
12524
- await this.ensureConnected(false);
12525
- } catch {
12526
- return { stopped: false, reason: "not-running" };
12568
+ const results = [];
12569
+ const errors = [];
12570
+ let complete = true;
12571
+ const ignoreSet = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...ignore]);
12572
+ const indexableExts = new Set(INDEXABLE_EXTENSIONS);
12573
+ let dirCount = 0;
12574
+ const walk = async (dir) => {
12575
+ throwIfAborted(signal);
12576
+ if (dirCount > 0 && dirCount % YIELD_EVERY_N === 0) {
12577
+ await yieldEventLoop();
12578
+ throwIfAborted(signal);
12527
12579
  }
12528
- const pid = this.info?.pid;
12580
+ let entries;
12529
12581
  try {
12530
- this.transition("stopping", { pid });
12531
- await this.request(
12532
- { type: "shutdown", reason },
12533
- { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
12534
- );
12535
- return { stopped: true, pid };
12536
- } catch (error) {
12537
- const forceKilled = this.forceKillKnownServer();
12538
- return {
12539
- stopped: forceKilled,
12540
- pid,
12541
- reason: forceKilled ? `force-killed after graceful shutdown failed: ${error instanceof Error ? error.message : String(error)}` : error instanceof Error ? error.message : String(error)
12542
- };
12543
- } finally {
12544
- this.close();
12582
+ entries = await fs17.readdir(dir, { withFileTypes: true });
12583
+ } catch (err) {
12584
+ complete = false;
12585
+ errors.push(`scan error: ${dir}: ${err instanceof Error ? err.message : String(err)}`);
12586
+ return;
12545
12587
  }
12546
- }
12547
- async configure(watchExternal, debounceMs) {
12548
- await this.ensureConnected(true);
12549
- const startedAt = Date.now();
12550
- const result = await this.request(
12551
- { type: "configure", watchExternal, debounceMs },
12552
- { timeoutMs: SERVER_CONTROL_TIMEOUT_MS }
12553
- );
12554
- if (isProjectIndexServerHealth(result.health)) {
12555
- const now = Date.now();
12556
- this.health = {
12557
- status: "healthy",
12558
- checkedAt: now,
12559
- lastHealthyAt: now,
12560
- latencyMs: Math.max(0, now - startedAt),
12561
- missedHeartbeats: 0,
12562
- server: result.health
12563
- };
12564
- this.transition("connected", { pid: this.info?.pid });
12588
+ dirCount++;
12589
+ for (const e of entries) {
12590
+ if (ignoreSet.has(e.name)) continue;
12591
+ const full = path23.join(dir, e.name);
12592
+ const rel = path23.relative(projectRoot, full).replace(/\\/g, "/");
12593
+ if (e.isDirectory()) {
12594
+ if (isGitIgnored(rel, true)) continue;
12595
+ await walk(full);
12596
+ } else if (e.isFile()) {
12597
+ if (DEFAULT_IGNORE_FILES.has(e.name) || isGitIgnored(rel, false)) continue;
12598
+ const ext = path23.extname(e.name).toLowerCase();
12599
+ if (indexableExts.has(ext) || detectLang(full) !== null) {
12600
+ results.push(full);
12601
+ }
12602
+ }
12603
+ }
12604
+ };
12605
+ await walk(projectRoot);
12606
+ return { files: results, complete, errors };
12607
+ }
12608
+ function assignRefsToSymbols2(refs, symbols) {
12609
+ if (refs.length === 0 || symbols.length === 0) return [];
12610
+ const ordered = [...symbols].sort((a, b) => a.line - b.line || a.col - b.col || a.id - b.id);
12611
+ const seen = /* @__PURE__ */ new Set();
12612
+ const assigned = [];
12613
+ for (const ref of refs) {
12614
+ let owner2;
12615
+ for (const symbol of ordered) {
12616
+ if (symbol.line > ref.line) break;
12617
+ owner2 = symbol;
12565
12618
  }
12619
+ if (!owner2 && ref.callType === "import") owner2 = ordered[0];
12620
+ if (!owner2 || owner2.id <= 0) continue;
12621
+ const key = `${owner2.id}:${ref.toName}:${ref.callType}`;
12622
+ if (seen.has(key)) continue;
12623
+ seen.add(key);
12624
+ assigned.push({ ...ref, fromId: owner2.id });
12566
12625
  }
12567
- close() {
12568
- const socket = this.socket;
12569
- this.socket = null;
12570
- this.info = null;
12571
- this.activity = null;
12572
- this.health = null;
12573
- this.connectReject?.(new Error("codebase-index client disconnected"));
12574
- this.connectResolve = null;
12575
- this.connectReject = null;
12576
- if (socket && !socket.destroyed) socket.destroy();
12577
- this.rejectPending(new Error("codebase-index client disconnected"));
12578
- this.transition("offline");
12579
- maybeStopHeartbeatLoop();
12626
+ return assigned;
12627
+ }
12628
+ async function runIndexerWithStore(store, opts) {
12629
+ const { projectRoot, langs, ignore = [], signal } = opts;
12630
+ const relationGraphVersion = "2";
12631
+ const refResolutionVersion = "2";
12632
+ const force = (opts.force ?? false) || store.getMetadata("relation_graph_version") !== relationGraphVersion;
12633
+ const needsFullRefResolution = force || store.getMetadata("ref_resolution_version") !== refResolutionVersion;
12634
+ const startMs = Date.now();
12635
+ const errors = [];
12636
+ const langStats = {};
12637
+ let filesIndexed = 0;
12638
+ let symbolsIndexed = 0;
12639
+ const isGitIgnored = await loadGitignoreMatcher(projectRoot);
12640
+ let files;
12641
+ let discoveredFiles = null;
12642
+ let discoveryComplete = true;
12643
+ let trustedUnchanged;
12644
+ if (opts.files && opts.files.length > 0) {
12645
+ files = opts.files.map((f) => path23.resolve(projectRoot, f)).filter((f) => {
12646
+ if (!isWithinProject(projectRoot, f)) return false;
12647
+ const rel = path23.relative(projectRoot, f).replace(/\\/g, "/");
12648
+ return !rel.split("/").some((seg) => DEFAULT_IGNORE.includes(seg)) && !DEFAULT_IGNORE_FILES.has(path23.basename(f)) && !isGitIgnored(rel, false);
12649
+ });
12650
+ } else {
12651
+ const discovery = await findSourceFiles(projectRoot, ignore, isGitIgnored, signal);
12652
+ files = discovery.files;
12653
+ errors.push(...discovery.errors);
12654
+ discoveryComplete = discovery.complete;
12655
+ discoveredFiles = new Set(files);
12656
+ trustedUnchanged = discovery.trustedUnchanged;
12580
12657
  }
12581
- request(message, options) {
12582
- const socket = this.socket;
12583
- if (!socket || socket.destroyed) {
12584
- return Promise.reject(new Error("codebase-index server connection is not available"));
12585
- }
12586
- const id = this.nextId++;
12587
- return new Promise((resolve16, reject) => {
12588
- const timer = setTimeout(() => {
12589
- const entry = this.pending.get(id);
12590
- if (!entry) return;
12591
- this.pending.delete(id);
12592
- this.write({ type: "cancel", id });
12593
- const error = new IndexTimeoutError(
12594
- `Index ${message.type === "request" ? message.op : message.type} exceeded its ${options.timeoutMs}ms watchdog timeout`
12595
- );
12596
- this.cleanupPending(entry);
12597
- entry.reject(error);
12598
- }, options.timeoutMs);
12599
- timer.unref?.();
12600
- const signal = options.signal;
12601
- const onAbort = signal ? () => {
12602
- const entry = this.pending.get(id);
12603
- if (!entry) return;
12604
- this.pending.delete(id);
12605
- this.write({ type: "cancel", id });
12606
- this.cleanupPending(entry);
12607
- entry.reject(cancellationError(signal));
12608
- } : void 0;
12609
- this.pending.set(id, {
12610
- resolve: resolve16,
12611
- reject,
12612
- timer,
12613
- signal,
12614
- onAbort,
12615
- onProgress: options.onProgress
12616
- });
12617
- if (signal && onAbort) {
12618
- signal.addEventListener("abort", onAbort, { once: true });
12619
- if (signal.aborted) {
12620
- onAbort();
12621
- return;
12622
- }
12623
- }
12624
- this.write({ ...message, id });
12658
+ if (langs && langs.length > 0) {
12659
+ const langSet = new Set(langs);
12660
+ files = files.filter((f) => {
12661
+ const lang = detectLang(f);
12662
+ return lang ? langSet.has(lang) : false;
12625
12663
  });
12626
12664
  }
12627
- async ensureConnected(spawnIfMissing) {
12628
- if (this.socket && !this.socket.destroyed && this.info) return;
12629
- if (this.connecting) return this.connecting;
12630
- this.transition("connecting");
12631
- this.connecting = this.connectWithElection(spawnIfMissing).catch((error) => {
12632
- this.transition("error", { error });
12633
- throw error;
12634
- }).finally(() => {
12635
- this.connecting = null;
12665
+ if (force) store.clearAll();
12666
+ const existingMeta = /* @__PURE__ */ new Map();
12667
+ if (!force) {
12668
+ for (const meta of store.getAllFileMetas()) existingMeta.set(meta.file, meta);
12669
+ }
12670
+ const totalFilesForProgress = files.length;
12671
+ let filesPreSkipped = 0;
12672
+ if (!force && trustedUnchanged) {
12673
+ files = files.filter((file) => {
12674
+ const meta = existingMeta.get(file);
12675
+ if (!meta || !trustedUnchanged.has(file)) return true;
12676
+ langStats[meta.lang] = (langStats[meta.lang] ?? 0) + meta.symbolCount;
12677
+ symbolsIndexed += meta.symbolCount;
12678
+ filesIndexed++;
12679
+ filesPreSkipped++;
12680
+ return false;
12636
12681
  });
12637
- return this.connecting;
12682
+ if (filesPreSkipped > 0) opts.onProgress?.(filesPreSkipped, totalFilesForProgress);
12638
12683
  }
12639
- async connectWithElection(spawnIfMissing) {
12640
- const deadline = Date.now() + (spawnIfMissing ? SERVER_START_TIMEOUT_MS : CONNECT_ATTEMPT_TIMEOUT_MS);
12641
- let spawned = false;
12642
- let staleAttempts = 0;
12643
- let lastError = new Error("codebase-index server unavailable");
12644
- while (Date.now() < deadline) {
12645
- try {
12646
- await this.connectOnce();
12647
- return;
12648
- } catch (error) {
12649
- lastError = error;
12650
- if (error instanceof StaleProjectIndexServerError) {
12651
- staleAttempts++;
12652
- if (!spawnIfMissing) break;
12653
- if (staleAttempts >= 3) this.forceKillServer(error.pid);
12654
- spawned = false;
12655
- await delay(100);
12656
- continue;
12684
+ const parallelBatch = resolveParallelBatch();
12685
+ let filesSinceLastYield = 0;
12686
+ for (let batchStart = 0; batchStart < files.length; batchStart += parallelBatch) {
12687
+ const batchEnd = Math.min(batchStart + parallelBatch, files.length);
12688
+ const batchFiles = files.slice(batchStart, batchEnd);
12689
+ opts.onProgress?.(filesPreSkipped + batchEnd, totalFilesForProgress);
12690
+ filesSinceLastYield += batchFiles.length;
12691
+ if (filesSinceLastYield >= YIELD_EVERY_N) {
12692
+ filesSinceLastYield = 0;
12693
+ await yieldEventLoop();
12694
+ if (isFrugalPerf()) {
12695
+ await new Promise((r) => setTimeout(r, 8));
12696
+ }
12697
+ throwIfAborted(signal);
12698
+ }
12699
+ const statOpts = signal ? { signal } : {};
12700
+ const statReadParse = await Promise.allSettled(
12701
+ batchFiles.map(
12702
+ async (file) => {
12703
+ let stat17;
12704
+ try {
12705
+ stat17 = await fs17.stat(file, statOpts);
12706
+ } catch (e) {
12707
+ if (isAbortError(e)) throw e;
12708
+ return {
12709
+ file,
12710
+ stat: null,
12711
+ lang: "",
12712
+ parsed: null,
12713
+ error: `stat error: ${e instanceof Error ? e.message : String(e)}`,
12714
+ missing: isMissingPathError(e)
12715
+ };
12716
+ }
12717
+ if (!stat17.isFile()) return { file, stat: stat17, lang: "", parsed: null };
12718
+ const lang = detectLang(file);
12719
+ if (!lang) return { file, stat: stat17, lang: "", parsed: null };
12720
+ if (stat17.size > MAX_INDEX_FILE_BYTES) {
12721
+ return {
12722
+ file,
12723
+ stat: stat17,
12724
+ lang,
12725
+ parsed: null,
12726
+ error: `file too large (${stat17.size} bytes; max ${MAX_INDEX_FILE_BYTES})`
12727
+ };
12728
+ }
12729
+ const meta = existingMeta.get(file);
12730
+ if (!force && meta && meta.mtimeMs === Math.floor(stat17.mtimeMs)) {
12731
+ return { file, stat: stat17, lang, parsed: null, skippedMeta: meta };
12732
+ }
12733
+ let content;
12734
+ try {
12735
+ content = await fs17.readFile(file, { encoding: "utf8", signal });
12736
+ } catch (e) {
12737
+ if (isAbortError(e)) throw e;
12738
+ return {
12739
+ file,
12740
+ stat: stat17,
12741
+ lang,
12742
+ parsed: null,
12743
+ error: `read error: ${e instanceof Error ? e.message : String(e)}`
12744
+ };
12745
+ }
12746
+ let parsed;
12747
+ try {
12748
+ parsed = await parseFileContent(file, content, lang);
12749
+ } catch (e) {
12750
+ return {
12751
+ file,
12752
+ stat: stat17,
12753
+ lang,
12754
+ parsed: null,
12755
+ error: `parse error: ${e instanceof Error ? e.message : String(e)}`
12756
+ };
12757
+ }
12758
+ return { file, stat: stat17, lang, parsed, content };
12657
12759
  }
12760
+ )
12761
+ );
12762
+ const batchEntries = [];
12763
+ const deleteForFiles = [];
12764
+ for (let fi = 0; fi < statReadParse.length; fi++) {
12765
+ const settled = statReadParse[fi];
12766
+ const file = expectDefined6(batchFiles[fi]);
12767
+ if (settled.status === "rejected") {
12768
+ const err = settled.reason;
12769
+ if (err instanceof Error && isAbortError(err)) throw err;
12770
+ errors.push(`batch error: ${file}: ${err instanceof Error ? err.message : String(err)}`);
12771
+ continue;
12658
12772
  }
12659
- if (!spawnIfMissing) break;
12660
- if (!spawned) {
12661
- this.spawnDetachedServer();
12662
- spawned = true;
12773
+ const result = settled.value;
12774
+ if (result.error) {
12775
+ if (result.missing) store.deleteFile(file);
12776
+ errors.push(`${file}: ${result.error}`);
12777
+ continue;
12663
12778
  }
12664
- await delay(75);
12665
- }
12666
- throw lastError;
12667
- }
12668
- connectOnce() {
12669
- this.socket?.destroy();
12670
- this.socket = null;
12671
- this.info = null;
12672
- this.activity = null;
12673
- this.health = null;
12674
- this.buffer = "";
12675
- return new Promise((resolve16, reject) => {
12676
- const socket = net3.createConnection(this.endpoint);
12677
- this.socket = socket;
12678
- socket.setEncoding("utf8");
12679
- const timer = setTimeout(() => {
12680
- reject(new Error("codebase-index server handshake timed out"));
12681
- socket.destroy();
12682
- }, CONNECT_ATTEMPT_TIMEOUT_MS);
12683
- timer.unref?.();
12684
- const finishResolve = () => {
12685
- clearTimeout(timer);
12686
- this.connectResolve = null;
12687
- this.connectReject = null;
12688
- resolve16();
12689
- };
12690
- const finishReject = (error) => {
12691
- clearTimeout(timer);
12692
- this.connectResolve = null;
12693
- this.connectReject = null;
12694
- reject(error);
12695
- };
12696
- this.connectResolve = finishResolve;
12697
- this.connectReject = finishReject;
12698
- socket.on("data", (chunk) => this.onData(socket, chunk));
12699
- socket.on("error", (error) => {
12700
- if (!this.info) finishReject(error);
12779
+ const { stat: stat17, lang, parsed } = result;
12780
+ if (result.skippedMeta) {
12781
+ langStats[lang] = (langStats[lang] ?? 0) + result.skippedMeta.symbolCount;
12782
+ symbolsIndexed += result.skippedMeta.symbolCount;
12783
+ filesIndexed++;
12784
+ continue;
12785
+ }
12786
+ if (!lang || !parsed) {
12787
+ if (lang) {
12788
+ store.upsertFile({
12789
+ file,
12790
+ lang,
12791
+ mtimeMs: Math.floor(stat17.mtimeMs),
12792
+ symbolCount: 0,
12793
+ lastIndexed: Date.now()
12794
+ });
12795
+ filesIndexed++;
12796
+ }
12797
+ continue;
12798
+ }
12799
+ if (parsed.symbols.length === 0) {
12800
+ store.replaceEmptyFile({
12801
+ file,
12802
+ lang,
12803
+ mtimeMs: Math.floor(stat17.mtimeMs),
12804
+ symbolCount: 0,
12805
+ lastIndexed: Date.now()
12806
+ });
12807
+ filesIndexed++;
12808
+ continue;
12809
+ }
12810
+ batchEntries.push({
12811
+ file,
12812
+ lang,
12813
+ symbols: parsed.symbols,
12814
+ refs: parsed.refs ?? [],
12815
+ mtimeMs: Math.floor(stat17.mtimeMs),
12816
+ symbolCount: parsed.symbols.length
12701
12817
  });
12702
- socket.on("close", () => this.onClose(socket));
12703
- });
12704
- }
12705
- onData(socket, chunk) {
12706
- if (socket !== this.socket) return;
12707
- this.buffer += chunk;
12708
- while (true) {
12709
- const newline = this.buffer.indexOf("\n");
12710
- if (newline < 0) {
12711
- if (this.buffer.length > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12712
- socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
12818
+ deleteForFiles.push(file);
12819
+ }
12820
+ if (batchEntries.length > 0) {
12821
+ try {
12822
+ store.commitBatch(batchEntries, { deleteForFiles });
12823
+ for (const entry of batchEntries) {
12824
+ const count = entry.symbols.length;
12825
+ symbolsIndexed += count;
12826
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + count;
12827
+ filesIndexed++;
12828
+ }
12829
+ } catch (err) {
12830
+ const message = err instanceof Error ? err.message : String(err);
12831
+ errors.push(`commitBatch failed: ${message} \u2014 falling back to per-file writes`);
12832
+ for (const entry of batchEntries) {
12833
+ try {
12834
+ store.deleteRefsForFile(entry.file);
12835
+ store.deleteSymbolsForFile(entry.file);
12836
+ const symbolsWithIds = store.insertSymbols(entry.symbols);
12837
+ symbolsIndexed += symbolsWithIds.length;
12838
+ langStats[entry.lang] = (langStats[entry.lang] ?? 0) + symbolsWithIds.length;
12839
+ filesIndexed++;
12840
+ if (entry.refs.length > 0 && symbolsWithIds.length > 0) {
12841
+ const fallbackBatch = assignRefsToSymbols2(entry.refs, symbolsWithIds);
12842
+ if (fallbackBatch.length > 0) store.insertRefsBatch(fallbackBatch);
12843
+ }
12844
+ store.resolveRefsForNames([
12845
+ ...entry.symbols.map((symbol) => symbol.name),
12846
+ ...entry.refs.map((ref) => ref.toName)
12847
+ ]);
12848
+ store.upsertFile({
12849
+ file: entry.file,
12850
+ lang: entry.lang,
12851
+ mtimeMs: entry.mtimeMs,
12852
+ symbolCount: entry.symbolCount,
12853
+ lastIndexed: Date.now()
12854
+ });
12855
+ } catch (innerErr) {
12856
+ errors.push(
12857
+ `fallback write failed: ${entry.file}: ${innerErr instanceof Error ? innerErr.message : String(innerErr)}`
12858
+ );
12859
+ }
12713
12860
  }
12714
- return;
12715
- }
12716
- if (newline > PROJECT_INDEX_SERVER_MAX_FRAME_CHARS) {
12717
- socket.destroy(new Error("codebase-index server response exceeds the IPC limit"));
12718
- return;
12719
- }
12720
- const line = this.buffer.slice(0, newline);
12721
- this.buffer = this.buffer.slice(newline + 1);
12722
- if (!line) continue;
12723
- let message;
12724
- try {
12725
- message = JSON.parse(line);
12726
- } catch {
12727
- socket.destroy(new Error("invalid codebase-index server response"));
12728
- return;
12729
12861
  }
12730
- this.onMessage(message);
12731
12862
  }
12732
12863
  }
12733
- onMessage(message) {
12734
- if (message.type === "hello") {
12735
- if (message.protocolVersion !== PROJECT_INDEX_SERVER_PROTOCOL_VERSION) {
12736
- this.rejectStaleServer(
12737
- message,
12738
- `codebase-index protocol mismatch: client=${PROJECT_INDEX_SERVER_PROTOCOL_VERSION}, server=${message.protocolVersion}`
12739
- );
12740
- return;
12741
- }
12742
- const expectedBuildId = projectIndexServerExpectedBuildId();
12743
- if (expectedBuildId && message.buildId !== expectedBuildId) {
12744
- this.rejectStaleServer(
12745
- message,
12746
- `codebase-index build mismatch: client=${expectedBuildId}, server=${message.buildId ?? "legacy"}`
12747
- );
12748
- return;
12864
+ if (discoveredFiles && discoveryComplete) {
12865
+ for (const [file_] of existingMeta) {
12866
+ if (!discoveredFiles.has(file_)) {
12867
+ store.deleteFile(file_);
12749
12868
  }
12750
- this.info = message;
12751
- this.markResponsive();
12752
- this.transition("connected", { pid: message.pid });
12753
- ensureHeartbeatLoop();
12754
- this.connectResolve?.();
12755
- return;
12756
- }
12757
- if (message.type === "index-state") {
12758
- this.activity = message.state;
12759
- this.markResponsive();
12760
- this.transition("connected", { pid: this.info?.pid });
12761
- return;
12762
- }
12763
- const entry = this.pending.get(message.id);
12764
- if (!entry) return;
12765
- this.markResponsive();
12766
- const status = connectionStates.get(this.endpoint)?.status;
12767
- if (status === "degraded" || status === "unresponsive") {
12768
- this.transition("connected", { pid: this.info?.pid });
12769
- }
12770
- if (message.type === "progress") {
12771
- entry.onProgress?.(message.current, message.total);
12772
- return;
12773
- }
12774
- this.pending.delete(message.id);
12775
- this.cleanupPending(entry);
12776
- if (message.ok) entry.resolve(message.result);
12777
- else entry.reject(remoteError(message.error, message.errorName));
12778
- }
12779
- onClose(socket) {
12780
- if (socket !== this.socket) return;
12781
- const wasConnected = this.info !== null;
12782
- this.socket = null;
12783
- this.info = null;
12784
- this.activity = null;
12785
- this.health = null;
12786
- const error = new Error("codebase-index server connection closed");
12787
- this.connectReject?.(error);
12788
- this.connectResolve = null;
12789
- this.connectReject = null;
12790
- this.rejectPending(error);
12791
- if (wasConnected) this.transition("error", { error });
12792
- maybeStopHeartbeatLoop();
12793
- }
12794
- cleanupPending(entry) {
12795
- clearTimeout(entry.timer);
12796
- if (entry.signal && entry.onAbort) {
12797
- entry.signal.removeEventListener("abort", entry.onAbort);
12798
- }
12799
- }
12800
- rejectPending(error) {
12801
- const entries = [...this.pending.values()];
12802
- this.pending.clear();
12803
- for (const entry of entries) {
12804
- this.cleanupPending(entry);
12805
- entry.reject(error);
12806
- }
12807
- }
12808
- write(message) {
12809
- const socket = this.socket;
12810
- if (socket && !socket.destroyed) socket.write(encodeProjectServerMessage(message));
12811
- }
12812
- rejectStaleServer(message, reason) {
12813
- const socket = this.socket;
12814
- if (socket && !socket.destroyed) {
12815
- socket.write(
12816
- encodeProjectServerMessage({
12817
- type: "shutdown",
12818
- id: 0,
12819
- reason: "stale-build-replacement"
12820
- })
12821
- );
12822
- const timer = setTimeout(() => socket.destroy(), 25);
12823
- timer.unref?.();
12824
12869
  }
12825
- this.connectReject?.(new StaleProjectIndexServerError(reason, message.pid));
12826
12870
  }
12827
- spawnDetachedServer() {
12828
- const url = resolveProjectServerUrl();
12829
- if (!url) throw new Error("built codebase-index project server is unavailable");
12830
- if (process.platform !== "win32") {
12831
- try {
12832
- fs17.rmSync(this.endpoint, { force: true });
12833
- } catch {
12834
- }
12835
- }
12836
- const args = [fileURLToPath2(url), "--project-root", this.projectRoot];
12837
- if (this.indexDir) args.push("--index-dir", this.indexDir);
12838
- const child = spawn7(process.execPath, args, {
12839
- detached: true,
12840
- stdio: "ignore",
12841
- windowsHide: true,
12842
- env: process.env
12871
+ if (needsFullRefResolution) store.resolveRefs();
12872
+ store.setMetadata("ref_resolution_version", refResolutionVersion);
12873
+ store.setMetadata("relation_graph_version", relationGraphVersion);
12874
+ if (!opts.files || filesIndexed >= 50) store.optimize();
12875
+ store.setLastIndexed(Date.now());
12876
+ if (!opts.files) store.compactIfNeeded();
12877
+ const durationMs = Date.now() - startMs;
12878
+ return {
12879
+ filesIndexed,
12880
+ symbolsIndexed,
12881
+ langStats,
12882
+ durationMs,
12883
+ errors
12884
+ };
12885
+ }
12886
+
12887
+ // src/codebase-index/index-service.ts
12888
+ async function indexService(args, hooks = {}) {
12889
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12890
+ try {
12891
+ return await runIndexerWithStore(store, {
12892
+ projectRoot: args.projectRoot,
12893
+ indexDir: args.indexDir,
12894
+ files: args.files,
12895
+ force: args.force,
12896
+ langs: args.langs,
12897
+ ignore: args.ignore,
12898
+ signal: hooks.signal,
12899
+ onProgress: hooks.onProgress
12843
12900
  });
12844
- child.unref();
12901
+ } finally {
12902
+ indexStorePool.release(store);
12845
12903
  }
12846
- forceKillKnownServer() {
12847
- const pid = this.info?.pid;
12848
- return pid ? this.forceKillServer(pid) : false;
12904
+ }
12905
+ function searchService(args) {
12906
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12907
+ try {
12908
+ return store.searchRanked(
12909
+ args.query,
12910
+ {
12911
+ kind: args.kind,
12912
+ lang: args.lang,
12913
+ file: args.file,
12914
+ lspKind: args.lspKind
12915
+ },
12916
+ args.limit
12917
+ );
12918
+ } finally {
12919
+ indexStorePool.release(store);
12849
12920
  }
12850
- forceKillServer(pid) {
12851
- if (pid === process.pid) return false;
12852
- try {
12853
- process.kill(pid);
12854
- const metadataPath = projectIndexServerMetadataPath(this.projectRoot, this.indexDir);
12855
- try {
12856
- const metadata = JSON.parse(fs17.readFileSync(metadataPath, "utf8"));
12857
- if (metadata.pid === pid) fs17.rmSync(metadataPath, { force: true });
12858
- } catch {
12859
- }
12860
- return true;
12861
- } catch {
12862
- return false;
12863
- }
12921
+ }
12922
+ function statsService(args) {
12923
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12924
+ try {
12925
+ return store.getStats();
12926
+ } finally {
12927
+ indexStorePool.release(store);
12864
12928
  }
12865
- };
12866
- var connections = /* @__PURE__ */ new Map();
12867
- var heartbeatTimer;
12868
- function ensureHeartbeatLoop() {
12869
- if (heartbeatTimer) return;
12870
- heartbeatTimer = setInterval(() => {
12871
- for (const connection of connections.values()) {
12872
- if (connection.isConnected()) void connection.checkHealth(false).catch(() => {
12873
- });
12874
- }
12875
- }, SERVER_HEARTBEAT_INTERVAL_MS);
12876
- heartbeatTimer.unref?.();
12877
12929
  }
12878
- function maybeStopHeartbeatLoop() {
12879
- if (!heartbeatTimer) return;
12880
- if ([...connections.values()].some((connection) => connection.isConnected())) return;
12881
- clearInterval(heartbeatTimer);
12882
- heartbeatTimer = void 0;
12930
+ function packageGraphService(args) {
12931
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12932
+ try {
12933
+ return store.getPackageGraph();
12934
+ } finally {
12935
+ indexStorePool.release(store);
12936
+ }
12883
12937
  }
12884
- function connectionFor(projectRoot, indexDir) {
12885
- const endpoint = projectIndexServerEndpoint(projectRoot, indexDir);
12886
- let connection = connections.get(endpoint);
12887
- if (!connection) {
12888
- connection = new ProjectServerConnection(projectRoot, indexDir, endpoint);
12889
- connections.set(endpoint, connection);
12938
+ function fileGraphService(args) {
12939
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12940
+ try {
12941
+ return store.getFileGraph(args.packageFilter);
12942
+ } finally {
12943
+ indexStorePool.release(store);
12890
12944
  }
12891
- return connection;
12892
12945
  }
12893
- function callProjectIndexServer(op, args, options) {
12894
- return connectionFor(args.projectRoot, args.indexDir).call(op, args, options);
12946
+ function symbolGraphService(args) {
12947
+ const store = indexStorePool.acquire(args.projectRoot, { indexDir: args.indexDir });
12948
+ try {
12949
+ return store.getSymbolGraph(args.fileFilter);
12950
+ } finally {
12951
+ indexStorePool.release(store);
12952
+ }
12895
12953
  }
12896
12954
 
12897
12955
  // src/codebase-index/background-indexer.ts
@@ -13002,8 +13060,17 @@ function terminateWorker(reason) {
13002
13060
  if (w) void w.terminate().catch(() => {
13003
13061
  });
13004
13062
  }
13063
+ var warnedInvalidEndpoints = /* @__PURE__ */ new Set();
13064
+ function warnEndpointInvalidOnce(availability) {
13065
+ if (warnedInvalidEndpoints.has(availability.endpoint)) return;
13066
+ warnedInvalidEndpoints.add(availability.endpoint);
13067
+ process.stderr.write(
13068
+ `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.
13069
+ `
13070
+ );
13071
+ }
13005
13072
  function callIndexOp(op, args, opts) {
13006
- const availability = resolveProjectIndexDaemonAvailability();
13073
+ const availability = resolveProjectIndexDaemonAvailability(args.projectRoot, args.indexDir);
13007
13074
  if (availability.kind === "available") {
13008
13075
  return callProjectIndexServer(op, args, opts);
13009
13076
  }
@@ -13014,6 +13081,14 @@ function callIndexOp(op, args, opts) {
13014
13081
  )
13015
13082
  );
13016
13083
  }
13084
+ if (availability.kind === "endpoint-invalid") {
13085
+ warnEndpointInvalidOnce(availability);
13086
+ return Promise.reject(
13087
+ new Error(
13088
+ `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.`
13089
+ )
13090
+ );
13091
+ }
13017
13092
  const w = ensureWorker();
13018
13093
  if (!w) return callInline(op, args, opts);
13019
13094
  if (opts.signal?.aborted) {