@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
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
import * as fsp from 'node:fs/promises';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { DEFAULT_LIST_MAX_ENTRIES, DEFAULT_MAX_DEPTH, DEFAULT_SEARCH_TIMEOUT_MS, PARALLEL_CONCURRENCY, } from '../constants.js';
|
|
4
|
-
import { createTimedAbortSignal, isHidden, processInParallel, withAbort, } from '../fs-helpers.js';
|
|
5
|
-
import { isSensitivePath } from '../path-policy.js';
|
|
6
|
-
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
7
|
-
import { isEntryAccessibleByType, needsStatsForSort, resolveEntryType, resolveStopReason, withOptionalStoppedReason, } from './common.js';
|
|
8
|
-
import { globEntries } from './glob-engine.js';
|
|
9
|
-
function normalizePattern(pattern) {
|
|
10
|
-
if (!pattern || pattern.length === 0)
|
|
11
|
-
return undefined;
|
|
12
|
-
return pattern;
|
|
13
|
-
}
|
|
14
|
-
function normalizeOptions(options) {
|
|
15
|
-
const pattern = normalizePattern(options.pattern);
|
|
16
|
-
const normalized = {
|
|
17
|
-
includeHidden: options.includeHidden ?? false,
|
|
18
|
-
excludePatterns: options.excludePatterns ?? [],
|
|
19
|
-
maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
20
|
-
maxEntries: options.maxEntries ?? DEFAULT_LIST_MAX_ENTRIES,
|
|
21
|
-
sortBy: options.sortBy ?? 'name',
|
|
22
|
-
includeSymlinkTargets: options.includeSymlinkTargets ?? false,
|
|
23
|
-
timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
|
|
24
|
-
...(pattern !== undefined ? { pattern } : {}),
|
|
25
|
-
};
|
|
26
|
-
return normalized;
|
|
27
|
-
}
|
|
28
|
-
function sortEntries(entries, sortBy) {
|
|
29
|
-
const compare = {
|
|
30
|
-
name: (a, b) => a.name.localeCompare(b.name),
|
|
31
|
-
type: (a, b) => a.type.localeCompare(b.type),
|
|
32
|
-
size: (a, b) => (a.size ?? 0) - (b.size ?? 0),
|
|
33
|
-
modified: (a, b) => (a.modified?.getTime() ?? 0) - (b.modified?.getTime() ?? 0),
|
|
34
|
-
}[sortBy];
|
|
35
|
-
entries.sort(compare);
|
|
36
|
-
}
|
|
37
|
-
function resolveMaxDepth(normalized) {
|
|
38
|
-
if (!normalized.pattern) {
|
|
39
|
-
return 1;
|
|
40
|
-
}
|
|
41
|
-
return normalized.maxDepth;
|
|
42
|
-
}
|
|
43
|
-
async function* readDirectoryEntries(basePath, normalized, needsStats, signal) {
|
|
44
|
-
const dirents = await withAbort(fsp.readdir(basePath, { withFileTypes: true }), signal);
|
|
45
|
-
const entries = [];
|
|
46
|
-
for (const dirent of dirents) {
|
|
47
|
-
if (!normalized.includeHidden && isHidden(dirent.name)) {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
entries.push({ dirent, entryPath: path.join(basePath, dirent.name) });
|
|
51
|
-
}
|
|
52
|
-
if (!needsStats) {
|
|
53
|
-
for (const entry of entries) {
|
|
54
|
-
yield {
|
|
55
|
-
path: entry.entryPath,
|
|
56
|
-
dirent: entry.dirent,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const { results, errors } = await processInParallel(entries.map((_, index) => index), async (index) => {
|
|
62
|
-
const candidate = entries[index];
|
|
63
|
-
if (!candidate) {
|
|
64
|
-
throw new Error(`Entry index out of range: ${String(index)}`);
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
index,
|
|
68
|
-
stats: await withAbort(fsp.lstat(candidate.entryPath), signal),
|
|
69
|
-
};
|
|
70
|
-
}, PARALLEL_CONCURRENCY, signal);
|
|
71
|
-
if (errors.length > 0) {
|
|
72
|
-
throw errors[0]?.error ?? new Error('Failed to read entry stats');
|
|
73
|
-
}
|
|
74
|
-
const statsByIndex = [];
|
|
75
|
-
for (const result of results) {
|
|
76
|
-
statsByIndex[result.index] = result.stats;
|
|
77
|
-
}
|
|
78
|
-
for (let index = 0; index < entries.length; index += 1) {
|
|
79
|
-
const entry = entries[index];
|
|
80
|
-
if (!entry)
|
|
81
|
-
continue;
|
|
82
|
-
const stats = statsByIndex[index];
|
|
83
|
-
yield {
|
|
84
|
-
path: entry.entryPath,
|
|
85
|
-
dirent: entry.dirent,
|
|
86
|
-
...(stats ? { stats } : {}),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
function createEntryStream(basePath, normalized, maxDepth, needsStats, signal) {
|
|
91
|
-
if (shouldUseFastPath(normalized, maxDepth)) {
|
|
92
|
-
return readDirectoryEntries(basePath, normalized, needsStats, signal);
|
|
93
|
-
}
|
|
94
|
-
return globEntries({
|
|
95
|
-
cwd: basePath,
|
|
96
|
-
pattern: normalized.pattern ?? '*',
|
|
97
|
-
excludePatterns: normalized.excludePatterns,
|
|
98
|
-
includeHidden: normalized.includeHidden,
|
|
99
|
-
baseNameMatch: false,
|
|
100
|
-
caseSensitiveMatch: true,
|
|
101
|
-
maxDepth,
|
|
102
|
-
followSymbolicLinks: false,
|
|
103
|
-
onlyFiles: false,
|
|
104
|
-
stats: needsStats,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
function shouldUseFastPath(normalized, maxDepth) {
|
|
108
|
-
return (!normalized.pattern &&
|
|
109
|
-
normalized.excludePatterns.length === 0 &&
|
|
110
|
-
maxDepth === 1);
|
|
111
|
-
}
|
|
112
|
-
function resolveRelativePath(basePath, entryPath) {
|
|
113
|
-
return path.relative(basePath, entryPath) || path.basename(entryPath);
|
|
114
|
-
}
|
|
115
|
-
async function resolveSymlinkTarget(entryType, includeSymlinkTargets, entryPath) {
|
|
116
|
-
if (entryType !== 'symlink' || !includeSymlinkTargets) {
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
119
|
-
return fsp.readlink(entryPath).catch(() => undefined);
|
|
120
|
-
}
|
|
121
|
-
function updateTotals(entryType, totals) {
|
|
122
|
-
if (entryType === 'file')
|
|
123
|
-
totals.files += 1;
|
|
124
|
-
if (entryType === 'directory')
|
|
125
|
-
totals.directories += 1;
|
|
126
|
-
}
|
|
127
|
-
function buildDirectoryEntry(basePath, entry, entryType, needsStats, symlinkTarget) {
|
|
128
|
-
const size = needsStats && entry.stats?.isFile() ? entry.stats.size : undefined;
|
|
129
|
-
const modified = needsStats ? entry.stats?.mtime : undefined;
|
|
130
|
-
return {
|
|
131
|
-
name: path.basename(entry.path),
|
|
132
|
-
path: entry.path,
|
|
133
|
-
relativePath: resolveRelativePath(basePath, entry.path),
|
|
134
|
-
type: entryType,
|
|
135
|
-
...(size !== undefined ? { size } : {}),
|
|
136
|
-
...(modified !== undefined ? { modified } : {}),
|
|
137
|
-
...(symlinkTarget !== undefined ? { symlinkTarget } : {}),
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
function trackSymlink(entryType, includeSymlinkTargets, counters) {
|
|
141
|
-
if (entryType === 'symlink' && !includeSymlinkTargets) {
|
|
142
|
-
counters.symlinksNotFollowed += 1;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
function appendEntry(entry, entryType, symlinkTarget, ctx) {
|
|
146
|
-
updateTotals(entryType, ctx.totals);
|
|
147
|
-
ctx.entries.push(buildDirectoryEntry(ctx.basePath, entry, entryType, ctx.needsStats, symlinkTarget));
|
|
148
|
-
}
|
|
149
|
-
async function enqueueAppendEntry(entry, entryType, ctx, pending, flushPending) {
|
|
150
|
-
if (!ctx.includeSymlinkTargets) {
|
|
151
|
-
appendEntry(entry, entryType, undefined, ctx);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
// Preserve the original behavior: resolve symlink targets in parallel
|
|
155
|
-
// when includeSymlinkTargets is enabled (bounded by PARALLEL_CONCURRENCY).
|
|
156
|
-
const task = (async () => {
|
|
157
|
-
const symlinkTarget = await resolveSymlinkTarget(entryType, ctx.includeSymlinkTargets, entry.path);
|
|
158
|
-
appendEntry(entry, entryType, symlinkTarget, ctx);
|
|
159
|
-
})();
|
|
160
|
-
pending.push(task);
|
|
161
|
-
if (pending.length >= PARALLEL_CONCURRENCY) {
|
|
162
|
-
await flushPending();
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
function buildSummary(entries, totals, maxDepth, truncated, stoppedReason, counters) {
|
|
166
|
-
const summary = {
|
|
167
|
-
totalEntries: entries.length,
|
|
168
|
-
entriesScanned: entries.length,
|
|
169
|
-
entriesVisible: entries.length,
|
|
170
|
-
totalFiles: totals.files,
|
|
171
|
-
totalDirectories: totals.directories,
|
|
172
|
-
maxDepthReached: maxDepth,
|
|
173
|
-
truncated,
|
|
174
|
-
skippedInaccessible: counters.skippedInaccessible,
|
|
175
|
-
symlinksNotFollowed: counters.symlinksNotFollowed,
|
|
176
|
-
};
|
|
177
|
-
return withOptionalStoppedReason(summary, stoppedReason);
|
|
178
|
-
}
|
|
179
|
-
async function collectEntries(basePath, normalized, signal, needsStats, maxDepth) {
|
|
180
|
-
const entries = [];
|
|
181
|
-
const totals = { files: 0, directories: 0 };
|
|
182
|
-
const counters = { skippedInaccessible: 0, symlinksNotFollowed: 0 };
|
|
183
|
-
const basePathDirectories = [basePath];
|
|
184
|
-
const accessDeps = {
|
|
185
|
-
normalizePath,
|
|
186
|
-
isPathWithinDirectories,
|
|
187
|
-
isSensitivePath,
|
|
188
|
-
validateSymlinkPath: validateExistingPathDetailed,
|
|
189
|
-
};
|
|
190
|
-
let truncated = false;
|
|
191
|
-
let stoppedReason;
|
|
192
|
-
const pending = [];
|
|
193
|
-
let acceptedCount = 0;
|
|
194
|
-
const stream = createEntryStream(basePath, normalized, maxDepth, needsStats, signal);
|
|
195
|
-
const flushPending = async () => {
|
|
196
|
-
if (pending.length === 0)
|
|
197
|
-
return;
|
|
198
|
-
await Promise.allSettled(pending.splice(0));
|
|
199
|
-
};
|
|
200
|
-
const appendCtx = {
|
|
201
|
-
basePath,
|
|
202
|
-
needsStats,
|
|
203
|
-
includeSymlinkTargets: normalized.includeSymlinkTargets,
|
|
204
|
-
totals,
|
|
205
|
-
entries,
|
|
206
|
-
};
|
|
207
|
-
for await (const entry of stream) {
|
|
208
|
-
const stopReason = resolveStopReason({
|
|
209
|
-
signal,
|
|
210
|
-
current: acceptedCount,
|
|
211
|
-
max: normalized.maxEntries,
|
|
212
|
-
abortedReason: 'aborted',
|
|
213
|
-
maxReason: 'maxEntries',
|
|
214
|
-
});
|
|
215
|
-
if (stopReason) {
|
|
216
|
-
truncated = true;
|
|
217
|
-
stoppedReason = stopReason;
|
|
218
|
-
break;
|
|
219
|
-
}
|
|
220
|
-
const entryType = resolveEntryType(entry.dirent);
|
|
221
|
-
trackSymlink(entryType, normalized.includeSymlinkTargets, counters);
|
|
222
|
-
const accessible = await isEntryAccessibleByType(entry.path, entryType, basePathDirectories, signal, accessDeps);
|
|
223
|
-
if (!accessible) {
|
|
224
|
-
counters.skippedInaccessible += 1;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
acceptedCount += 1;
|
|
228
|
-
await enqueueAppendEntry(entry, entryType, appendCtx, pending, flushPending);
|
|
229
|
-
}
|
|
230
|
-
if (normalized.includeSymlinkTargets) {
|
|
231
|
-
await flushPending();
|
|
232
|
-
}
|
|
233
|
-
return { entries, totals, truncated, stoppedReason, counters };
|
|
234
|
-
}
|
|
235
|
-
async function executeListDirectory(basePath, normalized, signal) {
|
|
236
|
-
const needsStats = needsStatsForSort(normalized.sortBy);
|
|
237
|
-
const maxDepth = resolveMaxDepth(normalized);
|
|
238
|
-
const { entries, totals, truncated, stoppedReason, counters } = await collectEntries(basePath, normalized, signal, needsStats, maxDepth);
|
|
239
|
-
sortEntries(entries, normalized.sortBy);
|
|
240
|
-
return {
|
|
241
|
-
entries,
|
|
242
|
-
summary: buildSummary(entries, totals, maxDepth, truncated, stoppedReason, counters),
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
export async function listDirectory(dirPath, options = {}) {
|
|
246
|
-
const normalized = normalizeOptions(options);
|
|
247
|
-
const { signal, cleanup } = createTimedAbortSignal(options.signal, normalized.timeoutMs);
|
|
248
|
-
const basePath = await validateExistingDirectory(dirPath, signal);
|
|
249
|
-
try {
|
|
250
|
-
const { entries, summary } = await executeListDirectory(basePath, normalized, signal);
|
|
251
|
-
return { path: basePath, entries, summary };
|
|
252
|
-
}
|
|
253
|
-
finally {
|
|
254
|
-
cleanup();
|
|
255
|
-
}
|
|
256
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
interface ReadMultipleResult {
|
|
2
|
-
path: string;
|
|
3
|
-
content?: string;
|
|
4
|
-
truncated?: boolean;
|
|
5
|
-
totalLines?: number;
|
|
6
|
-
readMode?: 'full' | 'head' | 'range';
|
|
7
|
-
head?: number;
|
|
8
|
-
startLine?: number;
|
|
9
|
-
endLine?: number;
|
|
10
|
-
linesRead?: number;
|
|
11
|
-
hasMoreLines?: boolean;
|
|
12
|
-
error?: string;
|
|
13
|
-
}
|
|
14
|
-
interface ReadMultipleOptions {
|
|
15
|
-
encoding?: BufferEncoding;
|
|
16
|
-
maxSize?: number;
|
|
17
|
-
maxTotalSize?: number;
|
|
18
|
-
head?: number;
|
|
19
|
-
startLine?: number;
|
|
20
|
-
endLine?: number;
|
|
21
|
-
signal?: AbortSignal;
|
|
22
|
-
onReadComplete?: () => void;
|
|
23
|
-
}
|
|
24
|
-
export declare function readMultipleFiles(filePaths: readonly string[], options?: ReadMultipleOptions): Promise<ReadMultipleResult[]>;
|
|
25
|
-
export {};
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import * as fsp from 'node:fs/promises';
|
|
2
|
-
import { DEFAULT_READ_MANY_MAX_TOTAL_SIZE, MAX_TEXT_FILE_SIZE, PARALLEL_CONCURRENCY, } from '../constants.js';
|
|
3
|
-
import { processInParallel, readFile, readFileWithStats, withAbort, } from '../fs-helpers.js';
|
|
4
|
-
import { validateExistingPath } from '../path-validation.js';
|
|
5
|
-
import { applyIndexedErrors, applyIndexedValues } from './common.js';
|
|
6
|
-
const UNKNOWN_PATH = '(unknown)';
|
|
7
|
-
function estimateReadSize(stats, maxSize) {
|
|
8
|
-
// `readFile`/`readFileWithStats` are always invoked with a `maxSize` cap, so the
|
|
9
|
-
// combined budget should reflect the maximum number of bytes we might actually read.
|
|
10
|
-
return Math.min(stats.size, maxSize);
|
|
11
|
-
}
|
|
12
|
-
function buildReadOptions(options) {
|
|
13
|
-
const readOptions = {
|
|
14
|
-
encoding: options.encoding,
|
|
15
|
-
maxSize: options.maxSize,
|
|
16
|
-
};
|
|
17
|
-
applyLineSelection(readOptions, options);
|
|
18
|
-
return readOptions;
|
|
19
|
-
}
|
|
20
|
-
function buildReadMultipleResult(filePath, result) {
|
|
21
|
-
const output = {
|
|
22
|
-
path: filePath,
|
|
23
|
-
content: result.content,
|
|
24
|
-
truncated: result.truncated,
|
|
25
|
-
readMode: result.readMode,
|
|
26
|
-
};
|
|
27
|
-
if (result.totalLines !== undefined)
|
|
28
|
-
output.totalLines = result.totalLines;
|
|
29
|
-
if (result.head !== undefined)
|
|
30
|
-
output.head = result.head;
|
|
31
|
-
if (result.startLine !== undefined)
|
|
32
|
-
output.startLine = result.startLine;
|
|
33
|
-
if (result.endLine !== undefined)
|
|
34
|
-
output.endLine = result.endLine;
|
|
35
|
-
if (result.linesRead !== undefined)
|
|
36
|
-
output.linesRead = result.linesRead;
|
|
37
|
-
if (result.hasMoreLines !== undefined) {
|
|
38
|
-
output.hasMoreLines = result.hasMoreLines;
|
|
39
|
-
}
|
|
40
|
-
return output;
|
|
41
|
-
}
|
|
42
|
-
async function readSingleFile(task, readOptions) {
|
|
43
|
-
const { filePath, index, validPath, stats } = task;
|
|
44
|
-
const result = validPath && stats
|
|
45
|
-
? await readFileWithStats(filePath, validPath, stats, readOptions)
|
|
46
|
-
: await readFile(filePath, readOptions);
|
|
47
|
-
return {
|
|
48
|
-
index,
|
|
49
|
-
value: buildReadMultipleResult(filePath, result),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
async function readFilesInParallel(filesToProcess, options, signal, onReadComplete) {
|
|
53
|
-
const readOptions = buildReadOptions(options);
|
|
54
|
-
if (signal) {
|
|
55
|
-
readOptions.signal = signal;
|
|
56
|
-
}
|
|
57
|
-
return processInParallel(filesToProcess, async (task) => {
|
|
58
|
-
const result = await readSingleFile(task, readOptions);
|
|
59
|
-
onReadComplete?.();
|
|
60
|
-
return result;
|
|
61
|
-
}, PARALLEL_CONCURRENCY, signal);
|
|
62
|
-
}
|
|
63
|
-
function normalizeReadMultipleOptions(options) {
|
|
64
|
-
const normalized = {
|
|
65
|
-
encoding: options.encoding ?? 'utf-8',
|
|
66
|
-
maxSize: Math.min(options.maxSize ?? MAX_TEXT_FILE_SIZE, MAX_TEXT_FILE_SIZE),
|
|
67
|
-
maxTotalSize: options.maxTotalSize ?? DEFAULT_READ_MANY_MAX_TOTAL_SIZE,
|
|
68
|
-
};
|
|
69
|
-
applyLineSelection(normalized, options);
|
|
70
|
-
return normalized;
|
|
71
|
-
}
|
|
72
|
-
function applyLineSelection(target, source) {
|
|
73
|
-
if (source.head !== undefined)
|
|
74
|
-
target.head = source.head;
|
|
75
|
-
if (source.startLine !== undefined)
|
|
76
|
-
target.startLine = source.startLine;
|
|
77
|
-
if (source.endLine !== undefined)
|
|
78
|
-
target.endLine = source.endLine;
|
|
79
|
-
}
|
|
80
|
-
function resolveNormalizedOptions(options) {
|
|
81
|
-
const { signal, ...rest } = options;
|
|
82
|
-
return {
|
|
83
|
-
normalized: normalizeReadMultipleOptions(rest),
|
|
84
|
-
...(signal ? { signal } : {}),
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
async function validateFile(filePath, index, signal) {
|
|
88
|
-
const validPath = await validateExistingPath(filePath, signal);
|
|
89
|
-
const stats = await withAbort(fsp.stat(validPath), signal);
|
|
90
|
-
return { filePath, index, validPath, stats };
|
|
91
|
-
}
|
|
92
|
-
function markRemainingSkipped(startIndex, total, skippedBudget) {
|
|
93
|
-
for (let index = startIndex; index < total; index += 1) {
|
|
94
|
-
skippedBudget.add(index);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
async function tryValidateFile(filePath, index, signal) {
|
|
98
|
-
try {
|
|
99
|
-
return await validateFile(filePath, index, signal);
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
async function validateBatch(tasks, signal) {
|
|
106
|
-
if (tasks.length === 0)
|
|
107
|
-
return new Map();
|
|
108
|
-
const { results } = await processInParallel(tasks, async (task) => tryValidateFile(task.filePath, task.index, signal), PARALLEL_CONCURRENCY, signal);
|
|
109
|
-
const infos = new Map();
|
|
110
|
-
for (const info of results) {
|
|
111
|
-
if (!info)
|
|
112
|
-
continue;
|
|
113
|
-
infos.set(info.index, info);
|
|
114
|
-
}
|
|
115
|
-
return infos;
|
|
116
|
-
}
|
|
117
|
-
function applyBudgetForRange(options) {
|
|
118
|
-
const { batchStart, batchEnd, totalFiles, maxTotalSize, maxSize, validated, skippedBudget, totalSize: startingTotalSize, } = options;
|
|
119
|
-
let totalSize = startingTotalSize;
|
|
120
|
-
for (let index = batchStart; index < batchEnd; index += 1) {
|
|
121
|
-
const info = validated.get(index);
|
|
122
|
-
if (!info)
|
|
123
|
-
continue;
|
|
124
|
-
const { exceeded, totalSize: nextTotalSize } = applyBudget(totalSize, estimateReadSize(info.stats, maxSize), maxTotalSize, index, totalFiles, skippedBudget);
|
|
125
|
-
if (exceeded) {
|
|
126
|
-
return { totalSize, exceeded: true };
|
|
127
|
-
}
|
|
128
|
-
totalSize = nextTotalSize;
|
|
129
|
-
}
|
|
130
|
-
return { totalSize, exceeded: false };
|
|
131
|
-
}
|
|
132
|
-
async function collectFileBudget(filePaths, maxTotalSize, maxSize, signal) {
|
|
133
|
-
const skippedBudget = new Set();
|
|
134
|
-
const validated = new Map();
|
|
135
|
-
let totalSize = 0;
|
|
136
|
-
const totalFiles = filePaths.length;
|
|
137
|
-
for (let batchStart = 0; batchStart < totalFiles; batchStart += PARALLEL_CONCURRENCY) {
|
|
138
|
-
const batchTasks = [];
|
|
139
|
-
const batchEnd = Math.min(batchStart + PARALLEL_CONCURRENCY, totalFiles);
|
|
140
|
-
for (let index = batchStart; index < batchEnd; index += 1) {
|
|
141
|
-
const filePath = filePaths[index];
|
|
142
|
-
if (!filePath)
|
|
143
|
-
continue;
|
|
144
|
-
if (validated.has(index))
|
|
145
|
-
continue;
|
|
146
|
-
batchTasks.push({ filePath, index });
|
|
147
|
-
}
|
|
148
|
-
const batchInfos = await validateBatch(batchTasks, signal);
|
|
149
|
-
for (const [index, info] of batchInfos) {
|
|
150
|
-
validated.set(index, info);
|
|
151
|
-
}
|
|
152
|
-
const budgetResult = applyBudgetForRange({
|
|
153
|
-
batchStart,
|
|
154
|
-
batchEnd,
|
|
155
|
-
totalFiles,
|
|
156
|
-
maxTotalSize,
|
|
157
|
-
maxSize,
|
|
158
|
-
validated,
|
|
159
|
-
skippedBudget,
|
|
160
|
-
totalSize,
|
|
161
|
-
});
|
|
162
|
-
const { exceeded, totalSize: nextTotalSize } = budgetResult;
|
|
163
|
-
if (exceeded) {
|
|
164
|
-
return { skippedBudget, validated };
|
|
165
|
-
}
|
|
166
|
-
totalSize = nextTotalSize;
|
|
167
|
-
}
|
|
168
|
-
return { skippedBudget, validated };
|
|
169
|
-
}
|
|
170
|
-
function buildOutput(filePaths) {
|
|
171
|
-
const output = new Array(filePaths.length);
|
|
172
|
-
for (let index = 0; index < filePaths.length; index += 1) {
|
|
173
|
-
output[index] = { path: filePaths[index] ?? UNKNOWN_PATH };
|
|
174
|
-
}
|
|
175
|
-
return output;
|
|
176
|
-
}
|
|
177
|
-
function resolveErrorOriginalIndex(failureIndex, filesToProcess, totalInputFiles) {
|
|
178
|
-
// processInParallel implementations vary: some return error indices relative to
|
|
179
|
-
// the submitted batch (filesToProcess), others may forward the task/index.
|
|
180
|
-
const batchIndex = filesToProcess[failureIndex]?.index;
|
|
181
|
-
if (typeof batchIndex === 'number' &&
|
|
182
|
-
batchIndex >= 0 &&
|
|
183
|
-
batchIndex < totalInputFiles) {
|
|
184
|
-
return batchIndex;
|
|
185
|
-
}
|
|
186
|
-
if (failureIndex >= 0 && failureIndex < totalInputFiles) {
|
|
187
|
-
return failureIndex;
|
|
188
|
-
}
|
|
189
|
-
return undefined;
|
|
190
|
-
}
|
|
191
|
-
function buildFilesToProcess(filePaths, validated, skippedBudget) {
|
|
192
|
-
const filesToProcess = [];
|
|
193
|
-
for (let index = 0; index < filePaths.length; index += 1) {
|
|
194
|
-
if (skippedBudget.has(index))
|
|
195
|
-
continue;
|
|
196
|
-
const filePath = filePaths[index];
|
|
197
|
-
if (!filePath)
|
|
198
|
-
continue;
|
|
199
|
-
const cached = validated.get(index);
|
|
200
|
-
if (cached) {
|
|
201
|
-
filesToProcess.push({
|
|
202
|
-
filePath,
|
|
203
|
-
index,
|
|
204
|
-
validPath: cached.validPath,
|
|
205
|
-
stats: cached.stats,
|
|
206
|
-
});
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
filesToProcess.push({ filePath, index });
|
|
210
|
-
}
|
|
211
|
-
return filesToProcess;
|
|
212
|
-
}
|
|
213
|
-
function applyBudget(totalSize, estimatedSize, maxTotalSize, index, totalFiles, skippedBudget) {
|
|
214
|
-
if (totalSize + estimatedSize > maxTotalSize) {
|
|
215
|
-
skippedBudget.add(index);
|
|
216
|
-
markRemainingSkipped(index + 1, totalFiles, skippedBudget);
|
|
217
|
-
return { totalSize, exceeded: true };
|
|
218
|
-
}
|
|
219
|
-
return { totalSize: totalSize + estimatedSize, exceeded: false };
|
|
220
|
-
}
|
|
221
|
-
function applySkippedBudget(output, skippedBudget, filePaths, maxTotalSize) {
|
|
222
|
-
for (const index of skippedBudget) {
|
|
223
|
-
const filePath = filePaths[index];
|
|
224
|
-
if (!filePath)
|
|
225
|
-
continue;
|
|
226
|
-
output[index] = {
|
|
227
|
-
path: filePath,
|
|
228
|
-
error: `Skipped: combined estimated read would exceed maxTotalSize (${maxTotalSize} bytes)`,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
export async function readMultipleFiles(filePaths, options = {}) {
|
|
233
|
-
if (filePaths.length === 0)
|
|
234
|
-
return [];
|
|
235
|
-
const { normalized, signal } = resolveNormalizedOptions(options);
|
|
236
|
-
const output = buildOutput(filePaths);
|
|
237
|
-
const { skippedBudget, validated } = await collectFileBudget(filePaths, normalized.maxTotalSize, normalized.maxSize, signal);
|
|
238
|
-
const filesToProcess = buildFilesToProcess(filePaths, validated, skippedBudget);
|
|
239
|
-
const { results, errors } = await readFilesInParallel(filesToProcess, normalized, signal, options.onReadComplete);
|
|
240
|
-
applyIndexedValues(output, results);
|
|
241
|
-
applyIndexedErrors({
|
|
242
|
-
output,
|
|
243
|
-
errors,
|
|
244
|
-
resolveIndex: (failureIndex) => resolveErrorOriginalIndex(failureIndex, filesToProcess, filePaths.length),
|
|
245
|
-
buildValue: (resolvedIndex, error) => ({
|
|
246
|
-
path: filePaths[resolvedIndex] ?? UNKNOWN_PATH,
|
|
247
|
-
error: error.message,
|
|
248
|
-
}),
|
|
249
|
-
});
|
|
250
|
-
applySkippedBudget(output, skippedBudget, filePaths, normalized.maxTotalSize);
|
|
251
|
-
return output;
|
|
252
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { SearchFilesResult } from '../../config.js';
|
|
2
|
-
type SortBy = 'name' | 'size' | 'modified' | 'path';
|
|
3
|
-
interface SearchFilesOptions {
|
|
4
|
-
maxResults?: number;
|
|
5
|
-
sortBy?: SortBy;
|
|
6
|
-
maxDepth?: number;
|
|
7
|
-
maxFilesScanned?: number;
|
|
8
|
-
timeoutMs?: number;
|
|
9
|
-
baseNameMatch?: boolean;
|
|
10
|
-
skipSymlinks?: boolean;
|
|
11
|
-
includeHidden?: boolean;
|
|
12
|
-
respectGitignore?: boolean;
|
|
13
|
-
signal?: AbortSignal;
|
|
14
|
-
onProgress?: (progress: {
|
|
15
|
-
total?: number;
|
|
16
|
-
current: number;
|
|
17
|
-
}) => void;
|
|
18
|
-
}
|
|
19
|
-
interface Sortable {
|
|
20
|
-
name?: string;
|
|
21
|
-
size?: number;
|
|
22
|
-
modified?: Date;
|
|
23
|
-
path?: string;
|
|
24
|
-
}
|
|
25
|
-
export declare function sortSearchResults(results: Sortable[], sortBy: SortBy): void;
|
|
26
|
-
export declare function searchFiles(basePath: string, pattern: string, excludePatterns?: readonly string[], options?: SearchFilesOptions): Promise<SearchFilesResult>;
|
|
27
|
-
export {};
|