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