@lzehrung/codegraph 2.3.20 → 2.3.21

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 (46) hide show
  1. package/codegraph-skill/codegraph/SKILL.md +1 -0
  2. package/dist/agent/session.d.ts +2 -2
  3. package/dist/agent/session.d.ts.map +1 -1
  4. package/dist/agent/session.js +13 -9
  5. package/dist/agent/session.js.map +1 -1
  6. package/dist/bin/cli.js +36335 -36073
  7. package/dist/config.d.ts +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +0 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/indexer/build-cache/manifest.d.ts +2 -2
  12. package/dist/indexer/build-cache/manifest.d.ts.map +1 -1
  13. package/dist/indexer/build-cache/manifest.js +8 -33
  14. package/dist/indexer/build-cache/manifest.js.map +1 -1
  15. package/dist/indexer/build-index.d.ts.map +1 -1
  16. package/dist/indexer/build-index.js +33 -10
  17. package/dist/indexer/build-index.js.map +1 -1
  18. package/dist/indexer/build-manifest.d.ts +3 -0
  19. package/dist/indexer/build-manifest.d.ts.map +1 -1
  20. package/dist/indexer/build-manifest.js +1 -1
  21. package/dist/indexer/build-manifest.js.map +1 -1
  22. package/dist/indexer/finalize.d.ts +2 -1
  23. package/dist/indexer/finalize.d.ts.map +1 -1
  24. package/dist/indexer/finalize.js +1 -0
  25. package/dist/indexer/finalize.js.map +1 -1
  26. package/dist/indexer/incremental-plan.d.ts +3 -2
  27. package/dist/indexer/incremental-plan.d.ts.map +1 -1
  28. package/dist/indexer/incremental-plan.js +8 -4
  29. package/dist/indexer/incremental-plan.js.map +1 -1
  30. package/dist/indexer/types.d.ts +3 -1
  31. package/dist/indexer/types.d.ts.map +1 -1
  32. package/dist/lifecycle/manifest.d.ts.map +1 -1
  33. package/dist/lifecycle/manifest.js +6 -4
  34. package/dist/lifecycle/manifest.js.map +1 -1
  35. package/dist/util/git.d.ts +13 -10
  36. package/dist/util/git.d.ts.map +1 -1
  37. package/dist/util/git.js +29 -25
  38. package/dist/util/git.js.map +1 -1
  39. package/dist/util/projectFiles/definitions.d.ts +19 -0
  40. package/dist/util/projectFiles/definitions.d.ts.map +1 -1
  41. package/dist/util/projectFiles/definitions.js.map +1 -1
  42. package/dist/util/projectFiles.d.ts +72 -21
  43. package/dist/util/projectFiles.d.ts.map +1 -1
  44. package/dist/util/projectFiles.js +279 -30
  45. package/dist/util/projectFiles.js.map +1 -1
  46. package/package.json +3 -3
@@ -12,7 +12,8 @@ export { isRelativePathInside, matchesDiscoveryGlob } from "./discoveryPath.js";
12
12
  import { PROJECT_FILE_DEFINITIONS, } from "./projectFiles/definitions.js";
13
13
  import { trimToNull } from "./projectFiles/parsers.js";
14
14
  import { mapLimitSemaphore } from "./concurrency.js";
