@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.
Files changed (76) hide show
  1. package/README.md +635 -377
  2. package/dist/cli.js +2 -2
  3. package/dist/completions.js +2 -3
  4. package/dist/index.js +2 -2
  5. package/dist/lib/file-operations/{common.d.ts → core.d.ts} +6 -0
  6. package/dist/lib/file-operations/{common.js → core.js} +45 -0
  7. package/dist/lib/file-operations/metadata.d.ts +73 -0
  8. package/dist/lib/file-operations/metadata.js +889 -0
  9. package/dist/lib/file-operations/{search-content.d.ts → search.d.ts} +29 -3
  10. package/dist/lib/file-operations/{search-content.js → search.js} +409 -82
  11. package/dist/lib/file-operations/{glob-engine.d.ts → traversal.d.ts} +18 -1
  12. package/dist/lib/file-operations/{glob-engine.js → traversal.js} +25 -2
  13. package/dist/lib/fs-helpers.d.ts +1 -0
  14. package/dist/lib/fs-helpers.js +10 -2
  15. package/dist/lib/observability.js +1 -1
  16. package/dist/lib/{path-validation.d.ts → paths.d.ts} +4 -0
  17. package/dist/lib/{path-validation.js → paths.js} +112 -1
  18. package/dist/lib/utils.d.ts +15 -0
  19. package/dist/lib/utils.js +33 -0
  20. package/dist/resources/generated-instructions.js +6 -6
  21. package/dist/resources/tool-catalog.js +9 -9
  22. package/dist/resources/tool-info.js +3 -3
  23. package/dist/resources/workflows.js +10 -23
  24. package/dist/schemas.js +15 -15
  25. package/dist/server/bootstrap.d.ts +19 -1
  26. package/dist/server/bootstrap.js +103 -9
  27. package/dist/server/roots-manager.d.ts +2 -2
  28. package/dist/server/roots-manager.js +3 -3
  29. package/dist/tools/apply-patch.js +7 -3
  30. package/dist/tools/calculate-hash.js +15 -15
  31. package/dist/tools/create-directory.js +1 -1
  32. package/dist/tools/delete-file.js +7 -4
  33. package/dist/tools/diff-files.js +2 -2
  34. package/dist/tools/edit-file.js +14 -13
  35. package/dist/tools/list-directory.js +5 -7
  36. package/dist/tools/move-file.js +2 -1
  37. package/dist/tools/read-multiple.js +13 -26
  38. package/dist/tools/read.js +3 -3
  39. package/dist/tools/replace-in-files.js +17 -20
  40. package/dist/tools/roots.js +4 -6
  41. package/dist/tools/search-content.js +9 -11
  42. package/dist/tools/search-files.js +4 -6
  43. package/dist/tools/shared.d.ts +18 -1
  44. package/dist/tools/shared.js +39 -19
  45. package/dist/tools/stat-many.js +14 -24
  46. package/dist/tools/stat.js +4 -3
  47. package/dist/tools/task-support.js +1 -1
  48. package/dist/tools/tree.js +3 -4
  49. package/dist/tools/write-file.js +1 -1
  50. package/package.json +9 -5
  51. package/dist/lib/file-operations/file-info.d.ts +0 -10
  52. package/dist/lib/file-operations/file-info.js +0 -143
  53. package/dist/lib/file-operations/gitignore.d.ts +0 -6
  54. package/dist/lib/file-operations/gitignore.js +0 -45
  55. package/dist/lib/file-operations/list-directory.d.ts +0 -14
  56. package/dist/lib/file-operations/list-directory.js +0 -256
  57. package/dist/lib/file-operations/read-multiple-files.d.ts +0 -25
  58. package/dist/lib/file-operations/read-multiple-files.js +0 -252
  59. package/dist/lib/file-operations/search-files.d.ts +0 -27
  60. package/dist/lib/file-operations/search-files.js +0 -218
  61. package/dist/lib/file-operations/search-worker.d.ts +0 -2
  62. package/dist/lib/file-operations/search-worker.js +0 -129
  63. package/dist/lib/file-operations/tree.d.ts +0 -28
  64. package/dist/lib/file-operations/tree.js +0 -269
  65. package/dist/lib/path-format.d.ts +0 -1
  66. package/dist/lib/path-format.js +0 -7
  67. package/dist/lib/path-policy.d.ts +0 -2
  68. package/dist/lib/path-policy.js +0 -100
  69. package/dist/lib/type-guards.d.ts +0 -1
  70. package/dist/lib/type-guards.js +0 -3
  71. package/dist/server/capabilities.d.ts +0 -10
  72. package/dist/server/capabilities.js +0 -48
  73. package/dist/server/logging.d.ts +0 -7
  74. package/dist/server/logging.js +0 -41
  75. package/dist/server/types.d.ts +0 -4
  76. 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,2 +0,0 @@
1
- import type { ScanRequest, ScanResult, WorkerResponse } from './search-content.js';
2
- export type { WorkerResponse, ScanRequest, ScanResult };
@@ -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;
@@ -1,7 +0,0 @@
1
- const WINDOWS_PATH_SEPARATOR = '\\';
2
- const POSIX_PATH_SEPARATOR = '/';
3
- export function toPosixPath(value) {
4
- return value.includes(WINDOWS_PATH_SEPARATOR)
5
- ? value.replace(/\\/gu, POSIX_PATH_SEPARATOR)
6
- : value;
7
- }
@@ -1,2 +0,0 @@
1
- export declare function isSensitivePath(requestedPath: string, resolvedPath?: string): boolean;
2
- export declare function assertAllowedFileAccess(requestedPath: string, resolvedPath?: string): void;