@j0hanz/filesystem-mcp 1.7.3 → 1.9.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.
- package/README.md +635 -377
- package/dist/cli.js +2 -2
- package/dist/completions.js +2 -3
- package/dist/index.js +2 -2
- package/dist/lib/file-operations/{common.d.ts → core.d.ts} +6 -0
- package/dist/lib/file-operations/{common.js → core.js} +45 -0
- package/dist/lib/file-operations/metadata.d.ts +73 -0
- package/dist/lib/file-operations/metadata.js +889 -0
- package/dist/lib/file-operations/{search-content.d.ts → search.d.ts} +29 -3
- package/dist/lib/file-operations/{search-content.js → search.js} +409 -82
- package/dist/lib/file-operations/{glob-engine.d.ts → traversal.d.ts} +18 -1
- package/dist/lib/file-operations/{glob-engine.js → traversal.js} +25 -2
- package/dist/lib/fs-helpers.d.ts +1 -0
- package/dist/lib/fs-helpers.js +10 -2
- package/dist/lib/observability.js +1 -1
- package/dist/lib/{path-validation.d.ts → paths.d.ts} +4 -0
- package/dist/lib/{path-validation.js → paths.js} +112 -1
- package/dist/lib/utils.d.ts +15 -0
- package/dist/lib/utils.js +33 -0
- package/dist/resources/generated-instructions.js +6 -6
- package/dist/resources/tool-catalog.js +9 -9
- package/dist/resources/tool-info.js +3 -3
- package/dist/resources/workflows.js +10 -23
- package/dist/schemas.js +15 -15
- package/dist/server/bootstrap.d.ts +19 -1
- package/dist/server/bootstrap.js +103 -9
- package/dist/server/roots-manager.d.ts +2 -2
- package/dist/server/roots-manager.js +3 -3
- package/dist/tools/apply-patch.js +7 -3
- package/dist/tools/calculate-hash.js +15 -15
- package/dist/tools/create-directory.js +1 -1
- package/dist/tools/delete-file.js +7 -4
- package/dist/tools/diff-files.js +2 -2
- package/dist/tools/edit-file.js +14 -13
- package/dist/tools/list-directory.js +5 -7
- package/dist/tools/move-file.js +2 -1
- package/dist/tools/read-multiple.js +13 -26
- package/dist/tools/read.js +3 -3
- package/dist/tools/replace-in-files.js +17 -20
- package/dist/tools/roots.js +4 -6
- package/dist/tools/search-content.js +9 -11
- package/dist/tools/search-files.js +4 -6
- package/dist/tools/shared.d.ts +18 -1
- package/dist/tools/shared.js +39 -19
- package/dist/tools/stat-many.js +14 -24
- package/dist/tools/stat.js +4 -3
- package/dist/tools/task-support.js +1 -1
- package/dist/tools/tree.js +3 -4
- package/dist/tools/write-file.js +1 -1
- package/package.json +9 -5
- package/dist/lib/file-operations/file-info.d.ts +0 -10
- package/dist/lib/file-operations/file-info.js +0 -143
- package/dist/lib/file-operations/gitignore.d.ts +0 -6
- package/dist/lib/file-operations/gitignore.js +0 -45
- package/dist/lib/file-operations/list-directory.d.ts +0 -14
- package/dist/lib/file-operations/list-directory.js +0 -256
- package/dist/lib/file-operations/read-multiple-files.d.ts +0 -25
- package/dist/lib/file-operations/read-multiple-files.js +0 -252
- package/dist/lib/file-operations/search-files.d.ts +0 -27
- package/dist/lib/file-operations/search-files.js +0 -218
- package/dist/lib/file-operations/search-worker.d.ts +0 -2
- package/dist/lib/file-operations/search-worker.js +0 -129
- package/dist/lib/file-operations/tree.d.ts +0 -28
- package/dist/lib/file-operations/tree.js +0 -269
- package/dist/lib/path-format.d.ts +0 -1
- package/dist/lib/path-format.js +0 -7
- package/dist/lib/path-policy.d.ts +0 -2
- package/dist/lib/path-policy.js +0 -100
- package/dist/lib/type-guards.d.ts +0 -1
- package/dist/lib/type-guards.js +0 -3
- package/dist/server/capabilities.d.ts +0 -10
- package/dist/server/capabilities.js +0 -48
- package/dist/server/logging.d.ts +0 -7
- package/dist/server/logging.js +0 -41
- package/dist/server/types.d.ts +0 -4
- package/dist/server/types.js +0 -1
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
import * as fsp from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_LIST_MAX_ENTRIES, DEFAULT_MAX_DEPTH, DEFAULT_READ_MANY_MAX_TOTAL_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, getMimeType, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../constants.js';
|
|
4
|
+
import { isAbortError } from '../errors.js';
|
|
5
|
+
import { assertNotAborted, getFileType, isHidden, processInParallel, readFile, readFileWithStats, withAbort, withTimedAbortSignal, } from '../fs-helpers.js';
|
|
6
|
+
import { assertAllowedFileAccess, isPathWithinDirectories, isSensitivePath, normalizePath, toPosixPath, validateExistingDirectory, validateExistingPath, validateExistingPathDetailed, } from '../paths.js';
|
|
7
|
+
import { applyIndexedErrors, applyIndexedValues, isEntryAccessibleByType, isIgnoredByGitignore, loadRootGitignore, needsStatsForSort, resolveEntryType, resolveStopReason, withOptionalStoppedReason, } from './core.js';
|
|
8
|
+
import { globEntries } from './traversal.js';
|
|
9
|
+
const PERM_STRINGS = [
|
|
10
|
+
'---',
|
|
11
|
+
'--x',
|
|
12
|
+
'-w-',
|
|
13
|
+
'-wx',
|
|
14
|
+
'r--',
|
|
15
|
+
'r-x',
|
|
16
|
+
'rw-',
|
|
17
|
+
'rwx',
|
|
18
|
+
];
|
|
19
|
+
const UNKNOWN_PATH = '(unknown)';
|
|
20
|
+
function getPermissions(mode) {
|
|
21
|
+
const ownerIndex = (mode >> 6) & 0b111;
|
|
22
|
+
const groupIndex = (mode >> 3) & 0b111;
|
|
23
|
+
const otherIndex = mode & 0b111;
|
|
24
|
+
const owner = PERM_STRINGS[ownerIndex] ?? '---';
|
|
25
|
+
const group = PERM_STRINGS[groupIndex] ?? '---';
|
|
26
|
+
const other = PERM_STRINGS[otherIndex] ?? '---';
|
|
27
|
+
return `${owner}${group}${other}`;
|
|
28
|
+
}
|
|
29
|
+
function buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, symlinkTarget) {
|
|
30
|
+
const tokenEstimate = stats.isFile() ? Math.ceil(stats.size / 4) : undefined;
|
|
31
|
+
return {
|
|
32
|
+
name,
|
|
33
|
+
path: requestedPath,
|
|
34
|
+
type: isSymlink ? 'symlink' : getFileType(stats),
|
|
35
|
+
size: stats.size,
|
|
36
|
+
...(tokenEstimate !== undefined ? { tokenEstimate } : {}),
|
|
37
|
+
created: stats.birthtime,
|
|
38
|
+
modified: stats.mtime,
|
|
39
|
+
accessed: stats.atime,
|
|
40
|
+
permissions: getPermissions(stats.mode),
|
|
41
|
+
isHidden: isHidden(name),
|
|
42
|
+
...(mimeType !== undefined ? { mimeType } : {}),
|
|
43
|
+
...(symlinkTarget !== undefined ? { symlinkTarget } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async function getSymlinkTarget(pathToRead, signal) {
|
|
47
|
+
assertNotAborted(signal);
|
|
48
|
+
try {
|
|
49
|
+
return await withAbort(fsp.readlink(pathToRead), signal);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (isAbortError(error))
|
|
53
|
+
throw error;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function getFileInfo(filePath, options = {}) {
|
|
58
|
+
const { signal } = options;
|
|
59
|
+
assertNotAborted(signal);
|
|
60
|
+
const { requestedPath, resolvedPath, isSymlink } = await validateExistingPathDetailed(filePath, signal);
|
|
61
|
+
assertAllowedFileAccess(requestedPath, resolvedPath);
|
|
62
|
+
const name = path.basename(requestedPath);
|
|
63
|
+
const ext = path.extname(name).toLowerCase();
|
|
64
|
+
const includeMimeType = options.includeMimeType !== false;
|
|
65
|
+
const mimeType = includeMimeType && ext.length > 0 ? getMimeType(ext) : undefined;
|
|
66
|
+
const symlinkTarget = isSymlink
|
|
67
|
+
? await getSymlinkTarget(requestedPath, signal)
|
|
68
|
+
: undefined;
|
|
69
|
+
const stats = await withAbort(fsp.stat(resolvedPath), signal);
|
|
70
|
+
return buildFileInfoResult(name, requestedPath, isSymlink, stats, mimeType, symlinkTarget);
|
|
71
|
+
}
|
|
72
|
+
function buildEmptyResult() {
|
|
73
|
+
return {
|
|
74
|
+
results: [],
|
|
75
|
+
summary: { total: 0, succeeded: 0, failed: 0, totalSize: 0 },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function processFileInfo(filePath, options) {
|
|
79
|
+
const info = await getFileInfo(filePath, options);
|
|
80
|
+
return { path: filePath, info };
|
|
81
|
+
}
|
|
82
|
+
function buildIndexedPathTasks(paths) {
|
|
83
|
+
const tasks = [];
|
|
84
|
+
for (let index = 0; index < paths.length; index += 1) {
|
|
85
|
+
const filePath = paths[index];
|
|
86
|
+
if (filePath === undefined)
|
|
87
|
+
continue;
|
|
88
|
+
tasks.push({ filePath, index });
|
|
89
|
+
}
|
|
90
|
+
return tasks;
|
|
91
|
+
}
|
|
92
|
+
async function readFileInfoInParallel(paths, options) {
|
|
93
|
+
return processInParallel(buildIndexedPathTasks(paths), async ({ filePath, index }) => {
|
|
94
|
+
const value = await processFileInfo(filePath, options);
|
|
95
|
+
options.onProgress?.();
|
|
96
|
+
return { index, value };
|
|
97
|
+
}, PARALLEL_CONCURRENCY, options.signal);
|
|
98
|
+
}
|
|
99
|
+
function calculateSummary(results) {
|
|
100
|
+
let succeeded = 0;
|
|
101
|
+
let failed = 0;
|
|
102
|
+
let totalSize = 0;
|
|
103
|
+
for (const result of results) {
|
|
104
|
+
if (result.info !== undefined) {
|
|
105
|
+
succeeded++;
|
|
106
|
+
totalSize += result.info.size;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
failed++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
total: results.length,
|
|
114
|
+
succeeded,
|
|
115
|
+
failed,
|
|
116
|
+
totalSize,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export async function getMultipleFileInfo(paths, options = {}) {
|
|
120
|
+
if (paths.length === 0)
|
|
121
|
+
return buildEmptyResult();
|
|
122
|
+
const output = new Array(paths.length);
|
|
123
|
+
for (let index = 0; index < paths.length; index += 1) {
|
|
124
|
+
output[index] = { path: paths[index] ?? UNKNOWN_PATH };
|
|
125
|
+
}
|
|
126
|
+
const { results, errors } = await readFileInfoInParallel(paths, options);
|
|
127
|
+
applyIndexedValues(output, results);
|
|
128
|
+
applyIndexedErrors({
|
|
129
|
+
output,
|
|
130
|
+
errors,
|
|
131
|
+
resolveIndex: (failureIndex) => failureIndex >= 0 && failureIndex < output.length
|
|
132
|
+
? failureIndex
|
|
133
|
+
: undefined,
|
|
134
|
+
buildValue: (resolvedIndex, error) => ({
|
|
135
|
+
path: paths[resolvedIndex] ?? UNKNOWN_PATH,
|
|
136
|
+
error: error.message,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
results: output,
|
|
141
|
+
summary: calculateSummary(output),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function normalizePattern(pattern) {
|
|
145
|
+
if (!pattern || pattern.length === 0)
|
|
146
|
+
return undefined;
|
|
147
|
+
return pattern;
|
|
148
|
+
}
|
|
149
|
+
function normalizeListOptions(options) {
|
|
150
|
+
const pattern = normalizePattern(options.pattern);
|
|
151
|
+
const normalized = {
|
|
152
|
+
includeHidden: options.includeHidden ?? false,
|
|
153
|
+
excludePatterns: options.excludePatterns ?? [],
|
|
154
|
+
maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
155
|
+
maxEntries: options.maxEntries ?? DEFAULT_LIST_MAX_ENTRIES,
|
|
156
|
+
sortBy: options.sortBy ?? 'name',
|
|
157
|
+
includeSymlinkTargets: options.includeSymlinkTargets ?? false,
|
|
158
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
|
|
159
|
+
...(pattern !== undefined ? { pattern } : {}),
|
|
160
|
+
};
|
|
161
|
+
return normalized;
|
|
162
|
+
}
|
|
163
|
+
function sortEntries(entries, sortBy) {
|
|
164
|
+
const compare = {
|
|
165
|
+
name: (a, b) => a.name.localeCompare(b.name),
|
|
166
|
+
type: (a, b) => a.type.localeCompare(b.type),
|
|
167
|
+
size: (a, b) => (a.size ?? 0) - (b.size ?? 0),
|
|
168
|
+
modified: (a, b) => (a.modified?.getTime() ?? 0) - (b.modified?.getTime() ?? 0),
|
|
169
|
+
}[sortBy];
|
|
170
|
+
entries.sort(compare);
|
|
171
|
+
}
|
|
172
|
+
function resolveMaxDepth(normalized) {
|
|
173
|
+
if (!normalized.pattern) {
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
return normalized.maxDepth;
|
|
177
|
+
}
|
|
178
|
+
async function* readDirectoryEntries(basePath, normalized, needsStats, signal) {
|
|
179
|
+
const dirents = await withAbort(fsp.readdir(basePath, { withFileTypes: true }), signal);
|
|
180
|
+
const entries = [];
|
|
181
|
+
for (const dirent of dirents) {
|
|
182
|
+
if (!normalized.includeHidden && isHidden(dirent.name)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
entries.push({ dirent, entryPath: path.join(basePath, dirent.name) });
|
|
186
|
+
}
|
|
187
|
+
if (!needsStats) {
|
|
188
|
+
for (const entry of entries) {
|
|
189
|
+
yield {
|
|
190
|
+
path: entry.entryPath,
|
|
191
|
+
dirent: entry.dirent,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const { results, errors } = await processInParallel(entries.map((_, index) => index), async (index) => {
|
|
197
|
+
const candidate = entries[index];
|
|
198
|
+
if (!candidate) {
|
|
199
|
+
throw new Error(`Entry index out of range: ${String(index)}`);
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
index,
|
|
203
|
+
stats: await withAbort(fsp.lstat(candidate.entryPath), signal),
|
|
204
|
+
};
|
|
205
|
+
}, PARALLEL_CONCURRENCY, signal);
|
|
206
|
+
if (errors.length > 0) {
|
|
207
|
+
throw errors[0]?.error ?? new Error('Failed to read entry stats');
|
|
208
|
+
}
|
|
209
|
+
const statsByIndex = [];
|
|
210
|
+
for (const result of results) {
|
|
211
|
+
statsByIndex[result.index] = result.stats;
|
|
212
|
+
}
|
|
213
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
214
|
+
const entry = entries[index];
|
|
215
|
+
if (!entry)
|
|
216
|
+
continue;
|
|
217
|
+
const stats = statsByIndex[index];
|
|
218
|
+
yield {
|
|
219
|
+
path: entry.entryPath,
|
|
220
|
+
dirent: entry.dirent,
|
|
221
|
+
...(stats ? { stats } : {}),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function createEntryStream(basePath, normalized, maxDepth, needsStats, signal) {
|
|
226
|
+
if (shouldUseFastPath(normalized, maxDepth)) {
|
|
227
|
+
return readDirectoryEntries(basePath, normalized, needsStats, signal);
|
|
228
|
+
}
|
|
229
|
+
return globEntries({
|
|
230
|
+
cwd: basePath,
|
|
231
|
+
pattern: normalized.pattern ?? '*',
|
|
232
|
+
excludePatterns: normalized.excludePatterns,
|
|
233
|
+
includeHidden: normalized.includeHidden,
|
|
234
|
+
baseNameMatch: false,
|
|
235
|
+
caseSensitiveMatch: true,
|
|
236
|
+
maxDepth,
|
|
237
|
+
followSymbolicLinks: false,
|
|
238
|
+
onlyFiles: false,
|
|
239
|
+
stats: needsStats,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function shouldUseFastPath(normalized, maxDepth) {
|
|
243
|
+
return (!normalized.pattern &&
|
|
244
|
+
normalized.excludePatterns.length === 0 &&
|
|
245
|
+
maxDepth === 1);
|
|
246
|
+
}
|
|
247
|
+
function resolveRelativePath(basePath, entryPath) {
|
|
248
|
+
return path.relative(basePath, entryPath) || path.basename(entryPath);
|
|
249
|
+
}
|
|
250
|
+
async function resolveSymlinkTarget(entryType, includeSymlinkTargets, entryPath) {
|
|
251
|
+
if (entryType !== 'symlink' || !includeSymlinkTargets) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
return fsp.readlink(entryPath).catch(() => undefined);
|
|
255
|
+
}
|
|
256
|
+
function updateTotals(entryType, totals) {
|
|
257
|
+
if (entryType === 'file')
|
|
258
|
+
totals.files += 1;
|
|
259
|
+
if (entryType === 'directory')
|
|
260
|
+
totals.directories += 1;
|
|
261
|
+
}
|
|
262
|
+
function buildDirectoryEntry(basePath, entry, entryType, needsStats, symlinkTarget) {
|
|
263
|
+
const size = needsStats && entry.stats?.isFile() ? entry.stats.size : undefined;
|
|
264
|
+
const modified = needsStats ? entry.stats?.mtime : undefined;
|
|
265
|
+
return {
|
|
266
|
+
name: path.basename(entry.path),
|
|
267
|
+
path: entry.path,
|
|
268
|
+
relativePath: resolveRelativePath(basePath, entry.path),
|
|
269
|
+
type: entryType,
|
|
270
|
+
...(size !== undefined ? { size } : {}),
|
|
271
|
+
...(modified !== undefined ? { modified } : {}),
|
|
272
|
+
...(symlinkTarget !== undefined ? { symlinkTarget } : {}),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function trackSymlink(entryType, includeSymlinkTargets, counters) {
|
|
276
|
+
if (entryType === 'symlink' && !includeSymlinkTargets) {
|
|
277
|
+
counters.symlinksNotFollowed += 1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function appendEntry(entry, entryType, symlinkTarget, ctx) {
|
|
281
|
+
updateTotals(entryType, ctx.totals);
|
|
282
|
+
ctx.entries.push(buildDirectoryEntry(ctx.basePath, entry, entryType, ctx.needsStats, symlinkTarget));
|
|
283
|
+
}
|
|
284
|
+
async function enqueueAppendEntry(entry, entryType, ctx, pending, flushPending) {
|
|
285
|
+
if (!ctx.includeSymlinkTargets) {
|
|
286
|
+
appendEntry(entry, entryType, undefined, ctx);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// Preserve the original behavior: resolve symlink targets in parallel
|
|
290
|
+
// when includeSymlinkTargets is enabled (bounded by PARALLEL_CONCURRENCY).
|
|
291
|
+
const task = (async () => {
|
|
292
|
+
const symlinkTarget = await resolveSymlinkTarget(entryType, ctx.includeSymlinkTargets, entry.path);
|
|
293
|
+
appendEntry(entry, entryType, symlinkTarget, ctx);
|
|
294
|
+
})();
|
|
295
|
+
pending.push(task);
|
|
296
|
+
if (pending.length >= PARALLEL_CONCURRENCY) {
|
|
297
|
+
await flushPending();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function buildSummary(entries, totals, maxDepth, truncated, stoppedReason, counters) {
|
|
301
|
+
const summary = {
|
|
302
|
+
totalEntries: entries.length,
|
|
303
|
+
entriesScanned: entries.length,
|
|
304
|
+
entriesVisible: entries.length,
|
|
305
|
+
totalFiles: totals.files,
|
|
306
|
+
totalDirectories: totals.directories,
|
|
307
|
+
maxDepthReached: maxDepth,
|
|
308
|
+
truncated,
|
|
309
|
+
skippedInaccessible: counters.skippedInaccessible,
|
|
310
|
+
symlinksNotFollowed: counters.symlinksNotFollowed,
|
|
311
|
+
};
|
|
312
|
+
return withOptionalStoppedReason(summary, stoppedReason);
|
|
313
|
+
}
|
|
314
|
+
async function collectEntries(basePath, normalized, signal, needsStats, maxDepth) {
|
|
315
|
+
const entries = [];
|
|
316
|
+
const totals = { files: 0, directories: 0 };
|
|
317
|
+
const counters = { skippedInaccessible: 0, symlinksNotFollowed: 0 };
|
|
318
|
+
const basePathDirectories = [basePath];
|
|
319
|
+
const accessDeps = {
|
|
320
|
+
normalizePath,
|
|
321
|
+
isPathWithinDirectories,
|
|
322
|
+
isSensitivePath,
|
|
323
|
+
validateSymlinkPath: validateExistingPathDetailed,
|
|
324
|
+
};
|
|
325
|
+
let truncated = false;
|
|
326
|
+
let stoppedReason;
|
|
327
|
+
const pending = [];
|
|
328
|
+
let acceptedCount = 0;
|
|
329
|
+
const stream = createEntryStream(basePath, normalized, maxDepth, needsStats, signal);
|
|
330
|
+
const flushPending = async () => {
|
|
331
|
+
if (pending.length === 0)
|
|
332
|
+
return;
|
|
333
|
+
await Promise.allSettled(pending.splice(0));
|
|
334
|
+
};
|
|
335
|
+
const appendCtx = {
|
|
336
|
+
basePath,
|
|
337
|
+
needsStats,
|
|
338
|
+
includeSymlinkTargets: normalized.includeSymlinkTargets,
|
|
339
|
+
totals,
|
|
340
|
+
entries,
|
|
341
|
+
};
|
|
342
|
+
for await (const entry of stream) {
|
|
343
|
+
const stopReason = resolveStopReason({
|
|
344
|
+
signal,
|
|
345
|
+
current: acceptedCount,
|
|
346
|
+
max: normalized.maxEntries,
|
|
347
|
+
abortedReason: 'aborted',
|
|
348
|
+
maxReason: 'maxEntries',
|
|
349
|
+
});
|
|
350
|
+
if (stopReason) {
|
|
351
|
+
truncated = true;
|
|
352
|
+
stoppedReason = stopReason;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
const entryType = resolveEntryType(entry.dirent);
|
|
356
|
+
trackSymlink(entryType, normalized.includeSymlinkTargets, counters);
|
|
357
|
+
const accessible = await isEntryAccessibleByType(entry.path, entryType, basePathDirectories, signal, accessDeps);
|
|
358
|
+
if (!accessible) {
|
|
359
|
+
counters.skippedInaccessible += 1;
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
acceptedCount += 1;
|
|
363
|
+
await enqueueAppendEntry(entry, entryType, appendCtx, pending, flushPending);
|
|
364
|
+
}
|
|
365
|
+
if (normalized.includeSymlinkTargets) {
|
|
366
|
+
await flushPending();
|
|
367
|
+
}
|
|
368
|
+
return { entries, totals, truncated, stoppedReason, counters };
|
|
369
|
+
}
|
|
370
|
+
async function executeListDirectory(basePath, normalized, signal) {
|
|
371
|
+
const needsStats = needsStatsForSort(normalized.sortBy);
|
|
372
|
+
const maxDepth = resolveMaxDepth(normalized);
|
|
373
|
+
const { entries, totals, truncated, stoppedReason, counters } = await collectEntries(basePath, normalized, signal, needsStats, maxDepth);
|
|
374
|
+
sortEntries(entries, normalized.sortBy);
|
|
375
|
+
return {
|
|
376
|
+
entries,
|
|
377
|
+
summary: buildSummary(entries, totals, maxDepth, truncated, stoppedReason, counters),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
export async function listDirectory(dirPath, options = {}) {
|
|
381
|
+
const normalized = normalizeListOptions(options);
|
|
382
|
+
return withTimedAbortSignal(options.signal, normalized.timeoutMs, async (signal) => {
|
|
383
|
+
const basePath = await validateExistingDirectory(dirPath, signal);
|
|
384
|
+
const { entries, summary } = await executeListDirectory(basePath, normalized, signal);
|
|
385
|
+
return { path: basePath, entries, summary };
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
function toSafeNonNegativeInt(value, fallback) {
|
|
389
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
390
|
+
return fallback;
|
|
391
|
+
const asInt = Math.floor(value);
|
|
392
|
+
return asInt >= 0 ? asInt : fallback;
|
|
393
|
+
}
|
|
394
|
+
function toSafePositiveInt(value, fallback) {
|
|
395
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
396
|
+
return fallback;
|
|
397
|
+
const asInt = Math.floor(value);
|
|
398
|
+
return asInt > 0 ? asInt : fallback;
|
|
399
|
+
}
|
|
400
|
+
function toSafeBoolean(value, fallback) {
|
|
401
|
+
if (typeof value !== 'boolean')
|
|
402
|
+
return fallback;
|
|
403
|
+
return value;
|
|
404
|
+
}
|
|
405
|
+
function normalizeTreeOptions(options) {
|
|
406
|
+
return {
|
|
407
|
+
maxDepth: toSafeNonNegativeInt(options.maxDepth, 5),
|
|
408
|
+
maxEntries: toSafeNonNegativeInt(options.maxEntries, 1000),
|
|
409
|
+
includeHidden: toSafeBoolean(options.includeHidden, false),
|
|
410
|
+
includeIgnored: toSafeBoolean(options.includeIgnored, false),
|
|
411
|
+
timeoutMs: toSafePositiveInt(options.timeoutMs, DEFAULT_SEARCH_TIMEOUT_MS),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function ensureParentNodes(rootNode, nodeByPath, relativePath) {
|
|
415
|
+
const normalized = toPosixPath(relativePath);
|
|
416
|
+
if (normalized.length === 0 || normalized === '.')
|
|
417
|
+
return rootNode;
|
|
418
|
+
const segments = [];
|
|
419
|
+
for (const segment of normalized.split('/')) {
|
|
420
|
+
if (segment.length > 0) {
|
|
421
|
+
segments.push(segment);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const parentSegmentCount = Math.max(0, segments.length - 1);
|
|
425
|
+
let current = rootNode;
|
|
426
|
+
let currentPath = '';
|
|
427
|
+
for (let index = 0; index < parentSegmentCount; index += 1) {
|
|
428
|
+
const segment = segments[index];
|
|
429
|
+
if (!segment)
|
|
430
|
+
continue;
|
|
431
|
+
currentPath =
|
|
432
|
+
currentPath.length === 0
|
|
433
|
+
? segment
|
|
434
|
+
: path.posix.join(currentPath, segment);
|
|
435
|
+
let child = nodeByPath.get(currentPath);
|
|
436
|
+
if (!child) {
|
|
437
|
+
child = {
|
|
438
|
+
name: segment,
|
|
439
|
+
type: 'directory',
|
|
440
|
+
relativePath: currentPath,
|
|
441
|
+
children: [],
|
|
442
|
+
};
|
|
443
|
+
nodeByPath.set(currentPath, child);
|
|
444
|
+
current.children ??= [];
|
|
445
|
+
current.children.push(child);
|
|
446
|
+
}
|
|
447
|
+
current = child;
|
|
448
|
+
}
|
|
449
|
+
return current;
|
|
450
|
+
}
|
|
451
|
+
function sortTree(node) {
|
|
452
|
+
if (!node.children)
|
|
453
|
+
return;
|
|
454
|
+
node.children.sort(compareTreeEntries);
|
|
455
|
+
for (const child of node.children) {
|
|
456
|
+
sortTree(child);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function compareTreeEntries(a, b) {
|
|
460
|
+
const diff = getTreeTypeRank(a.type) - getTreeTypeRank(b.type);
|
|
461
|
+
if (diff !== 0)
|
|
462
|
+
return diff;
|
|
463
|
+
return a.name.localeCompare(b.name);
|
|
464
|
+
}
|
|
465
|
+
function getTreeTypeRank(type) {
|
|
466
|
+
if (type === 'directory')
|
|
467
|
+
return 0;
|
|
468
|
+
if (type === 'file')
|
|
469
|
+
return 1;
|
|
470
|
+
return 2;
|
|
471
|
+
}
|
|
472
|
+
async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps) {
|
|
473
|
+
const type = resolveEntryType(entry.dirent);
|
|
474
|
+
const isAccessible = await isEntryAccessibleByType(entry.path, type, rootDirectories, signal, accessDeps);
|
|
475
|
+
if (!isAccessible) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
if (gitignoreMatcher &&
|
|
479
|
+
isIgnoredByGitignore(gitignoreMatcher, root, entry.path, {
|
|
480
|
+
isDirectory: type === 'directory',
|
|
481
|
+
})) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
const relative = path.relative(root, entry.path) || path.basename(entry.path);
|
|
485
|
+
const relativePosix = toPosixPath(relative);
|
|
486
|
+
const name = path.basename(entry.path);
|
|
487
|
+
return { type, relativePosix, name };
|
|
488
|
+
}
|
|
489
|
+
function upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent) {
|
|
490
|
+
const ensureDirectoryShape = (node) => {
|
|
491
|
+
if (node.type === 'directory') {
|
|
492
|
+
node.children ??= [];
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
delete node.children;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
const maybeUpdateType = (existing, nextType) => {
|
|
499
|
+
if (existing.type === nextType)
|
|
500
|
+
return;
|
|
501
|
+
const preservePopulatedDirectory = existing.type === 'directory' &&
|
|
502
|
+
Array.isArray(existing.children) &&
|
|
503
|
+
existing.children.length > 0;
|
|
504
|
+
if (preservePopulatedDirectory)
|
|
505
|
+
return;
|
|
506
|
+
existing.type = nextType;
|
|
507
|
+
ensureDirectoryShape(existing);
|
|
508
|
+
};
|
|
509
|
+
const attachChild = (child) => {
|
|
510
|
+
parent.children ??= [];
|
|
511
|
+
let seen = childPathIndexByParent.get(parent);
|
|
512
|
+
if (!seen) {
|
|
513
|
+
seen = new Set();
|
|
514
|
+
for (const entry of parent.children) {
|
|
515
|
+
seen.add(entry.relativePath);
|
|
516
|
+
}
|
|
517
|
+
childPathIndexByParent.set(parent, seen);
|
|
518
|
+
}
|
|
519
|
+
const key = child.relativePath;
|
|
520
|
+
if (seen.has(key))
|
|
521
|
+
return;
|
|
522
|
+
seen.add(key);
|
|
523
|
+
parent.children.push(child);
|
|
524
|
+
};
|
|
525
|
+
const existing = nodeByPath.get(resolved.relativePosix);
|
|
526
|
+
if (existing) {
|
|
527
|
+
// Avoid duplicate directory nodes when a child file is encountered before the directory entry.
|
|
528
|
+
// Prefer preserving an existing populated directory node over overwriting it.
|
|
529
|
+
maybeUpdateType(existing, resolved.type);
|
|
530
|
+
existing.name = resolved.name;
|
|
531
|
+
existing.relativePath = resolved.relativePosix;
|
|
532
|
+
ensureDirectoryShape(existing);
|
|
533
|
+
attachChild(existing);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const node = {
|
|
537
|
+
name: resolved.name,
|
|
538
|
+
type: resolved.type,
|
|
539
|
+
relativePath: resolved.relativePosix,
|
|
540
|
+
...(resolved.type === 'directory' ? { children: [] } : {}),
|
|
541
|
+
};
|
|
542
|
+
nodeByPath.set(resolved.relativePosix, node);
|
|
543
|
+
attachChild(node);
|
|
544
|
+
}
|
|
545
|
+
export function formatTreeAscii(tree) {
|
|
546
|
+
const lines = [];
|
|
547
|
+
const walk = (node, prefix, isLast, isRoot) => {
|
|
548
|
+
let connector = '';
|
|
549
|
+
let linePrefix = '';
|
|
550
|
+
if (!isRoot) {
|
|
551
|
+
connector = isLast ? '└── ' : '├── ';
|
|
552
|
+
linePrefix = prefix;
|
|
553
|
+
}
|
|
554
|
+
lines.push(`${linePrefix}${connector}${node.name}`);
|
|
555
|
+
if (!node.children || node.children.length === 0)
|
|
556
|
+
return;
|
|
557
|
+
let nextPrefix = '';
|
|
558
|
+
if (!isRoot) {
|
|
559
|
+
const continuation = isLast ? ' ' : '│ ';
|
|
560
|
+
nextPrefix = `${prefix}${continuation}`;
|
|
561
|
+
}
|
|
562
|
+
const count = node.children.length;
|
|
563
|
+
for (let index = 0; index < count; index += 1) {
|
|
564
|
+
const child = node.children[index];
|
|
565
|
+
if (child) {
|
|
566
|
+
walk(child, nextPrefix, index === count - 1, false);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
walk(tree, '', true, true);
|
|
571
|
+
return lines.join('\n');
|
|
572
|
+
}
|
|
573
|
+
export async function treeDirectory(dirPath, options = {}) {
|
|
574
|
+
const normalized = normalizeTreeOptions(options);
|
|
575
|
+
return withTimedAbortSignal(options.signal, normalized.timeoutMs, async (signal) => {
|
|
576
|
+
const root = await validateExistingDirectory(dirPath, signal);
|
|
577
|
+
const rootNormalized = normalizePath(root);
|
|
578
|
+
const rootDirectories = [rootNormalized];
|
|
579
|
+
const accessDeps = {
|
|
580
|
+
normalizePath,
|
|
581
|
+
isPathWithinDirectories,
|
|
582
|
+
isSensitivePath,
|
|
583
|
+
validateSymlinkPath: validateExistingPathDetailed,
|
|
584
|
+
};
|
|
585
|
+
const excludePatterns = normalized.includeIgnored
|
|
586
|
+
? []
|
|
587
|
+
: DEFAULT_EXCLUDE_PATTERNS;
|
|
588
|
+
const gitignoreMatcher = normalized.includeIgnored
|
|
589
|
+
? null
|
|
590
|
+
: await loadRootGitignore(root, signal);
|
|
591
|
+
const rootNode = {
|
|
592
|
+
name: path.basename(root) || root,
|
|
593
|
+
type: 'directory',
|
|
594
|
+
relativePath: '.',
|
|
595
|
+
children: [],
|
|
596
|
+
};
|
|
597
|
+
const nodeByPath = new Map();
|
|
598
|
+
const childPathIndexByParent = new WeakMap();
|
|
599
|
+
let totalEntries = 0;
|
|
600
|
+
let truncated = false;
|
|
601
|
+
const stream = globEntries({
|
|
602
|
+
cwd: root,
|
|
603
|
+
pattern: '**/*',
|
|
604
|
+
excludePatterns,
|
|
605
|
+
includeHidden: normalized.includeHidden,
|
|
606
|
+
baseNameMatch: false,
|
|
607
|
+
caseSensitiveMatch: true,
|
|
608
|
+
maxDepth: normalized.maxDepth,
|
|
609
|
+
followSymbolicLinks: false,
|
|
610
|
+
onlyFiles: false,
|
|
611
|
+
stats: false,
|
|
612
|
+
suppressErrors: true,
|
|
613
|
+
});
|
|
614
|
+
for await (const entry of stream) {
|
|
615
|
+
const stopReason = resolveStopReason({
|
|
616
|
+
signal,
|
|
617
|
+
current: totalEntries,
|
|
618
|
+
max: normalized.maxEntries,
|
|
619
|
+
abortedReason: 'aborted',
|
|
620
|
+
maxReason: 'maxEntries',
|
|
621
|
+
});
|
|
622
|
+
if (stopReason) {
|
|
623
|
+
truncated = true;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps);
|
|
627
|
+
if (!resolved) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const parent = ensureParentNodes(rootNode, nodeByPath, resolved.relativePosix);
|
|
631
|
+
upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent);
|
|
632
|
+
totalEntries += 1;
|
|
633
|
+
options.onProgress?.({ current: totalEntries });
|
|
634
|
+
}
|
|
635
|
+
sortTree(rootNode);
|
|
636
|
+
return {
|
|
637
|
+
root,
|
|
638
|
+
tree: rootNode,
|
|
639
|
+
truncated,
|
|
640
|
+
totalEntries,
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
function estimateReadSize(stats, maxSize) {
|
|
645
|
+
// `readFile`/`readFileWithStats` are always invoked with a `maxSize` cap, so the
|
|
646
|
+
// combined budget should reflect the maximum number of bytes we might actually read.
|
|
647
|
+
return Math.min(stats.size, maxSize);
|
|
648
|
+
}
|
|
649
|
+
function buildReadOptions(options) {
|
|
650
|
+
const readOptions = {
|
|
651
|
+
encoding: options.encoding,
|
|
652
|
+
maxSize: options.maxSize,
|
|
653
|
+
};
|
|
654
|
+
applyLineSelection(readOptions, options);
|
|
655
|
+
return readOptions;
|
|
656
|
+
}
|
|
657
|
+
function buildReadMultipleResult(filePath, result) {
|
|
658
|
+
const output = {
|
|
659
|
+
path: filePath,
|
|
660
|
+
content: result.content,
|
|
661
|
+
truncated: result.truncated,
|
|
662
|
+
readMode: result.readMode,
|
|
663
|
+
};
|
|
664
|
+
if (result.totalLines !== undefined)
|
|
665
|
+
output.totalLines = result.totalLines;
|
|
666
|
+
if (result.head !== undefined)
|
|
667
|
+
output.head = result.head;
|
|
668
|
+
if (result.startLine !== undefined)
|
|
669
|
+
output.startLine = result.startLine;
|
|
670
|
+
if (result.endLine !== undefined)
|
|
671
|
+
output.endLine = result.endLine;
|
|
672
|
+
if (result.linesRead !== undefined)
|
|
673
|
+
output.linesRead = result.linesRead;
|
|
674
|
+
if (result.hasMoreLines !== undefined) {
|
|
675
|
+
output.hasMoreLines = result.hasMoreLines;
|
|
676
|
+
}
|
|
677
|
+
return output;
|
|
678
|
+
}
|
|
679
|
+
async function readSingleFile(task, readOptions) {
|
|
680
|
+
const { filePath, index, validPath, stats } = task;
|
|
681
|
+
const result = validPath && stats
|
|
682
|
+
? await readFileWithStats(filePath, validPath, stats, readOptions)
|
|
683
|
+
: await readFile(filePath, readOptions);
|
|
684
|
+
return {
|
|
685
|
+
index,
|
|
686
|
+
value: buildReadMultipleResult(filePath, result),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
async function readFilesInParallel(filesToProcess, options, signal, onReadComplete) {
|
|
690
|
+
const readOptions = buildReadOptions(options);
|
|
691
|
+
if (signal) {
|
|
692
|
+
readOptions.signal = signal;
|
|
693
|
+
}
|
|
694
|
+
return processInParallel(filesToProcess, async (task) => {
|
|
695
|
+
const result = await readSingleFile(task, readOptions);
|
|
696
|
+
onReadComplete?.();
|
|
697
|
+
return result;
|
|
698
|
+
}, PARALLEL_CONCURRENCY, signal);
|
|
699
|
+
}
|
|
700
|
+
function normalizeReadMultipleOptions(options) {
|
|
701
|
+
const normalized = {
|
|
702
|
+
encoding: options.encoding ?? 'utf-8',
|
|
703
|
+
maxSize: Math.min(options.maxSize ?? MAX_TEXT_FILE_SIZE, MAX_TEXT_FILE_SIZE),
|
|
704
|
+
maxTotalSize: options.maxTotalSize ?? DEFAULT_READ_MANY_MAX_TOTAL_SIZE,
|
|
705
|
+
};
|
|
706
|
+
applyLineSelection(normalized, options);
|
|
707
|
+
return normalized;
|
|
708
|
+
}
|
|
709
|
+
function applyLineSelection(target, source) {
|
|
710
|
+
if (source.head !== undefined)
|
|
711
|
+
target.head = source.head;
|
|
712
|
+
if (source.startLine !== undefined)
|
|
713
|
+
target.startLine = source.startLine;
|
|
714
|
+
if (source.endLine !== undefined)
|
|
715
|
+
target.endLine = source.endLine;
|
|
716
|
+
}
|
|
717
|
+
function resolveListDirectoryNormalizedOptions(options) {
|
|
718
|
+
const { signal, ...rest } = options;
|
|
719
|
+
return {
|
|
720
|
+
normalized: normalizeReadMultipleOptions(rest),
|
|
721
|
+
...(signal ? { signal } : {}),
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
async function validateFile(filePath, index, signal) {
|
|
725
|
+
const validPath = await validateExistingPath(filePath, signal);
|
|
726
|
+
const stats = await withAbort(fsp.stat(validPath), signal);
|
|
727
|
+
return { filePath, index, validPath, stats };
|
|
728
|
+
}
|
|
729
|
+
function markRemainingSkipped(startIndex, total, skippedBudget) {
|
|
730
|
+
for (let index = startIndex; index < total; index += 1) {
|
|
731
|
+
skippedBudget.add(index);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function tryValidateFile(filePath, index, signal) {
|
|
735
|
+
try {
|
|
736
|
+
return await validateFile(filePath, index, signal);
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
async function validateBatch(tasks, signal) {
|
|
743
|
+
if (tasks.length === 0)
|
|
744
|
+
return new Map();
|
|
745
|
+
const { results } = await processInParallel(tasks, async (task) => tryValidateFile(task.filePath, task.index, signal), PARALLEL_CONCURRENCY, signal);
|
|
746
|
+
const infos = new Map();
|
|
747
|
+
for (const info of results) {
|
|
748
|
+
if (!info)
|
|
749
|
+
continue;
|
|
750
|
+
infos.set(info.index, info);
|
|
751
|
+
}
|
|
752
|
+
return infos;
|
|
753
|
+
}
|
|
754
|
+
function applyBudgetForRange(options) {
|
|
755
|
+
const { batchStart, batchEnd, totalFiles, maxTotalSize, maxSize, validated, skippedBudget, totalSize: startingTotalSize, } = options;
|
|
756
|
+
let totalSize = startingTotalSize;
|
|
757
|
+
for (let index = batchStart; index < batchEnd; index += 1) {
|
|
758
|
+
const info = validated.get(index);
|
|
759
|
+
if (!info)
|
|
760
|
+
continue;
|
|
761
|
+
const { exceeded, totalSize: nextTotalSize } = applyBudget(totalSize, estimateReadSize(info.stats, maxSize), maxTotalSize, index, totalFiles, skippedBudget);
|
|
762
|
+
if (exceeded) {
|
|
763
|
+
return { totalSize, exceeded: true };
|
|
764
|
+
}
|
|
765
|
+
totalSize = nextTotalSize;
|
|
766
|
+
}
|
|
767
|
+
return { totalSize, exceeded: false };
|
|
768
|
+
}
|
|
769
|
+
async function collectFileBudget(filePaths, maxTotalSize, maxSize, signal) {
|
|
770
|
+
const skippedBudget = new Set();
|
|
771
|
+
const validated = new Map();
|
|
772
|
+
let totalSize = 0;
|
|
773
|
+
const totalFiles = filePaths.length;
|
|
774
|
+
for (let batchStart = 0; batchStart < totalFiles; batchStart += PARALLEL_CONCURRENCY) {
|
|
775
|
+
const batchTasks = [];
|
|
776
|
+
const batchEnd = Math.min(batchStart + PARALLEL_CONCURRENCY, totalFiles);
|
|
777
|
+
for (let index = batchStart; index < batchEnd; index += 1) {
|
|
778
|
+
const filePath = filePaths[index];
|
|
779
|
+
if (!filePath)
|
|
780
|
+
continue;
|
|
781
|
+
if (validated.has(index))
|
|
782
|
+
continue;
|
|
783
|
+
batchTasks.push({ filePath, index });
|
|
784
|
+
}
|
|
785
|
+
const batchInfos = await validateBatch(batchTasks, signal);
|
|
786
|
+
for (const [index, info] of batchInfos) {
|
|
787
|
+
validated.set(index, info);
|
|
788
|
+
}
|
|
789
|
+
const budgetResult = applyBudgetForRange({
|
|
790
|
+
batchStart,
|
|
791
|
+
batchEnd,
|
|
792
|
+
totalFiles,
|
|
793
|
+
maxTotalSize,
|
|
794
|
+
maxSize,
|
|
795
|
+
validated,
|
|
796
|
+
skippedBudget,
|
|
797
|
+
totalSize,
|
|
798
|
+
});
|
|
799
|
+
const { exceeded, totalSize: nextTotalSize } = budgetResult;
|
|
800
|
+
if (exceeded) {
|
|
801
|
+
return { skippedBudget, validated };
|
|
802
|
+
}
|
|
803
|
+
totalSize = nextTotalSize;
|
|
804
|
+
}
|
|
805
|
+
return { skippedBudget, validated };
|
|
806
|
+
}
|
|
807
|
+
function buildOutput(filePaths) {
|
|
808
|
+
const output = new Array(filePaths.length);
|
|
809
|
+
for (let index = 0; index < filePaths.length; index += 1) {
|
|
810
|
+
output[index] = { path: filePaths[index] ?? UNKNOWN_PATH };
|
|
811
|
+
}
|
|
812
|
+
return output;
|
|
813
|
+
}
|
|
814
|
+
function resolveErrorOriginalIndex(failureIndex, filesToProcess, totalInputFiles) {
|
|
815
|
+
// processInParallel implementations vary: some return error indices relative to
|
|
816
|
+
// the submitted batch (filesToProcess), others may forward the task/index.
|
|
817
|
+
const batchIndex = filesToProcess[failureIndex]?.index;
|
|
818
|
+
if (typeof batchIndex === 'number' &&
|
|
819
|
+
batchIndex >= 0 &&
|
|
820
|
+
batchIndex < totalInputFiles) {
|
|
821
|
+
return batchIndex;
|
|
822
|
+
}
|
|
823
|
+
if (failureIndex >= 0 && failureIndex < totalInputFiles) {
|
|
824
|
+
return failureIndex;
|
|
825
|
+
}
|
|
826
|
+
return undefined;
|
|
827
|
+
}
|
|
828
|
+
function buildFilesToProcess(filePaths, validated, skippedBudget) {
|
|
829
|
+
const filesToProcess = [];
|
|
830
|
+
for (let index = 0; index < filePaths.length; index += 1) {
|
|
831
|
+
if (skippedBudget.has(index))
|
|
832
|
+
continue;
|
|
833
|
+
const filePath = filePaths[index];
|
|
834
|
+
if (!filePath)
|
|
835
|
+
continue;
|
|
836
|
+
const cached = validated.get(index);
|
|
837
|
+
if (cached) {
|
|
838
|
+
filesToProcess.push({
|
|
839
|
+
filePath,
|
|
840
|
+
index,
|
|
841
|
+
validPath: cached.validPath,
|
|
842
|
+
stats: cached.stats,
|
|
843
|
+
});
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
filesToProcess.push({ filePath, index });
|
|
847
|
+
}
|
|
848
|
+
return filesToProcess;
|
|
849
|
+
}
|
|
850
|
+
function applyBudget(totalSize, estimatedSize, maxTotalSize, index, totalFiles, skippedBudget) {
|
|
851
|
+
if (totalSize + estimatedSize > maxTotalSize) {
|
|
852
|
+
skippedBudget.add(index);
|
|
853
|
+
markRemainingSkipped(index + 1, totalFiles, skippedBudget);
|
|
854
|
+
return { totalSize, exceeded: true };
|
|
855
|
+
}
|
|
856
|
+
return { totalSize: totalSize + estimatedSize, exceeded: false };
|
|
857
|
+
}
|
|
858
|
+
function applySkippedBudget(output, skippedBudget, filePaths, maxTotalSize) {
|
|
859
|
+
for (const index of skippedBudget) {
|
|
860
|
+
const filePath = filePaths[index];
|
|
861
|
+
if (!filePath)
|
|
862
|
+
continue;
|
|
863
|
+
output[index] = {
|
|
864
|
+
path: filePath,
|
|
865
|
+
error: `Skipped: combined estimated read would exceed maxTotalSize (${maxTotalSize} bytes)`,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
export async function readMultipleFiles(filePaths, options = {}) {
|
|
870
|
+
if (filePaths.length === 0)
|
|
871
|
+
return [];
|
|
872
|
+
const { normalized, signal } = resolveListDirectoryNormalizedOptions(options);
|
|
873
|
+
const output = buildOutput(filePaths);
|
|
874
|
+
const { skippedBudget, validated } = await collectFileBudget(filePaths, normalized.maxTotalSize, normalized.maxSize, signal);
|
|
875
|
+
const filesToProcess = buildFilesToProcess(filePaths, validated, skippedBudget);
|
|
876
|
+
const { results, errors } = await readFilesInParallel(filesToProcess, normalized, signal, options.onReadComplete);
|
|
877
|
+
applyIndexedValues(output, results);
|
|
878
|
+
applyIndexedErrors({
|
|
879
|
+
output,
|
|
880
|
+
errors,
|
|
881
|
+
resolveIndex: (failureIndex) => resolveErrorOriginalIndex(failureIndex, filesToProcess, filePaths.length),
|
|
882
|
+
buildValue: (resolvedIndex, error) => ({
|
|
883
|
+
path: filePaths[resolvedIndex] ?? UNKNOWN_PATH,
|
|
884
|
+
error: error.message,
|
|
885
|
+
}),
|
|
886
|
+
});
|
|
887
|
+
applySkippedBudget(output, skippedBudget, filePaths, normalized.maxTotalSize);
|
|
888
|
+
return output;
|
|
889
|
+
}
|