15
- import { getGitRepositoryRoot, isGitProjectRootIgnored, isGitRepo, isGitTimeoutError, listGitExcludeFiles, listGitStageSpecialPaths, listTrackedFiles, listUntrackedFiles, } from "./git.js";
15
+ import { CODEGRAPH_CONFIG_FILE } from "../config.js";
16
+ import { createGitDiscoveryCache, getGitRepositoryRoot, isGitProjectRootIgnored, isGitRepo, isGitTimeoutError, listGitExcludeFiles, listGitStageSpecialPaths, listGitSubmoduleDirectories, listTrackedFiles, listUntrackedFiles, } from "./git.js";
16
17
  export const DEFAULT_PROJECT_FILE_IGNORES = [
17
18
  "**/node_modules/**",
18
19
  "**/.git/**",
@@ -100,6 +101,9 @@ export const DEFAULT_PROJECT_PATTERNS = [
100
101
  ...DEFAULT_PROJECT_MANIFESTS.map((name) => `**/${name}`),
101
102
  ];
102
103
  const REALPATH_FILTER_CONCURRENCY = 64;
104
+ export function createProjectDiscoveryContext(projectRoot) {
105
+ return new DiscoveryContext(projectRoot);
106
+ }
103
107
  function emitDiscoveryActivity(report, activity, current = 0, total = 0) {
104
108
  report?.({ activity, current, total });
105
109
  }
@@ -121,6 +125,35 @@ function isPresent(value) {
121
125
  function normalizeGlobPattern(globPattern) {
122
126
  return globPattern.trim().replace(/\\/g, "/");
123
127
  }
128
+ const ROOT_CONFIG_NAME_MATCHERS = [...DEFAULT_PROJECT_MANIFESTS, CODEGRAPH_CONFIG_FILE].map((pattern) => picomatch(normalizeGlobPattern(pattern), { dot: true }));
129
+ function isRootConfigFileName(fileName) {
130
+ return ROOT_CONFIG_NAME_MATCHERS.some((matcher) => matcher(fileName));
131
+ }
132
+ async function listRootConfigFiles(root, state) {
133
+ const entries = await readDirectoryWithFileTypes(root, state);
134
+ const files = [];
135
+ for (const entry of entries) {
136
+ if (!isRootConfigFileName(entry.name))
137
+ continue;
138
+ const file = normalizePath(path.join(root, entry.name));
139
+ if (!entry.isFile()) {
140
+ if (!entry.isSymbolicLink())
141
+ continue;
142
+ try {
143
+ if (!(await fsp.stat(file)).isFile())
144
+ continue;
145
+ }
146
+ catch (error) {
147
+ const code = error.code;
148
+ if (code === "ENOENT" || code === "ENOTDIR")
149
+ continue;
150
+ throw error;
151
+ }
152
+ }
153
+ files.push(file);
154
+ }
155
+ return files;
156
+ }
124
157
  const DIRECTORY_METADATA_EXTENSIONS = new Set();
125
158
  const DIRECTORY_METADATA_BASENAMES = new Set();
126
159
  for (const definition of PROJECT_FILE_DEFINITIONS) {
@@ -275,6 +308,126 @@ const EMPTY_GITIGNORE_INDEX = {
275
308
  repositoryAliases: [],
276
309
  byBaseDir: new Map(),
277
310
  };
311
+ class DiscoveryContext {
312
+ projectRoot;
313
+ git;
314
+ gitCandidatesByRoot = new Map();
315
+ ignoreIndexByKey = new Map();
316
+ fileTextByPath = new Map();
317
+ readdirCache = new Map();
318
+ globFilesystem;
319
+ constructor(projectRoot) {
320
+ this.projectRoot = projectRoot;
321
+ this.git = createGitDiscoveryCache();
322
+ this.globFilesystem = { readdir: createSharedReaddir(this) };
323
+ }
324
+ }
325
+ function createSharedReaddir(state) {
326
+ function readdir(directory, optionsOrCallback, callback) {
327
+ const listing = readDirectoryWithFileTypes(directory, state);
328
+ if (typeof optionsOrCallback === "function") {
329
+ listing.then((entries) => optionsOrCallback(null, entries.map((entry) => entry.name)), (error) => optionsOrCallback(error, []));
330
+ }
331
+ else if (callback) {
332
+ listing.then((entries) => callback(null, entries), (error) => callback(error, []));
333
+ }
334
+ }
335
+ return readdir;
336
+ }
337
+ function globFilesystemOption(state) {
338
+ if (!state)
339
+ return {};
340
+ return { fs: state.globFilesystem };
341
+ }
342
+ function globFilesystemField(state) {
343
+ if (!state)
344
+ return {};
345
+ return { globFilesystem: state.globFilesystem };
346
+ }
347
+ async function readDirectoryWithFileTypes(directory, state) {
348
+ if (!state)
349
+ return fsp.readdir(directory, { withFileTypes: true });
350
+ const key = fileIdentityKey(directory);
351
+ const cached = state.readdirCache.get(key);
352
+ if (cached)
353
+ return cached;
354
+ const pending = fsp.readdir(directory, { withFileTypes: true });
355
+ state.readdirCache.set(key, pending);
356
+ return pending;
357
+ }
358
+ async function readDiscoveryFileResult(state, filePath) {
359
+ if (!state) {
360
+ try {
361
+ return { ok: true, text: await fsp.readFile(filePath, "utf8") };
362
+ }
363
+ catch (error) {
364
+ return { ok: false, error };
365
+ }
366
+ }
367
+ const key = fileIdentityKey(filePath);
368
+ const cached = state.fileTextByPath.get(key);
369
+ if (cached)
370
+ return await cached;
371
+ const pending = (async () => {
372
+ try {
373
+ return { ok: true, text: await fsp.readFile(filePath, "utf8") };
374
+ }
375
+ catch (error) {
376
+ return { ok: false, error };
377
+ }
378
+ })();
379
+ state.fileTextByPath.set(key, pending);
380
+ return await pending;
381
+ }
382
+ export async function readProjectDiscoveryFileText(discoveryContext, filePath) {
383
+ const result = await readDiscoveryFileResult(discoveryContext, filePath);
384
+ if (result.ok)
385
+ return result.text;
386
+ if (result.error instanceof Error)
387
+ throw result.error;
388
+ throw new Error(`Failed to read ${filePath}: ${stringifyUnknown(result.error)}`);
389
+ }
390
+ async function sharedGitignoreIndex(state, cacheKey, load) {
391
+ if (!state)
392
+ return await load();
393
+ const cached = state.ignoreIndexByKey.get(cacheKey);
394
+ if (cached)
395
+ return await cached;
396
+ const pending = load();
397
+ state.ignoreIndexByKey.set(cacheKey, pending);
398
+ return await pending;
399
+ }
400
+ function rememberGitCandidates(state, root, candidates) {
401
+ if (!state)
402
+ return;
403
+ const key = fileIdentityKey(root);
404
+ if (state.gitCandidatesByRoot.has(key))
405
+ return;
406
+ state.gitCandidatesByRoot.set(key, Promise.resolve(candidates));
407
+ }
408
+ async function getSharedGitCandidates(context, root, logLevel, callbacks) {
409
+ const state = context;
410
+ const gitCache = context?.git;
411
+ if (!state)
412
+ return await listGitCandidateFiles(root, logLevel, callbacks, gitCache);
413
+ const key = fileIdentityKey(root);
414
+ const cached = state.gitCandidatesByRoot.get(key);
415
+ if (cached)
416
+ return await cached;
417
+ const pending = listGitCandidateFiles(root, logLevel, callbacks, gitCache);
418
+ state.gitCandidatesByRoot.set(key, pending);
419
+ return await pending;
420
+ }
421
+ function gitExcludeListOptions(gitCache) {
422
+ if (!gitCache)
423
+ return undefined;
424
+ return { discoveryCache: gitCache };
425
+ }
426
+ function gitUntrackedListOptions(gitCache) {
427
+ if (!gitCache)
428
+ return { respectGitignore: true };
429
+ return { respectGitignore: true, discoveryCache: gitCache };
430
+ }
278
431
  /**
279
432
  * Parse `.gitignore` rules from an already-discovered set of `.gitignore` files.
280
433
  *
@@ -284,7 +437,7 @@ const EMPTY_GITIGNORE_INDEX = {
284
437
  * child negation could re-include files. Depth-first ordering also keeps the "last match
285
438
  * wins" order deeper files depend on, and drops the locale sensitivity of `localeCompare`.
286
439
  */
287
- async function buildGitignoreIndex(sources, repositoryRoots = [], repositoryAliases = []) {
440
+ async function buildGitignoreIndex(sources, repositoryRoots = [], repositoryAliases = [], state) {
288
441
  const sorted = [...sources]
289
442
  .map(({ file, baseDir, repositoryRoot, priority }) => ({
290
443
  file: normalizePath(file),
@@ -316,13 +469,10 @@ async function buildGitignoreIndex(sources, repositoryRoots = [], repositoryAlia
316
469
  if (isIgnoredByGitignore(path.dirname(file), gitignoreIndex, true)) {
317
470
  continue;
318
471
  }
319
- let raw;
320
- try {
321
- raw = await fsp.readFile(file, "utf8");
322
- }
323
- catch {
472
+ const fileText = await readDiscoveryFileResult(state, file);
473
+ if (!fileText.ok)
324
474
  continue;
325
- }
475
+ const raw = fileText.text;
326
476
  const baseKey = fileIdentityKey(baseDir);
327
477
  for (const line of raw.split(/\r?\n/)) {
328
478
  const rule = parseGitignoreRule(baseDir, line);
@@ -338,16 +488,17 @@ async function buildGitignoreIndex(sources, repositoryRoots = [], repositoryAlia
338
488
  }
339
489
  return gitignoreIndex;
340
490
  }
341
- async function findGitignoreFiles(projectRoot) {
491
+ async function findGitignoreFiles(projectRoot, state) {
342
492
  return await fg(["**/.gitignore"], {
343
493
  cwd: projectRoot,
344
494
  absolute: true,
345
495
  dot: true,
346
496
  followSymbolicLinks: false,
347
497
  ignore: DEFAULT_PROJECT_FILE_IGNORES,
498
+ ...globFilesystemOption(state),
348
499
  });
349
500
  }
350
- async function loadGitignoreIndexForRootAliases(projectRoot) {
501
+ async function loadGitignoreIndexForRootAliases(projectRoot, state) {
351
502
  const roots = [projectRoot];
352
503
  const realRoot = await fsp.realpath(projectRoot);
353
504
  if (normalizePath(realRoot) !== normalizePath(projectRoot)) {
@@ -355,11 +506,11 @@ async function loadGitignoreIndexForRootAliases(projectRoot) {
355
506
  }
356
507
  const sources = [];
357
508
  for (const root of roots) {
358
- for (const file of await findGitignoreFiles(root)) {
509
+ for (const file of await findGitignoreFiles(root, state)) {
359
510
  sources.push({ file, baseDir: path.dirname(file) });
360
511
  }
361
512
  }
362
- return await buildGitignoreIndex(sources);
513
+ return await buildGitignoreIndex(sources, [], [], state);
363
514
  }
364
515
  /**
365
516
  * Whether `.gitignore` rules ignore `absolutePath`.
@@ -513,7 +664,7 @@ async function gitignoreFileIfPresent(directory) {
513
664
  * `.gitignore` files walks every gitignored tree looking for them, which times
514
665
  * out on a large ignored data directory even after file listing already finished.
515
666
  */
516
- async function findGitIgnoreSources(sourceRoots, candidateFiles) {
667
+ async function findGitIgnoreSources(sourceRoots, candidateFiles, gitCache) {
517
668
  const roots = Array.from(new Map(sourceRoots.map(({ path: sourceRoot, repositoryRoot }) => [
518
669
  fileIdentityKey(sourceRoot),
519
670
  { path: normalizePath(sourceRoot), repositoryRoot: normalizePath(repositoryRoot) },
@@ -561,7 +712,7 @@ async function findGitIgnoreSources(sourceRoots, candidateFiles) {
561
712
  const file = await gitignoreFileIfPresent(dir);
562
713
  return file ? [{ file, baseDir: dir, repositoryRoot, priority: 2 }] : [];
563
714
  }),
564
- Promise.all(uniqueRepositoryRoots.map((repositoryRoot) => listGitExcludeFiles(repositoryRoot))),
715
+ Promise.all(uniqueRepositoryRoots.map((repositoryRoot) => listGitExcludeFiles(repositoryRoot, gitExcludeListOptions(gitCache)))),
565
716
  ]);
566
717
  return [
567
718
  ...gitignoreHits.flat(),
@@ -577,15 +728,15 @@ async function findGitIgnoreSources(sourceRoots, candidateFiles) {
577
728
  * Tracked plus untracked-but-not-ignored files are Git candidates. Submodule files retain
578
729
  * their own repository boundary, and source roots cover logical and physical aliases.
579
730
  */
580
- async function listGitCandidateFiles(root, logLevel, opts) {
731
+ async function listGitCandidateFiles(root, logLevel, opts, gitCache) {
581
732
  let gitListStart;
582
733
  let gitIgnoreStart;
583
734
  try {
584
735
  const realRoot = normalizePath(await fsp.realpath(root));
585
- const gitRoot = (await isGitRepo(root)) ? root : realRoot;
586
- if (!(await isGitRepo(gitRoot)))
736
+ const gitRoot = (await isGitRepo(root, gitCache)) ? root : realRoot;
737
+ if (!(await isGitRepo(gitRoot, gitCache)))
587
738
  return null;
588
- if (await isGitProjectRootIgnored(realRoot))
739
+ if (await isGitProjectRootIgnored(realRoot, gitCache))
589
740
  return null;
590
741
  const gitRepositoryRoot = await getGitRepositoryRoot(gitRoot);
591
742
  if (!gitRepositoryRoot)
@@ -593,9 +744,10 @@ async function listGitCandidateFiles(root, logLevel, opts) {
593
744
  emitDiscoveryActivity(opts?.onDiscoveryProgress, "Listing Git files");
594
745
  const listStartedAt = performance.now();
595
746
  gitListStart = listStartedAt;
747
+ const untrackedOptions = gitUntrackedListOptions(gitCache);
596
748
  const [tracked, untracked, stageSpecial] = await Promise.all([
597
749
  listTrackedFiles(gitRoot, { recurseSubmodules: true, ...(logLevel === undefined ? {} : { logLevel }) }),
598
- listUntrackedFiles(gitRoot, { respectGitignore: true }),
750
+ listUntrackedFiles(gitRoot, untrackedOptions),
599
751
  listGitStageSpecialPaths(gitRoot, { recurse: true }),
600
752
  ]);
601
753
  const submoduleDirectories = stageSpecial.gitlinks;
@@ -603,7 +755,7 @@ async function listGitCandidateFiles(root, logLevel, opts) {
603
755
  logicalPath: normalizePath(logicalPath),
604
756
  physicalPath: normalizePath(await fsp.realpath(logicalPath).catch(() => logicalPath)),
605
757
  })));
606
- const submoduleUntracked = await Promise.all(submoduleRoots.map(async ({ physicalPath }) => await listUntrackedFiles(physicalPath, { respectGitignore: true })));
758
+ const submoduleUntracked = await Promise.all(submoduleRoots.map(async ({ physicalPath }) => await listUntrackedFiles(physicalPath, untrackedOptions)));
607
759
  const rawFiles = Array.from(new Set([...tracked, ...untracked, ...submoduleUntracked.flat()])).sort();
608
760
  const needsLogicalRemap = normalizePath(gitRoot) !== normalizePath(root);
609
761
  const files = Array.from(new Set(rawFiles.map((file) => needsLogicalRemap && isFilePathWithinRoot(realRoot, file)
@@ -629,7 +781,7 @@ async function listGitCandidateFiles(root, logLevel, opts) {
629
781
  emitDiscoveryActivity(opts?.onDiscoveryProgress, "Listing Git ignore files");
630
782
  const ignoreStartedAt = performance.now();
631
783
  gitIgnoreStart = ignoreStartedAt;
632
- const gitignoreFiles = await findGitIgnoreSources(sourceRoots, files);
784
+ const gitignoreFiles = await findGitIgnoreSources(sourceRoots, files, gitCache);
633
785
  emitDiscoveryTiming(opts?.onDiscoveryTiming, "git-ignore", ignoreStartedAt);
634
786
  gitIgnoreStart = undefined;
635
787
  emitDiscoveryActivity(opts?.onDiscoveryProgress, "Listing Git ignore files", gitignoreFiles.length, gitignoreFiles.length);
@@ -712,8 +864,9 @@ async function listProjectFilesInternal(projectRoot, patterns = DEFAULT_PROJECT_
712
864
  ...(options?.onDiscoveryProgress ? { onDiscoveryProgress: options.onDiscoveryProgress } : {}),
713
865
  ...(options?.onDiscoveryTiming ? { onDiscoveryTiming: options.onDiscoveryTiming } : {}),
714
866
  };
867
+ const discovery = options?.discoveryContext ?? createProjectDiscoveryContext(root);
715
868
  const gitCandidates = attemptedGitCandidates
716
- ? await listGitCandidateFiles(root, options?.logLevel, gitDiscoveryCallbacks)
869
+ ? await getSharedGitCandidates(discovery, root, options?.logLevel, gitDiscoveryCallbacks)
717
870
  : null;
718
871
  // Report only when this call actually asked Git, including a null result. Callers that
719
872
  // already enumerated candidates (build-index) can hand the same set to metadata discovery
@@ -723,13 +876,13 @@ async function listProjectFilesInternal(projectRoot, patterns = DEFAULT_PROJECT_
723
876
  }
724
877
  let gitignoreIndex = EMPTY_GITIGNORE_INDEX;
725
878
  if (useGitignore && gitCandidates) {
726
- gitignoreIndex = await buildGitignoreIndex(gitCandidates.gitignoreFiles, gitCandidates.gitignoreRoots, gitCandidates.gitignoreAliases);
879
+ gitignoreIndex = await sharedGitignoreIndex(discovery, `git:${fileIdentityKey(root)}`, () => buildGitignoreIndex(gitCandidates.gitignoreFiles, gitCandidates.gitignoreRoots, gitCandidates.gitignoreAliases, discovery));
727
880
  }
728
881
  else if (useGitignore) {
729
882
  const gitignoreRoot = options?.gitignoreRoot
730
883
  ? await ensureDirectoryReadable(options.gitignoreRoot, "Gitignore root")
731
884
  : root;
732
- gitignoreIndex = await loadGitignoreIndexForRootAliases(gitignoreRoot);
885
+ gitignoreIndex = await sharedGitignoreIndex(discovery, `fs:${fileIdentityKey(gitignoreRoot)}`, () => loadGitignoreIndexForRootAliases(gitignoreRoot, discovery));
733
886
  }
734
887
  // Git returns a candidate set, not a filtered result: the pattern, include,
735
888
  // default-ignore, and gitignore checks below still decide what is indexable.
@@ -746,6 +899,7 @@ async function listProjectFilesInternal(projectRoot, patterns = DEFAULT_PROJECT_
746
899
  dot: true,
747
900
  followSymbolicLinks: false,
748
901
  ignore: fastGlobIgnoreGlobs,
902
+ ...globFilesystemOption(discovery),
749
903
  });
750
904
  emitDiscoveryTiming(options?.onDiscoveryTiming, "filesystem-scan", scanStart);
751
905
  emitDiscoveryActivity(options?.onDiscoveryProgress, "Scanning project files", files.length, files.length);
@@ -765,6 +919,7 @@ async function listProjectFilesInternal(projectRoot, patterns = DEFAULT_PROJECT_
765
919
  dot: true,
766
920
  followSymbolicLinks: false,
767
921
  ignore: translatedUserIgnoreGlobs,
922
+ ...globFilesystemOption(discovery),
768
923
  })
769
924
  : [];
770
925
  const reportSourceSymlinkChecks = options?.onDiscoveryProgress
@@ -783,6 +938,7 @@ async function listProjectFilesInternal(projectRoot, patterns = DEFAULT_PROJECT_
783
938
  ? { onSymlinkDirectoriesDiscovered: options.onSymlinkDirectoriesDiscovered }
784
939
  : {}),
785
940
  ...(reportSourceSymlinkChecks ? { onPathCheckProgress: reportSourceSymlinkChecks } : {}),
941
+ ...globFilesystemField(discovery),
786
942
  };
787
943
  const safeSymlinkDirectories = await resolveSafeSymlinkDirectories(root, realRoot, symlinkProbeIgnoreGlobs, symlinkOptions);
788
944
  const linkedFiles = await listEntriesFromSafeSymlinkDirectories(root, realRoot, patterns, fastGlobIgnoreGlobs, {
@@ -796,6 +952,7 @@ async function listProjectFilesInternal(projectRoot, patterns = DEFAULT_PROJECT_
796
952
  globRoot,
797
953
  filterIgnoreGlobs: userIgnoreGlobs,
798
954
  resolvedSafeSymlinkDirectories: safeSymlinkDirectories,
955
+ ...globFilesystemField(discovery),
799
956
  });
800
957
  // Cheap path predicates run before the realpath confinement probe. Git enumerates
801
958
  // every tracked and untracked file it knows about, including trees this project
@@ -960,6 +1117,7 @@ async function resolveSafeSymlinkDirectories(root, realRoot, ignore, options) {
960
1117
  followSymbolicLinks: false,
961
1118
  objectMode: true,
962
1119
  ignore,
1120
+ ...(options.globFilesystem ? { fs: options.globFilesystem } : {}),
963
1121
  }));
964
1122
  const discovered = await verifySafeSymlinkDirectories(root, realRoot, entries.filter((entry) => entry.dirent.isSymbolicLink()).map((entry) => entry.path), options.onPathCheckProgress);
965
1123
  options.onSymlinkDirectoriesDiscovered?.(discovered, "filesystem");
@@ -985,6 +1143,7 @@ async function listEntriesFromSafeSymlinkDirectories(root, realRoot, patterns, i
985
1143
  ignore: locationIndependentIgnores,
986
1144
  ...(options.onlyFiles !== undefined ? { onlyFiles: options.onlyFiles } : {}),
987
1145
  ...(options.markDirectories !== undefined ? { markDirectories: options.markDirectories } : {}),
1146
+ ...(options.globFilesystem ? { fs: options.globFilesystem } : {}),
988
1147
  })).filter((filePath) => {
989
1148
  const cleanPath = filePath.endsWith("/") ? filePath.slice(0, -1) : filePath;
990
1149
  return !rootRelativeIgnoreMatchers.some((matcher) => matchesDiscoveryGlob(cleanPath, globRoot, matcher));
@@ -1019,14 +1178,14 @@ export async function filterRealPathsWithinRoot(paths, realRoot, onPathCheckProg
1019
1178
  function toProjectGlob(pattern) {
1020
1179
  return pattern.startsWith("**/") ? pattern : `**/${pattern}`;
1021
1180
  }
1022
- async function buildProjectFileInfo(def, filePath) {
1181
+ async function buildProjectFileInfo(def, filePath, state) {
1023
1182
  const normalizedPath = normalizePath(filePath);
1024
1183
  const projectRoot = normalizePath(path.dirname(filePath));
1025
1184
  let name = null;
1026
1185
  if (def.parseName && def.kind === "file") {
1027
1186
  try {
1028
- const raw = await fsp.readFile(filePath, "utf8");
1029
- name = def.parseName(raw, filePath);
1187
+ const text = await readProjectDiscoveryFileText(state, filePath);
1188
+ name = def.parseName(text, filePath);
1030
1189
  }
1031
1190
  catch {
1032
1191
  name = null;
@@ -1069,12 +1228,14 @@ async function discoverProjectFilesInternal(projectRoot, options) {
1069
1228
  return matcher ? matcher.test(fileName) : pattern === fileName;
1070
1229
  });
1071
1230
  };
1231
+ const discovery = options?.discoveryContext ?? createProjectDiscoveryContext(root);
1072
1232
  let gitCandidates;
1073
1233
  if (options?.knownGitCandidates !== undefined) {
1074
1234
  gitCandidates = options.knownGitCandidates;
1235
+ rememberGitCandidates(discovery, root, gitCandidates);
1075
1236
  }
1076
1237
  else {
1077
- gitCandidates = await listGitCandidateFiles(root, options?.logLevel, {
1238
+ gitCandidates = await getSharedGitCandidates(discovery, root, options?.logLevel, {
1078
1239
  ...(options?.onDiscoveryProgress ? { onDiscoveryProgress: options.onDiscoveryProgress } : {}),
1079
1240
  ...(options?.onDiscoveryTiming ? { onDiscoveryTiming: options.onDiscoveryTiming } : {}),
1080
1241
  });
@@ -1091,7 +1252,7 @@ async function discoverProjectFilesInternal(projectRoot, options) {
1091
1252
  : undefined;
1092
1253
  let rootSafeMatches;
1093
1254
  if (gitCandidates) {
1094
- const gitignoreIndex = await buildGitignoreIndex(gitCandidates.gitignoreFiles, gitCandidates.gitignoreRoots, gitCandidates.gitignoreAliases);
1255
+ const gitignoreIndex = await sharedGitignoreIndex(discovery, `git:${fileIdentityKey(root)}`, () => buildGitignoreIndex(gitCandidates.gitignoreFiles, gitCandidates.gitignoreRoots, gitCandidates.gitignoreAliases, discovery));
1095
1256
  const defaultIgnoreMatchers = DEFAULT_PROJECT_FILE_IGNORES.map((globPattern) => picomatch(globPattern, { dot: true }));
1096
1257
  const filterGitIgnoredEntries = (entries, directoryKeys = new Set()) => entries
1097
1258
  .map(({ path: filePath, realPath }) => ({ filePath: normalizePath(filePath), realPath }))
@@ -1116,6 +1277,7 @@ async function discoverProjectFilesInternal(projectRoot, options) {
1116
1277
  ? { onSymlinkDirectoriesDiscovered: options.onSymlinkDirectoriesDiscovered }
1117
1278
  : {}),
1118
1279
  ...(reportMetadataSymlinkChecks ? { onPathCheckProgress: reportMetadataSymlinkChecks } : {}),
1280
+ ...globFilesystemField(discovery),
1119
1281
  };
1120
1282
  const safeSymlinkDirectories = await resolveSafeSymlinkDirectories(root, realRoot, DEFAULT_PROJECT_FILE_IGNORES, symlinkOptions);
1121
1283
  const allPatterns = PROJECT_FILE_DEFINITIONS.flatMap((definition) => definition.patterns.map(toProjectGlob));
@@ -1123,6 +1285,7 @@ async function discoverProjectFilesInternal(projectRoot, options) {
1123
1285
  markDirectories: true,
1124
1286
  onlyFiles: false,
1125
1287
  resolvedSafeSymlinkDirectories: safeSymlinkDirectories,
1288
+ ...globFilesystemField(discovery),
1126
1289
  });
1127
1290
  const candidateFiles = Array.from(new Set(gitCandidates.files.map(normalizePath)));
1128
1291
  // Only a candidate whose basename can name a manifest becomes a metadata entry, so
@@ -1155,6 +1318,7 @@ async function discoverProjectFilesInternal(projectRoot, options) {
1155
1318
  ignore: DEFAULT_PROJECT_FILE_IGNORES,
1156
1319
  markDirectories: true,
1157
1320
  onlyFiles: false,
1321
+ ...globFilesystemOption(discovery),
1158
1322
  });
1159
1323
  const linkedMatches = await listEntriesFromSafeSymlinkDirectories(root, realRoot, allPatterns, DEFAULT_PROJECT_FILE_IGNORES, {
1160
1324
  markDirectories: true,
@@ -1166,6 +1330,7 @@ async function discoverProjectFilesInternal(projectRoot, options) {
1166
1330
  ? { onSymlinkDirectoriesDiscovered: options.onSymlinkDirectoriesDiscovered }
1167
1331
  : {}),
1168
1332
  ...(reportMetadataSymlinkChecks ? { onPathCheckProgress: reportMetadataSymlinkChecks } : {}),
1333
+ ...globFilesystemField(discovery),
1169
1334
  });
1170
1335
  rootSafeMatches = await filterRealPathsWithinRoot([...matches, ...linkedMatches].map((match) => (match.endsWith("/") ? match.slice(0, -1) : match)), realRoot, reportMetadataFileChecks);
1171
1336
  }
@@ -1182,7 +1347,7 @@ async function discoverProjectFilesInternal(projectRoot, options) {
1182
1347
  continue;
1183
1348
  const matchesPattern = matchesDefinition(fileName, definitionIndex);
1184
1349
  if (matchesPattern) {
1185
- entries.push(await buildProjectFileInfo(def, cleanMatch));
1350
+ entries.push(await buildProjectFileInfo(def, cleanMatch, discovery));
1186
1351
  }
1187
1352
  }
1188
1353
  });
@@ -1206,4 +1371,88 @@ async function discoverProjectFilesInternal(projectRoot, options) {
1206
1371
  throw new Error(`Failed to discover project files in ${root}: ${stringifyUnknown(error)}`);
1207
1372
  }
1208
1373
  }
1374
+ async function addGitExcludeFiles(files, repositoryRoot, gitCache) {
1375
+ const excludes = await listGitExcludeFiles(repositoryRoot, gitExcludeListOptions(gitCache));
1376
+ for (const source of excludes) {
1377
+ files.add(normalizePath(source.file));
1378
+ }
1379
+ }
1380
+ async function collectAncestorGitignoreFiles(fromDir, stopDir) {
1381
+ const files = [];
1382
+ let current = normalizePath(fromDir);
1383
+ const stopKey = fileIdentityKey(stopDir);
1384
+ while (true) {
1385
+ const hit = await gitignoreFileIfPresent(current);
1386
+ if (hit)
1387
+ files.push(hit);
1388
+ if (fileIdentityKey(current) === stopKey)
1389
+ break;
1390
+ const parent = normalizePath(path.dirname(current));
1391
+ if (parent === current)
1392
+ break;
1393
+ if (!isFilePathWithinRoot(stopDir, parent) && fileIdentityKey(parent) !== stopKey)
1394
+ break;
1395
+ current = parent;
1396
+ }
1397
+ return files;
1398
+ }
1399
+ async function addGitignoreFilesForRoots(files, roots, state, scanned) {
1400
+ for (const ignoreRoot of roots) {
1401
+ const key = fileIdentityKey(ignoreRoot);
1402
+ if (scanned.has(key))
1403
+ continue;
1404
+ scanned.add(key);
1405
+ for (const file of await findGitignoreFiles(ignoreRoot, state)) {
1406
+ files.add(normalizePath(file));
1407
+ }
1408
+ }
1409
+ }
1410
+ export async function collectConfigHashInputFiles(projectRoot, discoveryContext, logLevel, callbacks) {
1411
+ const root = await ensureDirectoryReadable(projectRoot, "Project root");
1412
+ const state = discoveryContext;
1413
+ const files = new Set(await listRootConfigFiles(root, state));
1414
+ const gitCache = discoveryContext.git;
1415
+ const gitCandidates = await getSharedGitCandidates(discoveryContext, root, logLevel, callbacks);
1416
+ if (gitCandidates) {
1417
+ for (const source of gitCandidates.gitignoreFiles) {
1418
+ files.add(normalizePath(source.file));
1419
+ }
1420
+ return { files: Array.from(files).sort() };
1421
+ }
1422
+ const realRoot = normalizePath(await fsp.realpath(root));
1423
+ const gitRootCandidate = (await isGitRepo(root, gitCache)) ? root : realRoot;
1424
+ if (!(await isGitRepo(gitRootCandidate, gitCache))) {
1425
+ await addGitignoreFilesForRoots(files, [root], state, new Set());
1426
+ return { files: Array.from(files).sort() };
1427
+ }
1428
+ if (await isGitProjectRootIgnored(realRoot, gitCache)) {
1429
+ const scanned = new Set();
1430
+ await addGitignoreFilesForRoots(files, [root], state, scanned);
1431
+ const gitRoot = await getGitRepositoryRoot(gitRootCandidate);
1432
+ if (gitRoot) {
1433
+ await addGitExcludeFiles(files, gitRoot, gitCache);
1434
+ const submoduleDirectories = await listGitSubmoduleDirectories(gitRoot, { recurse: true });
1435
+ await addGitignoreFilesForRoots(files, [gitRoot, ...submoduleDirectories], state, scanned);
1436
+ for (const submodule of submoduleDirectories) {
1437
+ await addGitExcludeFiles(files, submodule, gitCache);
1438
+ }
1439
+ }
1440
+ return { files: Array.from(files).sort() };
1441
+ }
1442
+ const gitRoot = await getGitRepositoryRoot(gitRootCandidate);
1443
+ if (gitRoot) {
1444
+ for (const file of await collectAncestorGitignoreFiles(root, gitRoot)) {
1445
+ files.add(normalizePath(file));
1446
+ }
1447
+ await addGitExcludeFiles(files, gitRoot, gitCache);
1448
+ const submoduleDirectories = await listGitSubmoduleDirectories(gitRoot, { recurse: true });
1449
+ for (const submodule of submoduleDirectories) {
1450
+ await addGitExcludeFiles(files, submodule, gitCache);
1451
+ }
1452
+ }
1453
+ return {
1454
+ files: Array.from(files).sort(),
1455
+ error: "Failed to enumerate config files: Git listing unavailable",
1456
+ };
1457
+ }
1209
1458
  //# sourceMappingURL=projectFiles.js.map