@j0hanz/fs-context-mcp 2.2.0 → 2.3.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 (131) hide show
  1. package/README.md +171 -417
  2. package/dist/assets/logo.svg +3766 -0
  3. package/dist/config.d.ts +0 -1
  4. package/dist/config.js +0 -1
  5. package/dist/index.d.ts +0 -1
  6. package/dist/index.js +0 -1
  7. package/dist/lib/constants.d.ts +0 -1
  8. package/dist/lib/constants.js +0 -1
  9. package/dist/lib/errors.d.ts +0 -1
  10. package/dist/lib/errors.js +0 -1
  11. package/dist/lib/file-operations/file-info.d.ts +0 -1
  12. package/dist/lib/file-operations/file-info.js +0 -1
  13. package/dist/lib/file-operations/gitignore.d.ts +0 -1
  14. package/dist/lib/file-operations/gitignore.js +0 -1
  15. package/dist/lib/file-operations/glob-engine.d.ts +0 -1
  16. package/dist/lib/file-operations/glob-engine.js +101 -84
  17. package/dist/lib/file-operations/list-directory.d.ts +0 -1
  18. package/dist/lib/file-operations/list-directory.js +91 -62
  19. package/dist/lib/file-operations/read-multiple-files.d.ts +0 -1
  20. package/dist/lib/file-operations/read-multiple-files.js +0 -1
  21. package/dist/lib/file-operations/search-content.d.ts +0 -1
  22. package/dist/lib/file-operations/search-content.js +68 -21
  23. package/dist/lib/file-operations/search-files.d.ts +0 -1
  24. package/dist/lib/file-operations/search-files.js +21 -11
  25. package/dist/lib/file-operations/search-worker.d.ts +0 -1
  26. package/dist/lib/file-operations/search-worker.js +0 -1
  27. package/dist/lib/file-operations/tree.d.ts +0 -1
  28. package/dist/lib/file-operations/tree.js +18 -8
  29. package/dist/lib/fs-helpers.d.ts +0 -1
  30. package/dist/lib/fs-helpers.js +19 -25
  31. package/dist/lib/observability.d.ts +0 -1
  32. package/dist/lib/observability.js +49 -43
  33. package/dist/lib/path-policy.d.ts +0 -1
  34. package/dist/lib/path-policy.js +0 -1
  35. package/dist/lib/path-validation.d.ts +0 -1
  36. package/dist/lib/path-validation.js +54 -38
  37. package/dist/lib/resource-store.d.ts +0 -1
  38. package/dist/lib/resource-store.js +0 -1
  39. package/dist/resources.d.ts +2 -3
  40. package/dist/resources.js +16 -3
  41. package/dist/schemas.d.ts +0 -1
  42. package/dist/schemas.js +35 -51
  43. package/dist/server.d.ts +0 -1
  44. package/dist/server.js +42 -11
  45. package/dist/tools/list-directory.d.ts +0 -1
  46. package/dist/tools/list-directory.js +14 -2
  47. package/dist/tools/read-multiple.d.ts +0 -1
  48. package/dist/tools/read-multiple.js +14 -2
  49. package/dist/tools/read.d.ts +0 -1
  50. package/dist/tools/read.js +14 -2
  51. package/dist/tools/roots.d.ts +0 -1
  52. package/dist/tools/roots.js +14 -2
  53. package/dist/tools/search-content.d.ts +0 -1
  54. package/dist/tools/search-content.js +29 -14
  55. package/dist/tools/search-files.d.ts +0 -1
  56. package/dist/tools/search-files.js +14 -2
  57. package/dist/tools/shared.d.ts +7 -1
  58. package/dist/tools/shared.js +7 -4
  59. package/dist/tools/stat-many.d.ts +0 -1
  60. package/dist/tools/stat-many.js +14 -2
  61. package/dist/tools/stat.d.ts +0 -1
  62. package/dist/tools/stat.js +14 -2
  63. package/dist/tools/tree.d.ts +0 -1
  64. package/dist/tools/tree.js +14 -2
  65. package/dist/tools.d.ts +0 -1
  66. package/dist/tools.js +0 -1
  67. package/package.json +22 -19
  68. package/dist/config.d.ts.map +0 -1
  69. package/dist/config.js.map +0 -1
  70. package/dist/index.d.ts.map +0 -1
  71. package/dist/index.js.map +0 -1
  72. package/dist/lib/constants.d.ts.map +0 -1
  73. package/dist/lib/constants.js.map +0 -1
  74. package/dist/lib/errors.d.ts.map +0 -1
  75. package/dist/lib/errors.js.map +0 -1
  76. package/dist/lib/file-operations/file-info.d.ts.map +0 -1
  77. package/dist/lib/file-operations/file-info.js.map +0 -1
  78. package/dist/lib/file-operations/gitignore.d.ts.map +0 -1
  79. package/dist/lib/file-operations/gitignore.js.map +0 -1
  80. package/dist/lib/file-operations/glob-engine.d.ts.map +0 -1
  81. package/dist/lib/file-operations/glob-engine.js.map +0 -1
  82. package/dist/lib/file-operations/list-directory.d.ts.map +0 -1
  83. package/dist/lib/file-operations/list-directory.js.map +0 -1
  84. package/dist/lib/file-operations/read-multiple-files.d.ts.map +0 -1
  85. package/dist/lib/file-operations/read-multiple-files.js.map +0 -1
  86. package/dist/lib/file-operations/search-content.d.ts.map +0 -1
  87. package/dist/lib/file-operations/search-content.js.map +0 -1
  88. package/dist/lib/file-operations/search-files.d.ts.map +0 -1
  89. package/dist/lib/file-operations/search-files.js.map +0 -1
  90. package/dist/lib/file-operations/search-worker.d.ts.map +0 -1
  91. package/dist/lib/file-operations/search-worker.js.map +0 -1
  92. package/dist/lib/file-operations/tree.d.ts.map +0 -1
  93. package/dist/lib/file-operations/tree.js.map +0 -1
  94. package/dist/lib/fs-helpers.d.ts.map +0 -1
  95. package/dist/lib/fs-helpers.js.map +0 -1
  96. package/dist/lib/observability.d.ts.map +0 -1
  97. package/dist/lib/observability.js.map +0 -1
  98. package/dist/lib/path-policy.d.ts.map +0 -1
  99. package/dist/lib/path-policy.js.map +0 -1
  100. package/dist/lib/path-validation.d.ts.map +0 -1
  101. package/dist/lib/path-validation.js.map +0 -1
  102. package/dist/lib/resource-store.d.ts.map +0 -1
  103. package/dist/lib/resource-store.js.map +0 -1
  104. package/dist/resources.d.ts.map +0 -1
  105. package/dist/resources.js.map +0 -1
  106. package/dist/schemas.d.ts.map +0 -1
  107. package/dist/schemas.js.map +0 -1
  108. package/dist/server.d.ts.map +0 -1
  109. package/dist/server.js.map +0 -1
  110. package/dist/tools/list-directory.d.ts.map +0 -1
  111. package/dist/tools/list-directory.js.map +0 -1
  112. package/dist/tools/read-multiple.d.ts.map +0 -1
  113. package/dist/tools/read-multiple.js.map +0 -1
  114. package/dist/tools/read.d.ts.map +0 -1
  115. package/dist/tools/read.js.map +0 -1
  116. package/dist/tools/roots.d.ts.map +0 -1
  117. package/dist/tools/roots.js.map +0 -1
  118. package/dist/tools/search-content.d.ts.map +0 -1
  119. package/dist/tools/search-content.js.map +0 -1
  120. package/dist/tools/search-files.d.ts.map +0 -1
  121. package/dist/tools/search-files.js.map +0 -1
  122. package/dist/tools/shared.d.ts.map +0 -1
  123. package/dist/tools/shared.js.map +0 -1
  124. package/dist/tools/stat-many.d.ts.map +0 -1
  125. package/dist/tools/stat-many.js.map +0 -1
  126. package/dist/tools/stat.d.ts.map +0 -1
  127. package/dist/tools/stat.js.map +0 -1
  128. package/dist/tools/tree.d.ts.map +0 -1
  129. package/dist/tools/tree.js.map +0 -1
  130. package/dist/tools.d.ts.map +0 -1
  131. package/dist/tools.js.map +0 -1
