@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,218 +0,0 @@
|
|
|
1
|
-
import * as path from 'node:path';
|
|
2
|
-
import { DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, } from '../constants.js';
|
|
3
|
-
import { createTimedAbortSignal } from '../fs-helpers.js';
|
|
4
|
-
import { isSensitivePath } from '../path-policy.js';
|
|
5
|
-
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
6
|
-
import { compareOptionalNumberDesc, compareStringValues, isEntryAccessibleByType, needsStatsForSort, resolveEntryType, resolveStopReason, stableSortByDerivedString, withOptionalStoppedReason, } from './common.js';
|
|
7
|
-
import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
|
|
8
|
-
import { globEntries } from './glob-engine.js';
|
|
9
|
-
// Internal default for find tool - not exposed to MCP users
|
|
10
|
-
const INTERNAL_MAX_RESULTS = 1000;
|
|
11
|
-
function normalizeOptions(options) {
|
|
12
|
-
const normalized = {
|
|
13
|
-
maxResults: options.maxResults ?? INTERNAL_MAX_RESULTS,
|
|
14
|
-
sortBy: options.sortBy ?? 'path',
|
|
15
|
-
maxFilesScanned: options.maxFilesScanned ?? DEFAULT_SEARCH_MAX_FILES,
|
|
16
|
-
timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
|
|
17
|
-
baseNameMatch: options.baseNameMatch ?? false,
|
|
18
|
-
skipSymlinks: options.skipSymlinks ?? true,
|
|
19
|
-
includeHidden: options.includeHidden ?? false,
|
|
20
|
-
respectGitignore: options.respectGitignore ?? false,
|
|
21
|
-
};
|
|
22
|
-
if (options.maxDepth !== undefined) {
|
|
23
|
-
normalized.maxDepth = options.maxDepth;
|
|
24
|
-
}
|
|
25
|
-
return normalized;
|
|
26
|
-
}
|
|
27
|
-
function buildSearchResult(entry, entryType, needsStats) {
|
|
28
|
-
let resolvedType = 'other';
|
|
29
|
-
if (entryType === 'directory') {
|
|
30
|
-
resolvedType = 'directory';
|
|
31
|
-
}
|
|
32
|
-
else if (entryType === 'file') {
|
|
33
|
-
resolvedType = 'file';
|
|
34
|
-
}
|
|
35
|
-
const size = needsStats && entry.stats?.isFile() ? entry.stats.size : undefined;
|
|
36
|
-
const modified = needsStats ? entry.stats?.mtime : undefined;
|
|
37
|
-
return {
|
|
38
|
-
path: entry.path,
|
|
39
|
-
type: resolvedType,
|
|
40
|
-
...(size !== undefined ? { size } : {}),
|
|
41
|
-
...(modified !== undefined ? { modified } : {}),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
function shouldStopCollecting(state, normalized, signal) {
|
|
45
|
-
const stopReason = resolveStopReason({
|
|
46
|
-
signal,
|
|
47
|
-
current: state.filesScanned,
|
|
48
|
-
max: normalized.maxFilesScanned,
|
|
49
|
-
abortedReason: 'timeout',
|
|
50
|
-
maxReason: 'maxFiles',
|
|
51
|
-
});
|
|
52
|
-
if (stopReason !== undefined) {
|
|
53
|
-
state.truncated = true;
|
|
54
|
-
state.stoppedReason = stopReason;
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
function shouldIncludeEntry(entryType, normalized) {
|
|
60
|
-
return !normalized.skipSymlinks || entryType !== 'symlink';
|
|
61
|
-
}
|
|
62
|
-
function createCollectState() {
|
|
63
|
-
return {
|
|
64
|
-
results: [],
|
|
65
|
-
filesScanned: 0,
|
|
66
|
-
truncated: false,
|
|
67
|
-
skippedInaccessible: 0,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
function buildSearchStream(root, pattern, excludePatterns, normalized, needsStats) {
|
|
71
|
-
const options = {
|
|
72
|
-
cwd: root,
|
|
73
|
-
pattern,
|
|
74
|
-
excludePatterns,
|
|
75
|
-
includeHidden: normalized.includeHidden,
|
|
76
|
-
baseNameMatch: normalized.baseNameMatch,
|
|
77
|
-
caseSensitiveMatch: true,
|
|
78
|
-
followSymbolicLinks: false,
|
|
79
|
-
onlyFiles: true,
|
|
80
|
-
stats: needsStats,
|
|
81
|
-
};
|
|
82
|
-
if (normalized.maxDepth !== undefined) {
|
|
83
|
-
options.maxDepth = normalized.maxDepth;
|
|
84
|
-
}
|
|
85
|
-
return globEntries(options);
|
|
86
|
-
}
|
|
87
|
-
function buildCollectResult(state) {
|
|
88
|
-
const outcome = {
|
|
89
|
-
results: state.results,
|
|
90
|
-
filesScanned: state.filesScanned,
|
|
91
|
-
truncated: state.truncated,
|
|
92
|
-
skippedInaccessible: state.skippedInaccessible,
|
|
93
|
-
};
|
|
94
|
-
if (state.stoppedReason !== undefined) {
|
|
95
|
-
outcome.stoppedReason = state.stoppedReason;
|
|
96
|
-
}
|
|
97
|
-
return outcome;
|
|
98
|
-
}
|
|
99
|
-
function handleEntry(entry, entryType, needsStats, normalized, state) {
|
|
100
|
-
state.results.push(buildSearchResult(entry, entryType, needsStats));
|
|
101
|
-
if (state.results.length >= normalized.maxResults) {
|
|
102
|
-
state.truncated = true;
|
|
103
|
-
state.stoppedReason = 'maxResults';
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function reportSearchFilesProgress(onProgress, current, total, force = false) {
|
|
107
|
-
if (!onProgress || current === 0)
|
|
108
|
-
return;
|
|
109
|
-
if (!force && current % 25 !== 0)
|
|
110
|
-
return;
|
|
111
|
-
onProgress({ current, total });
|
|
112
|
-
}
|
|
113
|
-
async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress) {
|
|
114
|
-
for await (const entry of stream) {
|
|
115
|
-
if (shouldStopCollecting(state, normalized, signal))
|
|
116
|
-
break;
|
|
117
|
-
state.filesScanned++;
|
|
118
|
-
reportSearchFilesProgress(onProgress, state.filesScanned, normalized.maxFilesScanned);
|
|
119
|
-
if (isEntryIgnoredByGitignore(gitignoreMatcher, root, entry.path, entry.relativePath)) {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const entryType = resolveEntryType(entry.dirent);
|
|
123
|
-
if (!shouldIncludeEntry(entryType, normalized)) {
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
const isAccessible = await isEntryAccessibleByType(entry.path, entryType, rootDirectories, signal, accessDeps);
|
|
127
|
-
if (!isAccessible) {
|
|
128
|
-
state.skippedInaccessible++;
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
handleEntry(entry, entryType, needsStats, normalized, state);
|
|
132
|
-
if (state.truncated)
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
reportSearchFilesProgress(onProgress, state.filesScanned, normalized.maxFilesScanned, true);
|
|
136
|
-
}
|
|
137
|
-
function isEntryIgnoredByGitignore(matcher, root, entryPath, relativePath) {
|
|
138
|
-
if (!matcher)
|
|
139
|
-
return false;
|
|
140
|
-
return isIgnoredByGitignore(matcher, root, entryPath, relativePath ? { relativePath } : {});
|
|
141
|
-
}
|
|
142
|
-
async function collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress) {
|
|
143
|
-
const needsStats = needsStatsForSort(normalized.sortBy);
|
|
144
|
-
const stream = buildSearchStream(root, pattern, excludePatterns, normalized, needsStats);
|
|
145
|
-
const state = createCollectState();
|
|
146
|
-
const rootDirectories = [root];
|
|
147
|
-
const accessDeps = {
|
|
148
|
-
normalizePath,
|
|
149
|
-
isPathWithinDirectories,
|
|
150
|
-
isSensitivePath,
|
|
151
|
-
validateSymlinkPath: validateExistingPathDetailed,
|
|
152
|
-
};
|
|
153
|
-
const gitignoreMatcher = normalized.respectGitignore
|
|
154
|
-
? await loadRootGitignore(root, signal)
|
|
155
|
-
: null;
|
|
156
|
-
await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress);
|
|
157
|
-
return buildCollectResult(state);
|
|
158
|
-
}
|
|
159
|
-
function buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible) {
|
|
160
|
-
const summary = {
|
|
161
|
-
matched: results.length,
|
|
162
|
-
truncated,
|
|
163
|
-
skippedInaccessible,
|
|
164
|
-
filesScanned,
|
|
165
|
-
};
|
|
166
|
-
return withOptionalStoppedReason(summary, stoppedReason);
|
|
167
|
-
}
|
|
168
|
-
function compareNameThenPath(a, b) {
|
|
169
|
-
const nameCompare = compareStringValues(a.name, b.name);
|
|
170
|
-
if (nameCompare !== 0)
|
|
171
|
-
return nameCompare;
|
|
172
|
-
return compareStringValues(a.path, b.path);
|
|
173
|
-
}
|
|
174
|
-
function comparePathThenName(a, b) {
|
|
175
|
-
const pathCompare = compareStringValues(a.path, b.path);
|
|
176
|
-
if (pathCompare !== 0)
|
|
177
|
-
return pathCompare;
|
|
178
|
-
return compareStringValues(a.name, b.name);
|
|
179
|
-
}
|
|
180
|
-
const SORT_COMPARATORS = {
|
|
181
|
-
size: (a, b) => compareOptionalNumberDesc(a.size, b.size, () => compareNameThenPath(a, b)),
|
|
182
|
-
modified: (a, b) => compareOptionalNumberDesc(a.modified?.getTime(), b.modified?.getTime(), () => compareNameThenPath(a, b)),
|
|
183
|
-
path: (a, b) => comparePathThenName(a, b),
|
|
184
|
-
name: (a, b) => compareNameThenPath(a, b),
|
|
185
|
-
};
|
|
186
|
-
export function sortSearchResults(results, sortBy) {
|
|
187
|
-
if (sortBy === 'name') {
|
|
188
|
-
stableSortByDerivedString(results, (item) => path.basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const comparator = SORT_COMPARATORS[sortBy];
|
|
192
|
-
results.sort(comparator);
|
|
193
|
-
}
|
|
194
|
-
async function runSearchFiles(root, pattern, excludePatterns, normalized, signal, onProgress) {
|
|
195
|
-
const { results, filesScanned, truncated, stoppedReason, skippedInaccessible, } = await collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress);
|
|
196
|
-
sortSearchResults(results, normalized.sortBy);
|
|
197
|
-
return {
|
|
198
|
-
results,
|
|
199
|
-
summary: buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible),
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
export async function searchFiles(basePath, pattern, excludePatterns = [], options = {}) {
|
|
203
|
-
const normalized = normalizeOptions(options);
|
|
204
|
-
const { signal, cleanup } = createTimedAbortSignal(options.signal, normalized.timeoutMs);
|
|
205
|
-
const root = await validateExistingDirectory(basePath, signal);
|
|
206
|
-
try {
|
|
207
|
-
const { results, summary } = await runSearchFiles(root, pattern, excludePatterns, normalized, signal, options.onProgress);
|
|
208
|
-
return {
|
|
209
|
-
basePath: root,
|
|
210
|
-
pattern,
|
|
211
|
-
results,
|
|
212
|
-
summary,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
finally {
|
|
216
|
-
cleanup();
|
|
217
|
-
}
|
|
218
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { parentPort, threadId, workerData } from 'node:worker_threads';
|
|
2
|
-
import { formatUnknownErrorMessage } from '../errors.js';
|
|
3
|
-
import { isProbablyBinary } from '../fs-helpers.js';
|
|
4
|
-
import { startPerfMeasure } from '../observability.js';
|
|
5
|
-
import { buildMatcher, scanFileInWorker } from './search-content.js';
|
|
6
|
-
const matcherCache = new Map();
|
|
7
|
-
const MAX_MATCHER_CACHE_SIZE = 100;
|
|
8
|
-
function getMatcherCacheKey(pattern, options) {
|
|
9
|
-
const cs = options.caseSensitive ? '1' : '0';
|
|
10
|
-
const ww = options.wholeWord ? '1' : '0';
|
|
11
|
-
const lit = options.isLiteral ? '1' : '0';
|
|
12
|
-
return `${pattern}|${cs}|${ww}|${lit}`;
|
|
13
|
-
}
|
|
14
|
-
function getCachedMatcher(pattern, options) {
|
|
15
|
-
const key = getMatcherCacheKey(pattern, options);
|
|
16
|
-
const cached = matcherCache.get(key);
|
|
17
|
-
if (cached) {
|
|
18
|
-
refreshMatcherCacheEntry(key, cached);
|
|
19
|
-
return cached;
|
|
20
|
-
}
|
|
21
|
-
const matcher = buildMatcher(pattern, options);
|
|
22
|
-
refreshMatcherCacheEntry(key, matcher);
|
|
23
|
-
evictOldestMatcherIfNeeded();
|
|
24
|
-
return matcher;
|
|
25
|
-
}
|
|
26
|
-
function refreshMatcherCacheEntry(key, matcher) {
|
|
27
|
-
matcherCache.delete(key);
|
|
28
|
-
matcherCache.set(key, matcher);
|
|
29
|
-
}
|
|
30
|
-
function evictOldestMatcherIfNeeded() {
|
|
31
|
-
if (matcherCache.size <= MAX_MATCHER_CACHE_SIZE)
|
|
32
|
-
return;
|
|
33
|
-
const firstKey = matcherCache.keys().next().value;
|
|
34
|
-
if (firstKey !== undefined) {
|
|
35
|
-
matcherCache.delete(firstKey);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
const cancelledRequests = new Set();
|
|
39
|
-
const activeRequests = new Set();
|
|
40
|
-
let shuttingDown = false;
|
|
41
|
-
function maybeFinishShutdown() {
|
|
42
|
-
if (!shuttingDown)
|
|
43
|
-
return;
|
|
44
|
-
if (activeRequests.size > 0)
|
|
45
|
-
return;
|
|
46
|
-
parentPort?.close();
|
|
47
|
-
}
|
|
48
|
-
function consumeCancelled(id) {
|
|
49
|
-
if (!cancelledRequests.has(id)) {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
cancelledRequests.delete(id);
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
function markCancelledIfActive(id) {
|
|
56
|
-
if (activeRequests.has(id)) {
|
|
57
|
-
cancelledRequests.add(id);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function buildScanResponse(id, result) {
|
|
61
|
-
return {
|
|
62
|
-
type: 'result',
|
|
63
|
-
id,
|
|
64
|
-
result,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
function buildErrorResponse(id, error) {
|
|
68
|
-
return {
|
|
69
|
-
type: 'error',
|
|
70
|
-
id,
|
|
71
|
-
error: formatUnknownErrorMessage(error),
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
async function handleScanRequest(request) {
|
|
75
|
-
const { id, resolvedPath, requestedPath, pattern, matcherOptions, scanOptions, maxMatches, } = request;
|
|
76
|
-
if (consumeCancelled(id))
|
|
77
|
-
return;
|
|
78
|
-
activeRequests.add(id);
|
|
79
|
-
const endMeasure = startPerfMeasure('searchWorker.scan', {
|
|
80
|
-
maxMatches,
|
|
81
|
-
});
|
|
82
|
-
let ok = false;
|
|
83
|
-
try {
|
|
84
|
-
const matcher = getCachedMatcher(pattern, matcherOptions);
|
|
85
|
-
const isCancelled = () => cancelledRequests.has(id);
|
|
86
|
-
const result = await scanFileInWorker(resolvedPath, requestedPath, matcher, scanOptions, maxMatches, isCancelled, isProbablyBinary);
|
|
87
|
-
if (consumeCancelled(id))
|
|
88
|
-
return;
|
|
89
|
-
parentPort?.postMessage(buildScanResponse(id, result));
|
|
90
|
-
ok = true;
|
|
91
|
-
}
|
|
92
|
-
catch (err) {
|
|
93
|
-
if (consumeCancelled(id))
|
|
94
|
-
return;
|
|
95
|
-
parentPort?.postMessage(buildErrorResponse(id, err));
|
|
96
|
-
}
|
|
97
|
-
finally {
|
|
98
|
-
activeRequests.delete(id);
|
|
99
|
-
cancelledRequests.delete(id);
|
|
100
|
-
endMeasure?.(ok);
|
|
101
|
-
maybeFinishShutdown();
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
function handleMessage(message) {
|
|
105
|
-
switch (message.type) {
|
|
106
|
-
case 'scan':
|
|
107
|
-
if (shuttingDown)
|
|
108
|
-
return;
|
|
109
|
-
void handleScanRequest(message);
|
|
110
|
-
break;
|
|
111
|
-
case 'cancel':
|
|
112
|
-
markCancelledIfActive(message.id);
|
|
113
|
-
break;
|
|
114
|
-
case 'shutdown':
|
|
115
|
-
shuttingDown = true;
|
|
116
|
-
for (const id of activeRequests) {
|
|
117
|
-
markCancelledIfActive(id);
|
|
118
|
-
}
|
|
119
|
-
maybeFinishShutdown();
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (parentPort) {
|
|
124
|
-
parentPort.on('message', handleMessage);
|
|
125
|
-
const data = workerData;
|
|
126
|
-
if (data?.debug) {
|
|
127
|
-
console.error(`[SearchWorker] Started with threadId=${String(threadId)}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { EntryType } from './common.js';
|
|
2
|
-
interface TreeEntry {
|
|
3
|
-
name: string;
|
|
4
|
-
type: EntryType;
|
|
5
|
-
relativePath: string;
|
|
6
|
-
children?: TreeEntry[];
|
|
7
|
-
}
|
|
8
|
-
interface TreeOptions {
|
|
9
|
-
maxDepth?: number;
|
|
10
|
-
maxEntries?: number;
|
|
11
|
-
includeHidden?: boolean;
|
|
12
|
-
includeIgnored?: boolean;
|
|
13
|
-
timeoutMs?: number;
|
|
14
|
-
signal?: AbortSignal;
|
|
15
|
-
onProgress?: (progress: {
|
|
16
|
-
total?: number;
|
|
17
|
-
current: number;
|
|
18
|
-
}) => void;
|
|
19
|
-
}
|
|
20
|
-
interface TreeResult {
|
|
21
|
-
root: string;
|
|
22
|
-
tree: TreeEntry;
|
|
23
|
-
truncated: boolean;
|
|
24
|
-
totalEntries: number;
|
|
25
|
-
}
|
|
26
|
-
export declare function formatTreeAscii(tree: TreeEntry): string;
|
|
27
|
-
export declare function treeDirectory(dirPath: string, options?: TreeOptions): Promise<TreeResult>;
|
|
28
|
-
export {};
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import * as path from 'node:path';
|
|
2
|
-
import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_TIMEOUT_MS, } from '../constants.js';
|
|
3
|
-
import { createTimedAbortSignal } from '../fs-helpers.js';
|
|
4
|
-
import { toPosixPath } from '../path-format.js';
|
|
5
|
-
import { isSensitivePath } from '../path-policy.js';
|
|
6
|
-
import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
|
|
7
|
-
import { isEntryAccessibleByType, resolveEntryType, resolveStopReason, } from './common.js';
|
|
8
|
-
import { isIgnoredByGitignore, loadRootGitignore } from './gitignore.js';
|
|
9
|
-
import { globEntries } from './glob-engine.js';
|
|
10
|
-
function toSafeNonNegativeInt(value, fallback) {
|
|
11
|
-
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
12
|
-
return fallback;
|
|
13
|
-
const asInt = Math.floor(value);
|
|
14
|
-
return asInt >= 0 ? asInt : fallback;
|
|
15
|
-
}
|
|
16
|
-
function toSafePositiveInt(value, fallback) {
|
|
17
|
-
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
18
|
-
return fallback;
|
|
19
|
-
const asInt = Math.floor(value);
|
|
20
|
-
return asInt > 0 ? asInt : fallback;
|
|
21
|
-
}
|
|
22
|
-
function toSafeBoolean(value, fallback) {
|
|
23
|
-
if (typeof value !== 'boolean')
|
|
24
|
-
return fallback;
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
|
-
function normalizeOptions(options) {
|
|
28
|
-
return {
|
|
29
|
-
maxDepth: toSafeNonNegativeInt(options.maxDepth, 5),
|
|
30
|
-
maxEntries: toSafeNonNegativeInt(options.maxEntries, 1000),
|
|
31
|
-
includeHidden: toSafeBoolean(options.includeHidden, false),
|
|
32
|
-
includeIgnored: toSafeBoolean(options.includeIgnored, false),
|
|
33
|
-
timeoutMs: toSafePositiveInt(options.timeoutMs, DEFAULT_SEARCH_TIMEOUT_MS),
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function ensureParentNodes(rootNode, nodeByPath, relativePath) {
|
|
37
|
-
const normalized = toPosixPath(relativePath);
|
|
38
|
-
if (normalized.length === 0 || normalized === '.')
|
|
39
|
-
return rootNode;
|
|
40
|
-
const segments = [];
|
|
41
|
-
for (const segment of normalized.split('/')) {
|
|
42
|
-
if (segment.length > 0) {
|
|
43
|
-
segments.push(segment);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
const parentSegmentCount = Math.max(0, segments.length - 1);
|
|
47
|
-
let current = rootNode;
|
|
48
|
-
let currentPath = '';
|
|
49
|
-
for (let index = 0; index < parentSegmentCount; index += 1) {
|
|
50
|
-
const segment = segments[index];
|
|
51
|
-
if (!segment)
|
|
52
|
-
continue;
|
|
53
|
-
currentPath =
|
|
54
|
-
currentPath.length === 0
|
|
55
|
-
? segment
|
|
56
|
-
: path.posix.join(currentPath, segment);
|
|
57
|
-
let child = nodeByPath.get(currentPath);
|
|
58
|
-
if (!child) {
|
|
59
|
-
child = {
|
|
60
|
-
name: segment,
|
|
61
|
-
type: 'directory',
|
|
62
|
-
relativePath: currentPath,
|
|
63
|
-
children: [],
|
|
64
|
-
};
|
|
65
|
-
nodeByPath.set(currentPath, child);
|
|
66
|
-
current.children ??= [];
|
|
67
|
-
current.children.push(child);
|
|
68
|
-
}
|
|
69
|
-
current = child;
|
|
70
|
-
}
|
|
71
|
-
return current;
|
|
72
|
-
}
|
|
73
|
-
function sortTree(node) {
|
|
74
|
-
if (!node.children)
|
|
75
|
-
return;
|
|
76
|
-
node.children.sort(compareTreeEntries);
|
|
77
|
-
for (const child of node.children) {
|
|
78
|
-
sortTree(child);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
function compareTreeEntries(a, b) {
|
|
82
|
-
const diff = getTreeTypeRank(a.type) - getTreeTypeRank(b.type);
|
|
83
|
-
if (diff !== 0)
|
|
84
|
-
return diff;
|
|
85
|
-
return a.name.localeCompare(b.name);
|
|
86
|
-
}
|
|
87
|
-
function getTreeTypeRank(type) {
|
|
88
|
-
if (type === 'directory')
|
|
89
|
-
return 0;
|
|
90
|
-
if (type === 'file')
|
|
91
|
-
return 1;
|
|
92
|
-
return 2;
|
|
93
|
-
}
|
|
94
|
-
async function resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps) {
|
|
95
|
-
const type = resolveEntryType(entry.dirent);
|
|
96
|
-
const isAccessible = await isEntryAccessibleByType(entry.path, type, rootDirectories, signal, accessDeps);
|
|
97
|
-
if (!isAccessible) {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
if (gitignoreMatcher &&
|
|
101
|
-
isIgnoredByGitignore(gitignoreMatcher, root, entry.path, {
|
|
102
|
-
isDirectory: type === 'directory',
|
|
103
|
-
})) {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
const relative = path.relative(root, entry.path) || path.basename(entry.path);
|
|
107
|
-
const relativePosix = toPosixPath(relative);
|
|
108
|
-
const name = path.basename(entry.path);
|
|
109
|
-
return { type, relativePosix, name };
|
|
110
|
-
}
|
|
111
|
-
function upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent) {
|
|
112
|
-
const ensureDirectoryShape = (node) => {
|
|
113
|
-
if (node.type === 'directory') {
|
|
114
|
-
node.children ??= [];
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
delete node.children;
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
const maybeUpdateType = (existing, nextType) => {
|
|
121
|
-
if (existing.type === nextType)
|
|
122
|
-
return;
|
|
123
|
-
const preservePopulatedDirectory = existing.type === 'directory' &&
|
|
124
|
-
Array.isArray(existing.children) &&
|
|
125
|
-
existing.children.length > 0;
|
|
126
|
-
if (preservePopulatedDirectory)
|
|
127
|
-
return;
|
|
128
|
-
existing.type = nextType;
|
|
129
|
-
ensureDirectoryShape(existing);
|
|
130
|
-
};
|
|
131
|
-
const attachChild = (child) => {
|
|
132
|
-
parent.children ??= [];
|
|
133
|
-
let seen = childPathIndexByParent.get(parent);
|
|
134
|
-
if (!seen) {
|
|
135
|
-
seen = new Set();
|
|
136
|
-
for (const entry of parent.children) {
|
|
137
|
-
seen.add(entry.relativePath);
|
|
138
|
-
}
|
|
139
|
-
childPathIndexByParent.set(parent, seen);
|
|
140
|
-
}
|
|
141
|
-
const key = child.relativePath;
|
|
142
|
-
if (seen.has(key))
|
|
143
|
-
return;
|
|
144
|
-
seen.add(key);
|
|
145
|
-
parent.children.push(child);
|
|
146
|
-
};
|
|
147
|
-
const existing = nodeByPath.get(resolved.relativePosix);
|
|
148
|
-
if (existing) {
|
|
149
|
-
// Avoid duplicate directory nodes when a child file is encountered before the directory entry.
|
|
150
|
-
// Prefer preserving an existing populated directory node over overwriting it.
|
|
151
|
-
maybeUpdateType(existing, resolved.type);
|
|
152
|
-
existing.name = resolved.name;
|
|
153
|
-
existing.relativePath = resolved.relativePosix;
|
|
154
|
-
ensureDirectoryShape(existing);
|
|
155
|
-
attachChild(existing);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
const node = {
|
|
159
|
-
name: resolved.name,
|
|
160
|
-
type: resolved.type,
|
|
161
|
-
relativePath: resolved.relativePosix,
|
|
162
|
-
...(resolved.type === 'directory' ? { children: [] } : {}),
|
|
163
|
-
};
|
|
164
|
-
nodeByPath.set(resolved.relativePosix, node);
|
|
165
|
-
attachChild(node);
|
|
166
|
-
}
|
|
167
|
-
export function formatTreeAscii(tree) {
|
|
168
|
-
const lines = [];
|
|
169
|
-
const walk = (node, prefix, isLast, isRoot) => {
|
|
170
|
-
let connector = '';
|
|
171
|
-
let linePrefix = '';
|
|
172
|
-
if (!isRoot) {
|
|
173
|
-
connector = isLast ? '└── ' : '├── ';
|
|
174
|
-
linePrefix = prefix;
|
|
175
|
-
}
|
|
176
|
-
lines.push(`${linePrefix}${connector}${node.name}`);
|
|
177
|
-
if (!node.children || node.children.length === 0)
|
|
178
|
-
return;
|
|
179
|
-
let nextPrefix = '';
|
|
180
|
-
if (!isRoot) {
|
|
181
|
-
const continuation = isLast ? ' ' : '│ ';
|
|
182
|
-
nextPrefix = `${prefix}${continuation}`;
|
|
183
|
-
}
|
|
184
|
-
const count = node.children.length;
|
|
185
|
-
for (let index = 0; index < count; index += 1) {
|
|
186
|
-
const child = node.children[index];
|
|
187
|
-
if (child) {
|
|
188
|
-
walk(child, nextPrefix, index === count - 1, false);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
walk(tree, '', true, true);
|
|
193
|
-
return lines.join('\n');
|
|
194
|
-
}
|
|
195
|
-
export async function treeDirectory(dirPath, options = {}) {
|
|
196
|
-
const normalized = normalizeOptions(options);
|
|
197
|
-
const { signal, cleanup } = createTimedAbortSignal(options.signal, normalized.timeoutMs);
|
|
198
|
-
const root = await validateExistingDirectory(dirPath, signal);
|
|
199
|
-
const rootNormalized = normalizePath(root);
|
|
200
|
-
const rootDirectories = [rootNormalized];
|
|
201
|
-
const accessDeps = {
|
|
202
|
-
normalizePath,
|
|
203
|
-
isPathWithinDirectories,
|
|
204
|
-
isSensitivePath,
|
|
205
|
-
validateSymlinkPath: validateExistingPathDetailed,
|
|
206
|
-
};
|
|
207
|
-
try {
|
|
208
|
-
const excludePatterns = normalized.includeIgnored
|
|
209
|
-
? []
|
|
210
|
-
: DEFAULT_EXCLUDE_PATTERNS;
|
|
211
|
-
const gitignoreMatcher = normalized.includeIgnored
|
|
212
|
-
? null
|
|
213
|
-
: await loadRootGitignore(root, signal);
|
|
214
|
-
const rootNode = {
|
|
215
|
-
name: path.basename(root) || root,
|
|
216
|
-
type: 'directory',
|
|
217
|
-
relativePath: '.',
|
|
218
|
-
children: [],
|
|
219
|
-
};
|
|
220
|
-
const nodeByPath = new Map();
|
|
221
|
-
const childPathIndexByParent = new WeakMap();
|
|
222
|
-
let totalEntries = 0;
|
|
223
|
-
let truncated = false;
|
|
224
|
-
const stream = globEntries({
|
|
225
|
-
cwd: root,
|
|
226
|
-
pattern: '**/*',
|
|
227
|
-
excludePatterns,
|
|
228
|
-
includeHidden: normalized.includeHidden,
|
|
229
|
-
baseNameMatch: false,
|
|
230
|
-
caseSensitiveMatch: true,
|
|
231
|
-
maxDepth: normalized.maxDepth,
|
|
232
|
-
followSymbolicLinks: false,
|
|
233
|
-
onlyFiles: false,
|
|
234
|
-
stats: false,
|
|
235
|
-
suppressErrors: true,
|
|
236
|
-
});
|
|
237
|
-
for await (const entry of stream) {
|
|
238
|
-
const stopReason = resolveStopReason({
|
|
239
|
-
signal,
|
|
240
|
-
current: totalEntries,
|
|
241
|
-
max: normalized.maxEntries,
|
|
242
|
-
abortedReason: 'aborted',
|
|
243
|
-
maxReason: 'maxEntries',
|
|
244
|
-
});
|
|
245
|
-
if (stopReason) {
|
|
246
|
-
truncated = true;
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
const resolved = await resolveTreeEntry(entry, root, rootDirectories, gitignoreMatcher, signal, accessDeps);
|
|
250
|
-
if (!resolved) {
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
const parent = ensureParentNodes(rootNode, nodeByPath, resolved.relativePosix);
|
|
254
|
-
upsertChildNode(parent, nodeByPath, resolved, childPathIndexByParent);
|
|
255
|
-
totalEntries += 1;
|
|
256
|
-
options.onProgress?.({ current: totalEntries });
|
|
257
|
-
}
|
|
258
|
-
sortTree(rootNode);
|
|
259
|
-
return {
|
|
260
|
-
root,
|
|
261
|
-
tree: rootNode,
|
|
262
|
-
truncated,
|
|
263
|
-
totalEntries,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
finally {
|
|
267
|
-
cleanup();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function toPosixPath(value: string): string;
|
package/dist/lib/path-format.js
DELETED