package/dist/config.d.ts CHANGED
@@ -115,4 +115,3 @@ export declare function formatOperationSummary(summary: {
115
115
  truncated?: boolean;
116
116
  truncatedReason?: string;
117
117
  }): string;
118
- //# sourceMappingURL=config.d.ts.map
package/dist/config.js CHANGED
@@ -28,4 +28,3 @@ export function formatOperationSummary(summary) {
28
28
  return '';
29
29
  return `\n[truncated: ${summary.truncatedReason ?? 'limit reached'}]`;
30
30
  }
31
- //# sourceMappingURL=config.js.map
package/dist/index.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -69,4 +69,3 @@ main().catch((error) => {
69
69
  console.error('Fatal error:', error instanceof Error ? error.message : String(error));
70
70
  process.exit(1);
71
71
  });
72
- //# sourceMappingURL=index.js.map
@@ -20,4 +20,3 @@ export declare const SENSITIVE_FILE_ALLOWLIST: string[];
20
20
  export declare const KNOWN_BINARY_EXTENSIONS: Set<string>;
21
21
  export declare const DEFAULT_EXCLUDE_PATTERNS: string[];
22
22
  export declare function getMimeType(ext: string): string;
23
- //# sourceMappingURL=constants.d.ts.map
@@ -211,4 +211,3 @@ export function getMimeType(ext) {
211
211
  const lowerExt = ext.toLowerCase();
212
212
  return MIME_TYPES.get(lowerExt) ?? 'application/octet-stream';
213
213
  }
214
- //# sourceMappingURL=constants.js.map
@@ -18,4 +18,3 @@ export declare class McpError extends Error {
18
18
  export declare function createDetailedError(error: unknown, path?: string, additionalDetails?: Record<string, unknown>): DetailedError;
19
19
  export declare function formatDetailedError(error: DetailedError): string;
20
20
  export declare function getSuggestion(code: ErrorCode): string;
21
- //# sourceMappingURL=errors.d.ts.map
@@ -138,4 +138,3 @@ export function formatDetailedError(error) {
138
138
  export function getSuggestion(code) {
139
139
  return ERROR_SUGGESTIONS[code];
140
140
  }
141
- //# sourceMappingURL=errors.js.map
@@ -9,4 +9,3 @@ interface GetMultipleFileInfoOptions {
9
9
  }
10
10
  export declare function getMultipleFileInfo(paths: readonly string[], options?: GetMultipleFileInfoOptions): Promise<GetMultipleFileInfoResult>;
11
11
  export {};
12
- //# sourceMappingURL=file-info.d.ts.map
@@ -157,4 +157,3 @@ export async function getMultipleFileInfo(paths, options = {}) {
157
157
  summary: calculateSummary(output),
158
158
  };
159
159
  }
160
- //# sourceMappingURL=file-info.js.map
@@ -3,4 +3,3 @@ export declare function loadRootGitignore(root: string, signal?: AbortSignal): P
3
3
  export declare function isIgnoredByGitignore(matcher: Ignore, root: string, absolutePath: string, options?: {
4
4
  isDirectory?: boolean;
5
5
  }): boolean;
6
- //# sourceMappingURL=gitignore.d.ts.map
@@ -39,4 +39,3 @@ export function isIgnoredByGitignore(matcher, root, absolutePath, options = {})
39
39
  }
40
40
  return matcher.ignores(normalized);
41
41
  }
42
- //# sourceMappingURL=gitignore.js.map
@@ -24,4 +24,3 @@ export interface GlobEntriesOptions {
24
24
  }
25
25
  export declare function globEntries(options: GlobEntriesOptions): AsyncGenerator<GlobEntry>;
26
26
  export {};
27
- //# sourceMappingURL=glob-engine.d.ts.map
@@ -2,6 +2,9 @@ import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { glob as fsGlob } from 'node:fs/promises';
4
4
  import { publishOpsTraceEnd, publishOpsTraceError, publishOpsTraceStart, shouldPublishOpsTrace, } from '../observability.js';
5
+ function normalizeToPosixPath(filePath) {
6
+ return filePath.replace(/\\/gu, '/');
7
+ }
5
8
  function normalizePattern(pattern, baseNameMatch) {
6
9
  const normalized = pattern.replace(/\\/gu, '/');
7
10
  if (!baseNameMatch)
@@ -41,14 +44,12 @@ function splitPatternPrefix(normalizedPattern) {
41
44
  remainder: remainderSegments.join('/'),
42
45
  };
43
46
  }
44
- function normalizeToPosixPath(filePath) {
45
- return filePath.replace(/\\/gu, '/');
46
- }
47
47
  function buildHiddenPatterns(normalizedPattern, maxDepth) {
48
48
  const patterns = new Set();
49
49
  patterns.add(normalizedPattern);
50
50
  const { prefix, remainder } = splitPatternPrefix(normalizedPattern);
51
51
  const remainderSegments = remainder.length > 0 ? remainder.split('/') : [];
52
+ // Dotfile candidate handling (e.g. foo/bar -> .foo/bar, foo/.bar)
52
53
  const dotfileSegments = [...remainderSegments];
53
54
  const firstCandidateIndex = dotfileSegments.findIndex((segment) => segment !== '**' && segment.length > 0);
54
55
  if (firstCandidateIndex !== -1) {
@@ -58,6 +59,7 @@ function buildHiddenPatterns(normalizedPattern, maxDepth) {
58
59
  patterns.add(`${prefix}${dotfileSegments.join('/')}`);
59
60
  }
60
61
  }
62
+ // Globstar handling (e.g. **/foo -> **/.*/**/foo, **/.foo)
61
63
  if (remainder.startsWith('**/')) {
62
64
  const afterGlobstar = remainder.slice('**/'.length);
63
65
  for (let depth = 0; depth <= maxDepth; depth += 1) {
@@ -70,6 +72,40 @@ function buildHiddenPatterns(normalizedPattern, maxDepth) {
70
72
  }
71
73
  return [...patterns];
72
74
  }
75
+ function shouldUseGlobDirents(options) {
76
+ return !options.stats && !options.followSymbolicLinks;
77
+ }
78
+ function normalizeOptions(options) {
79
+ const normalized = normalizePattern(options.pattern, options.baseNameMatch);
80
+ const maxHiddenDepth = options.maxDepth ?? 10;
81
+ const patterns = options.includeHidden
82
+ ? buildHiddenPatterns(normalized, maxHiddenDepth)
83
+ : [normalized];
84
+ const result = {
85
+ cwd: options.cwd,
86
+ patterns,
87
+ exclude: normalizeIgnorePatterns(options.excludePatterns),
88
+ useDirents: shouldUseGlobDirents(options),
89
+ suppressErrors: options.suppressErrors ?? false,
90
+ };
91
+ if (options.maxDepth !== undefined) {
92
+ result.maxDepth = options.maxDepth;
93
+ }
94
+ return result;
95
+ }
96
+ function resolveDepth(cwd, entryPath) {
97
+ const relative = path.relative(cwd, entryPath);
98
+ if (relative.length === 0)
99
+ return 0;
100
+ const normalized = normalizeToPosixPath(relative);
101
+ const parts = normalized.split('/').filter((part) => part.length > 0);
102
+ return parts.length;
103
+ }
104
+ function isWithinDepthLimit(normalized, entryPath) {
105
+ if (normalized.maxDepth === undefined)
106
+ return true;
107
+ return resolveDepth(normalized.cwd, entryPath) <= normalized.maxDepth;
108
+ }
73
109
  function toAbsolutePath(cwd, match) {
74
110
  return path.isAbsolute(match) ? match : path.resolve(cwd, match);
75
111
  }
@@ -78,124 +114,106 @@ function toAbsolutePathFromDirent(cwd, dirent) {
78
114
  return path.resolve(base, dirent.name);
79
115
  }
80
116
  function isGlobDirentLike(value) {
81
- return (typeof value === 'object' &&
82
- value !== null &&
83
- 'name' in value &&
84
- typeof value.name === 'string');
117
+ if (typeof value !== 'object' || value === null)
118
+ return false;
119
+ const v = value;
120
+ return (typeof v.name === 'string' &&
121
+ typeof v.isFile === 'function' &&
122
+ typeof v.isDirectory === 'function' &&
123
+ typeof v.isSymbolicLink === 'function');
85
124
  }
86
125
  function resolveAbsolutePathFromGlobMatch(cwd, match) {
87
126
  return typeof match === 'string'
88
127
  ? toAbsolutePath(cwd, match)
89
128
  : toAbsolutePathFromDirent(cwd, match);
90
129
  }
91
- function isWithinDepthLimit(options, entryPath) {
92
- if (options.maxDepth === undefined)
93
- return true;
94
- return resolveDepth(options.cwd, entryPath) <= options.maxDepth;
130
+ function passesOnlyFilesFilter(dirent, onlyFiles) {
131
+ return !onlyFiles || dirent.isFile();
132
+ }
133
+ function buildEntryFromGlobDirent(absolutePath, dirent, options) {
134
+ if (!passesOnlyFilesFilter(dirent, options.onlyFiles))
135
+ return null;
136
+ return { path: absolutePath, dirent };
137
+ }
138
+ async function buildEntry(entryPath, options) {
139
+ try {
140
+ const stats = options.followSymbolicLinks
141
+ ? await fs.stat(entryPath)
142
+ : await fs.lstat(entryPath);
143
+ if (!passesOnlyFilesFilter(stats, options.onlyFiles)) {
144
+ return null;
145
+ }
146
+ const result = { path: entryPath, dirent: stats };
147
+ if (options.stats)
148
+ result.stats = stats;
149
+ return result;
150
+ }
151
+ catch (error) {
152
+ if (options.suppressErrors)
153
+ return null;
154
+ throw error;
155
+ }
95
156
  }
96
- function createGlobIterator(pattern, cwd, exclude, withFileTypes, suppressErrors) {
157
+ function tryCreateGlobIterable(pattern, normalized) {
97
158
  try {
98
159
  return fsGlob(pattern, {
99
- cwd,
100
- exclude,
101
- withFileTypes,
160
+ cwd: normalized.cwd,
161
+ exclude: normalized.exclude,
162
+ withFileTypes: normalized.useDirents,
102
163
  });
103
164
  }
104
165
  catch (error) {
105
- if (suppressErrors)
166
+ if (normalized.suppressErrors)
106
167
  return null;
107
168
  throw error;
108
169
  }
109
170
  }
110
- function shouldUseGlobDirents(options) {
111
- return !options.stats && !options.followSymbolicLinks;
112
- }
113
- function buildEntryFromGlobDirent(absolutePath, dirent, options) {
114
- if (options.onlyFiles && !dirent.isFile())
115
- return null;
116
- return { path: absolutePath, dirent };
117
- }
118
- async function processGlobMatch(match, options, seen, useDirents) {
119
- const absolutePath = resolveAbsolutePathFromGlobMatch(options.cwd, match);
171
+ function shouldYieldEntry(absolutePath, normalized, seen) {
120
172
  if (seen.has(absolutePath))
121
- return null;
173
+ return false;
122
174
  seen.add(absolutePath);
123
- if (!isWithinDepthLimit(options, absolutePath))
124
- return null;
125
- const entry = useDirents && isGlobDirentLike(match)
126
- ? buildEntryFromGlobDirent(absolutePath, match, options)
127
- : await buildEntry(absolutePath, options);
128
- if (!entry)
129
- return null;
130
- if (options.onlyFiles && !entry.dirent.isFile())
175
+ if (!isWithinDepthLimit(normalized, absolutePath))
176
+ return false;
177
+ return true;
178
+ }
179
+ async function buildEntryFromMatch(match, options, normalized, seen) {
180
+ const absolutePath = resolveAbsolutePathFromGlobMatch(normalized.cwd, match);
181
+ if (!shouldYieldEntry(absolutePath, normalized, seen)) {
131
182
  return null;
132
- return entry;
183
+ }
184
+ if (normalized.useDirents && isGlobDirentLike(match)) {
185
+ return buildEntryFromGlobDirent(absolutePath, match, options);
186
+ }
187
+ return await buildEntry(absolutePath, options);
133
188
  }
134
- async function* scanPattern(pattern, options, exclude, seen) {
135
- const useDirents = shouldUseGlobDirents(options);
136
- const iterator = createGlobIterator(pattern, options.cwd, exclude, useDirents, options.suppressErrors ?? false);
137
- if (!iterator)
189
+ async function* scanPattern(pattern, options, normalized, seen) {
190
+ const iterable = tryCreateGlobIterable(pattern, normalized);
191
+ if (!iterable)
138
192
  return;
139
193
  try {
140
- for await (const match of iterator) {
141
- const entry = await processGlobMatch(match, options, seen, useDirents);
194
+ for await (const match of iterable) {
195
+ const entry = await buildEntryFromMatch(match, options, normalized, seen);
142
196
  if (entry)
143
197
  yield entry;
144
198
  }
145
199
  }
146
200
  catch (error) {
147
- if (options.suppressErrors)
201
+ if (normalized.suppressErrors)
148
202
  return;
149
203
  throw error;
150
204
  }
151
205
  }
152
- function resolveDepth(cwd, entryPath) {
153
- const relative = path.relative(cwd, entryPath);
154
- if (relative.length === 0)
155
- return 0;
156
- const normalized = normalizeToPosixPath(relative);
157
- const parts = normalized.split('/').filter((part) => part.length > 0);
158
- return parts.length;
159
- }
160
- async function buildEntry(entryPath, options) {
161
- try {
162
- const stats = options.followSymbolicLinks
163
- ? await fs.stat(entryPath)
164
- : await fs.lstat(entryPath);
165
- const result = {
166
- path: entryPath,
167
- dirent: stats,
168
- };
169
- if (options.stats)
170
- result.stats = stats;
171
- return result;
172
- }
173
- catch (error) {
174
- if (options.suppressErrors) {
175
- return null;
176
- }
177
- throw error;
178
- }
179
- }
180
206
  async function* nativeGlobEntries(options) {
181
- const normalized = normalizePattern(options.pattern, options.baseNameMatch);
182
- const maxHiddenDepth = options.maxDepth ?? 10;
183
- const patterns = options.includeHidden
184
- ? buildHiddenPatterns(normalized, maxHiddenDepth)
185
- : [normalized];
186
- const exclude = normalizeIgnorePatterns(options.excludePatterns);
207
+ const normalized = normalizeOptions(options);
187
208
  const seen = new Set();
188
- for (const pattern of patterns) {
189
- yield* scanPattern(pattern, options, exclude, seen);
209
+ for (const pattern of normalized.patterns) {
210
+ yield* scanPattern(pattern, options, normalized, seen);
190
211
  }
191
212
  }
192
213
  export async function* globEntries(options) {
193
214
  const engine = 'node:fs/promises.glob';
194
215
  const traceContext = shouldPublishOpsTrace()
195
- ? {
196
- op: 'globEntries',
197
- engine,
198
- }
216
+ ? { op: 'globEntries', engine }
199
217
  : undefined;
200
218
  if (traceContext)
201
219
  publishOpsTraceStart(traceContext);
@@ -212,4 +230,3 @@ export async function* globEntries(options) {
212
230
  publishOpsTraceEnd(traceContext);
213
231
  }
214
232
  }
215
- //# sourceMappingURL=glob-engine.js.map
@@ -11,4 +11,3 @@ export interface ListDirectoryOptions {
11
11
  signal?: AbortSignal;
12
12
  }
13
13
  export declare function listDirectory(dirPath: string, options?: ListDirectoryOptions): Promise<ListDirectoryResult>;
14
- //# sourceMappingURL=list-directory.d.ts.map
@@ -1,9 +1,9 @@
1
1
  import * as fsp from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { DEFAULT_LIST_MAX_ENTRIES, DEFAULT_MAX_DEPTH, DEFAULT_SEARCH_TIMEOUT_MS, PARALLEL_CONCURRENCY, } from '../constants.js';
4
- import { createTimedAbortSignal, withAbort } from '../fs-helpers.js';
4
+ import { createTimedAbortSignal, processInParallel, withAbort, } from '../fs-helpers.js';
5
5
  import { isSensitivePath } from '../path-policy.js';
6
- import { validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
6
+ import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
7
7
  import { globEntries } from './glob-engine.js';
8
8
  function normalizePattern(pattern) {
9
9
  if (!pattern || pattern.length === 0)
@@ -20,10 +20,8 @@ function normalizeOptions(options) {
20
20
  sortBy: options.sortBy ?? 'name',
21
21
  includeSymlinkTargets: options.includeSymlinkTargets ?? false,
22
22
  timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
23
+ ...(pattern !== undefined ? { pattern } : {}),
23
24
  };
24
- if (pattern !== undefined) {
25
- normalized.pattern = pattern;
26
- }
27
25
  return normalized;
28
26
  }
29
27
  function sortEntries(entries, sortBy) {
@@ -44,31 +42,51 @@ function resolveMaxDepth(normalized) {
44
42
  }
45
43
  return normalized.maxDepth;
46
44
  }
47
- function shouldStopScan(signal, entryCount, maxEntries) {
48
- if (signal.aborted) {
49
- return { stop: true, reason: 'aborted' };
50
- }
51
- if (entryCount >= maxEntries) {
52
- return { stop: true, reason: 'maxEntries' };
53
- }
54
- return { stop: false };
45
+ function getStopReason(signal, acceptedCount, maxEntries) {
46
+ if (signal.aborted)
47
+ return 'aborted';
48
+ if (acceptedCount >= maxEntries)
49
+ return 'maxEntries';
50
+ return undefined;
55
51
  }
56
52
  async function* readDirectoryEntries(basePath, normalized, needsStats, signal) {
57
53
  const dirents = await withAbort(fsp.readdir(basePath, { withFileTypes: true }), signal);
54
+ const entries = [];
58
55
  for (const dirent of dirents) {
59
56
  if (!normalized.includeHidden && dirent.name.startsWith('.')) {
60
57
  continue;
61
58
  }
62
- const entryPath = path.join(basePath, dirent.name);
63
- let stats;
64
- if (needsStats) {
65
- stats = await withAbort(fsp.lstat(entryPath), signal);
59
+ entries.push({ dirent, entryPath: path.join(basePath, dirent.name) });
60
+ }
61
+ if (!needsStats) {
62
+ for (const entry of entries) {
63
+ yield {
64
+ path: entry.entryPath,
65
+ dirent: entry.dirent,
66
+ };
66
67
  }
68
+ return;
69
+ }
70
+ const { results, errors } = await processInParallel(entries.map((entry, index) => ({ entry, index })), async ({ entry, index }) => ({
71
+ index,
72
+ stats: await withAbort(fsp.lstat(entry.entryPath), signal),
73
+ }), PARALLEL_CONCURRENCY, signal);
74
+ if (errors.length > 0) {
75
+ throw errors[0]?.error ?? new Error('Failed to read entry stats');
76
+ }
77
+ const statsByIndex = new Map();
78
+ for (const result of results) {
79
+ statsByIndex.set(result.index, result.stats);
80
+ }
81
+ let index = 0;
82
+ for (const entry of entries) {
83
+ const stats = statsByIndex.get(index);
67
84
  yield {
68
- path: entryPath,
69
- dirent,
85
+ path: entry.entryPath,
86
+ dirent: entry.dirent,
70
87
  ...(stats ? { stats } : {}),
71
88
  };
89
+ index += 1;
72
90
  }
73
91
  }
74
92
  function createEntryStream(basePath, normalized, maxDepth, needsStats, signal) {
@@ -109,10 +127,10 @@ async function resolveSymlinkTarget(entryType, includeSymlinkTargets, entryPath)
109
127
  }
110
128
  return await fsp.readlink(entryPath).catch(() => undefined);
111
129
  }
112
- function updateTotals(type, totals) {
113
- if (type === 'file')
130
+ function updateTotals(entryType, totals) {
131
+ if (entryType === 'file')
114
132
  totals.files += 1;
115
- if (type === 'directory')
133
+ if (entryType === 'directory')
116
134
  totals.directories += 1;
117
135
  }
118
136
  function buildDirectoryEntry(basePath, entry, entryType, needsStats, symlinkTarget) {
@@ -133,7 +151,19 @@ function trackSymlink(entryType, includeSymlinkTargets, counters) {
133
151
  counters.symlinksNotFollowed += 1;
134
152
  }
135
153
  }
136
- async function isEntryAccessible(entryPath, signal, counters) {
154
+ async function isEntryAccessible(entryPath, entryType, basePath, signal, counters) {
155
+ if (entryType !== 'symlink') {
156
+ const normalized = normalizePath(entryPath);
157
+ if (!isPathWithinDirectories(normalized, [basePath])) {
158
+ counters.skippedInaccessible += 1;
159
+ return false;
160
+ }
161
+ if (isSensitivePath(entryPath, normalized)) {
162
+ counters.skippedInaccessible += 1;
163
+ return false;
164
+ }
165
+ return true;
166
+ }
137
167
  try {
138
168
  const validated = await validateExistingPathDetailed(entryPath, signal);
139
169
  if (isSensitivePath(validated.requestedPath, validated.resolvedPath)) {
@@ -147,24 +177,27 @@ async function isEntryAccessible(entryPath, signal, counters) {
147
177
  return false;
148
178
  }
149
179
  }
150
- async function appendEntryWithQueue(basePath, entry, normalized, needsStats, totals, entries, pending, flushPending) {
151
- const task = appendEntry(basePath, entry, normalized, needsStats, totals, entries);
152
- if (normalized.includeSymlinkTargets) {
153
- pending.push(task);
154
- if (pending.length >= PARALLEL_CONCURRENCY) {
155
- await flushPending();
156
- }
180
+ function appendEntry(entry, entryType, symlinkTarget, ctx) {
181
+ updateTotals(entryType, ctx.totals);
182
+ ctx.entries.push(buildDirectoryEntry(ctx.basePath, entry, entryType, ctx.needsStats, symlinkTarget));
183
+ }
184
+ async function enqueueAppendEntry(entry, entryType, ctx, pending, flushPending) {
185
+ if (!ctx.includeSymlinkTargets) {
186
+ appendEntry(entry, entryType, undefined, ctx);
157
187
  return;
158
188
  }
159
- await task;
160
- }
161
- async function appendEntry(basePath, entry, options, needsStats, totals, entries) {
162
- const entryType = resolveEntryType(entry.dirent);
163
- const symlinkTarget = await resolveSymlinkTarget(entryType, options.includeSymlinkTargets, entry.path);
164
- updateTotals(entryType, totals);
165
- entries.push(buildDirectoryEntry(basePath, entry, entryType, needsStats, symlinkTarget));
189
+ // Preserve the original behavior: resolve symlink targets in parallel
190
+ // when includeSymlinkTargets is enabled (bounded by PARALLEL_CONCURRENCY).
191
+ const task = (async () => {
192
+ const symlinkTarget = await resolveSymlinkTarget(entryType, ctx.includeSymlinkTargets, entry.path);
193
+ appendEntry(entry, entryType, symlinkTarget, ctx);
194
+ })();
195
+ pending.push(task);
196
+ if (pending.length >= PARALLEL_CONCURRENCY) {
197
+ await flushPending();
198
+ }
166
199
  }
167
- function buildSummary(entries, totals, maxDepth, truncated, stoppedReason, extra) {
200
+ function buildSummary(entries, totals, maxDepth, truncated, stoppedReason, counters) {
168
201
  const baseSummary = {
169
202
  totalEntries: entries.length,
170
203
  entriesScanned: entries.length,
@@ -173,8 +206,8 @@ function buildSummary(entries, totals, maxDepth, truncated, stoppedReason, extra
173
206
  totalDirectories: totals.directories,
174
207
  maxDepthReached: maxDepth,
175
208
  truncated,
176
- skippedInaccessible: extra.skippedInaccessible,
177
- symlinksNotFollowed: extra.symlinksNotFollowed,
209
+ skippedInaccessible: counters.skippedInaccessible,
210
+ symlinksNotFollowed: counters.symlinksNotFollowed,
178
211
  };
179
212
  return {
180
213
  ...baseSummary,
@@ -184,56 +217,53 @@ function buildSummary(entries, totals, maxDepth, truncated, stoppedReason, extra
184
217
  async function collectEntries(basePath, normalized, signal, needsStats, maxDepth) {
185
218
  const entries = [];
186
219
  const totals = { files: 0, directories: 0 };
220
+ const counters = { skippedInaccessible: 0, symlinksNotFollowed: 0 };
187
221
  let truncated = false;
188
222
  let stoppedReason;
189
- const counters = { skippedInaccessible: 0, symlinksNotFollowed: 0 };
190
223
  const pending = [];
191
- let scheduledCount = 0;
224
+ let acceptedCount = 0;
192
225
  const stream = createEntryStream(basePath, normalized, maxDepth, needsStats, signal);
193
226
  const flushPending = async () => {
194
227
  if (pending.length === 0)
195
228
  return;
196
229
  await Promise.allSettled(pending.splice(0));
197
230
  };
231
+ const appendCtx = {
232
+ basePath,
233
+ needsStats,
234
+ includeSymlinkTargets: normalized.includeSymlinkTargets,
235
+ totals,
236
+ entries,
237
+ };
198
238
  for await (const entry of stream) {
199
- const stop = shouldStopScan(signal, scheduledCount, normalized.maxEntries);
200
- if (stop.stop) {
239
+ const stopReason = getStopReason(signal, acceptedCount, normalized.maxEntries);
240
+ if (stopReason) {
201
241
  truncated = true;
202
- stoppedReason = stop.reason;
242
+ stoppedReason = stopReason;
203
243
  break;
204
244
  }
205
245
  const entryType = resolveEntryType(entry.dirent);
206
246
  trackSymlink(entryType, normalized.includeSymlinkTargets, counters);
207
- const accessible = await isEntryAccessible(entry.path, signal, counters);
247
+ const accessible = await isEntryAccessible(entry.path, entryType, basePath, signal, counters);
208
248
  if (!accessible) {
209
249
  continue;
210
250
  }
211
- scheduledCount += 1;
212
- await appendEntryWithQueue(basePath, entry, normalized, needsStats, totals, entries, pending, flushPending);
251
+ acceptedCount += 1;
252
+ await enqueueAppendEntry(entry, entryType, appendCtx, pending, flushPending);
213
253
  }
214
254
  if (normalized.includeSymlinkTargets) {
215
255
  await flushPending();
216
256
  }
217
- return {
218
- entries,
219
- totals,
220
- truncated,
221
- stoppedReason,
222
- skippedInaccessible: counters.skippedInaccessible,
223
- symlinksNotFollowed: counters.symlinksNotFollowed,
224
- };
257
+ return { entries, totals, truncated, stoppedReason, counters };
225
258
  }
226
259
  async function executeListDirectory(basePath, normalized, signal) {
227
260
  const needsStats = needsStatsForSort(normalized.sortBy);
228
261
  const maxDepth = resolveMaxDepth(normalized);
229
- const { entries, totals, truncated, stoppedReason, skippedInaccessible, symlinksNotFollowed, } = await collectEntries(basePath, normalized, signal, needsStats, maxDepth);
262
+ const { entries, totals, truncated, stoppedReason, counters } = await collectEntries(basePath, normalized, signal, needsStats, maxDepth);
230
263
  sortEntries(entries, normalized.sortBy);
231
264
  return {
232
265
  entries,
233
- summary: buildSummary(entries, totals, maxDepth, truncated, stoppedReason, {
234
- skippedInaccessible,
235
- symlinksNotFollowed,
236
- }),
266
+ summary: buildSummary(entries, totals, maxDepth, truncated, stoppedReason, counters),
237
267
  };
238
268
  }
239
269
  export async function listDirectory(dirPath, options = {}) {
@@ -248,4 +278,3 @@ export async function listDirectory(dirPath, options = {}) {
248
278
  cleanup();
249
279
  }
250
280
  }
251
- //# sourceMappingURL=list-directory.js.map
@@ -21,4 +21,3 @@ export interface ReadMultipleOptions {
21
21
  signal?: AbortSignal;
22
22
  }
23
23
  export declare function readMultipleFiles(filePaths: readonly string[], options?: ReadMultipleOptions): Promise<ReadMultipleResult[]>;
24
- //# sourceMappingURL=read-multiple-files.d.ts.map
@@ -211,4 +211,3 @@ export async function readMultipleFiles(filePaths, options = {}) {
211
211
  applySkippedBudget(output, skippedBudget, filePaths, normalized.maxTotalSize);
212
212
  return output;
213
213
  }
214
- //# sourceMappingURL=read-multiple-files.js.map
@@ -70,4 +70,3 @@ export interface ScanError {
70
70
  export type WorkerResponse = ScanResult | ScanError;
71
71
  export declare function searchContent(basePath: string, pattern: string, options?: SearchContentOptions): Promise<SearchContentResult>;
72
72
  export {};
73
- //# sourceMappingURL=search-content.d.ts.map