@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,12 +1,15 @@
1
1
  import * as fsp from 'node:fs/promises';
2
2
  import { z } from 'zod';
3
- import type { ContentMatch, SearchContentResult } from '../../config.js';
3
+ import type { ContentMatch, SearchContentResult, SearchFilesResult } from '../../config.js';
4
4
  export declare const MatcherOptionsSchema: z.ZodObject<{
5
5
  caseSensitive: z.ZodBoolean;
6
6
  wholeWord: z.ZodBoolean;
7
7
  isLiteral: z.ZodBoolean;
8
8
  }, z.core.$strict>;
9
9
  export type MatcherOptions = z.infer<typeof MatcherOptionsSchema>;
10
+ export type Matcher = (line: string) => number;
11
+ export declare function validatePattern(pattern: string, options: MatcherOptions): void;
12
+ export declare function buildMatcher(pattern: string, options: MatcherOptions): Matcher;
10
13
  export interface ScanFileOptions {
11
14
  maxFileSize: number;
12
15
  skipBinary: boolean;
@@ -36,8 +39,6 @@ export interface SearchContentOptions extends Partial<ResolvedOptions> {
36
39
  current: number;
37
40
  }) => void;
38
41
  }
39
- export type Matcher = (line: string) => number;
40
- export declare function buildMatcher(pattern: string, options: MatcherOptions): Matcher;
41
42
  type BinaryDetector = (resolvedPath: string, handle: fsp.FileHandle, signal?: AbortSignal) => Promise<boolean>;
42
43
  export interface ScanRequest {
43
44
  type: 'scan';
@@ -73,4 +74,29 @@ interface WorkerScanResult {
73
74
  }
74
75
  export declare function scanFileInWorker(resolvedPath: string, requestedPath: string, matcher: Matcher, options: ScanFileOptions, maxMatches: number, isCancelled: () => boolean, isBinaryDetector: BinaryDetector): Promise<WorkerScanResult>;
75
76
  export declare function searchContent(basePath: string, pattern: string, options?: SearchContentOptions): Promise<SearchContentResult>;
77
+ type SortBy = 'name' | 'size' | 'modified' | 'path';
78
+ interface SearchFilesOptions {
79
+ maxResults?: number;
80
+ sortBy?: SortBy;
81
+ maxDepth?: number;
82
+ maxFilesScanned?: number;
83
+ timeoutMs?: number;
84
+ baseNameMatch?: boolean;
85
+ skipSymlinks?: boolean;
86
+ includeHidden?: boolean;
87
+ respectGitignore?: boolean;
88
+ signal?: AbortSignal;
89
+ onProgress?: (progress: {
90
+ total?: number;
91
+ current: number;
92
+ }) => void;
93
+ }
94
+ interface Sortable {
95
+ name?: string;
96
+ size?: number;
97
+ modified?: Date;
98
+ path?: string;
99
+ }
100
+ export declare function sortSearchResults(results: Sortable[], sortBy: SortBy): void;
101
+ export declare function searchFiles(basePath: string, pattern: string, excludePatterns?: readonly string[], options?: SearchFilesOptions): Promise<SearchFilesResult>;
76
102
  export {};
@@ -1,70 +1,23 @@
1
1
  import * as fsp from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { fileURLToPath, pathToFileURL } from 'node:url';
4
- import { Worker } from 'node:worker_threads';
5
- import { z } from 'zod';
4
+ import { parentPort, threadId, Worker, workerData } from 'node:worker_threads';
6
5
  import RE2 from 're2';
7
6
  import safeRegex from 'safe-regex2';
7
+ import { z } from 'zod';
8
8
  import { DEFAULT_EXCLUDE_PATTERNS, DEFAULT_SEARCH_MAX_FILES, DEFAULT_SEARCH_TIMEOUT_MS, MAX_LINE_CONTENT_LENGTH, MAX_SEARCHABLE_FILE_SIZE, SEARCH_WORKERS, } from '../constants.js';
9
9
  import { ErrorCode, formatUnknownErrorMessage, isTimeoutLikeError, McpError, } from '../errors.js';
10
- import { assertNotAborted, createTimedAbortSignal, isProbablyBinary, withAbort, } from '../fs-helpers.js';
11
- import { assertAllowedFileAccess, isSensitivePath } from '../path-policy.js';
12
- import { isPathWithinDirectories, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../path-validation.js';
13
- import { withOptionalStoppedReason } from './common.js';
14
- import { globEntries } from './glob-engine.js';
15
- // --- Configuration & Schemas ---
16
- const INTERNAL_MAX_RESULTS = 500;
10
+ import { assertNotAborted, isProbablyBinary, withAbort, withTimedAbortSignal, } from '../fs-helpers.js';
11
+ import { startPerfMeasure } from '../observability.js';
12
+ import { assertAllowedFileAccess, isPathWithinDirectories, isSensitivePath, normalizePath, validateExistingDirectory, validateExistingPathDetailed, } from '../paths.js';
13
+ import { mergeOptions, omitOptionKeys, reportPeriodicProgress, } from '../utils.js';
14
+ import { compareOptionalNumberDesc, compareStringValues, isEntryAccessibleByType, isIgnoredByGitignore, loadRootGitignore, needsStatsForSort, resolveEntryType, resolveStopReason, stableSortByDerivedString, withOptionalStoppedReason, } from './core.js';
15
+ import { buildGlobOptions, globEntries } from './traversal.js';
17
16
  export const MatcherOptionsSchema = z.strictObject({
18
17
  caseSensitive: z.boolean(),
19
18
  wholeWord: z.boolean(),
20
19
  isLiteral: z.boolean(),
21
20
  });
22
- const SearchOptionsSchema = z.strictObject({
23
- filePattern: z.string().min(1),
24
- excludePatterns: z.array(z.string()),
25
- caseSensitive: z.boolean(),
26
- maxResults: z.number().int().nonnegative(),
27
- maxFileSize: z.number().int().nonnegative(),
28
- maxFilesScanned: z.number().int().nonnegative(),
29
- timeoutMs: z.number().int().nonnegative(),
30
- skipBinary: z.boolean(),
31
- contextLines: z.number().int().nonnegative(),
32
- wholeWord: z.boolean(),
33
- isLiteral: z.boolean(),
34
- includeHidden: z.boolean(),
35
- baseNameMatch: z.boolean(),
36
- caseSensitiveFileMatch: z.boolean(),
37
- });
38
- const DEFAULTS = {
39
- filePattern: '**/*',
40
- excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
41
- caseSensitive: false,
42
- maxResults: INTERNAL_MAX_RESULTS,
43
- maxFileSize: MAX_SEARCHABLE_FILE_SIZE,
44
- maxFilesScanned: DEFAULT_SEARCH_MAX_FILES,
45
- timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
46
- skipBinary: true,
47
- contextLines: 0,
48
- wholeWord: false,
49
- isLiteral: true,
50
- includeHidden: false,
51
- baseNameMatch: false,
52
- caseSensitiveFileMatch: true,
53
- };
54
- const ERROR_SCAN_CANCELLED = 'Scan cancelled';
55
- const ERROR_WORKER_POOL_CLOSED = 'Worker pool closed';
56
- // --- Helpers ---
57
- function resolveOptions(options) {
58
- const rest = { ...options };
59
- delete rest.signal;
60
- delete rest.onProgress;
61
- const merged = { ...DEFAULTS, ...rest };
62
- const result = SearchOptionsSchema.safeParse(merged);
63
- if (!result.success) {
64
- throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid search options: ${result.error.message}`, undefined, { errors: z.treeifyError(result.error) });
65
- }
66
- return result.data;
67
- }
68
21
  function countRegexLineMatches(regex, line) {
69
22
  regex.lastIndex = 0;
70
23
  let count = 0;
@@ -82,7 +35,7 @@ function buildRegexPattern(pattern, options) {
82
35
  const escaped = options.isLiteral ? escapeLiteral(pattern) : pattern;
83
36
  return options.wholeWord ? `\\b${escaped}\\b` : escaped;
84
37
  }
85
- function validatePattern(pattern, options) {
38
+ export function validatePattern(pattern, options) {
86
39
  if (options.isLiteral && pattern.length === 0)
87
40
  return;
88
41
  if (options.isLiteral && !options.wholeWord)
@@ -129,6 +82,52 @@ export function buildMatcher(pattern, options) {
129
82
  validatePattern(pattern, options); // Re-validate to be safe
130
83
  return buildRegexMatcher(final, options.caseSensitive);
131
84
  }
85
+ // --- Configuration & Schemas ---
86
+ const SEARCH_CONTENT_MAX_RESULTS = 500;
87
+ const SearchOptionsSchema = z.strictObject({
88
+ filePattern: z.string().min(1),
89
+ excludePatterns: z.array(z.string()),
90
+ caseSensitive: z.boolean(),
91
+ maxResults: z.number().int().nonnegative(),
92
+ maxFileSize: z.number().int().nonnegative(),
93
+ maxFilesScanned: z.number().int().nonnegative(),
94
+ timeoutMs: z.number().int().nonnegative(),
95
+ skipBinary: z.boolean(),
96
+ contextLines: z.number().int().nonnegative(),
97
+ wholeWord: z.boolean(),
98
+ isLiteral: z.boolean(),
99
+ includeHidden: z.boolean(),
100
+ baseNameMatch: z.boolean(),
101
+ caseSensitiveFileMatch: z.boolean(),
102
+ });
103
+ const DEFAULTS = {
104
+ filePattern: '**/*',
105
+ excludePatterns: DEFAULT_EXCLUDE_PATTERNS,
106
+ caseSensitive: false,
107
+ maxResults: SEARCH_CONTENT_MAX_RESULTS,
108
+ maxFileSize: MAX_SEARCHABLE_FILE_SIZE,
109
+ maxFilesScanned: DEFAULT_SEARCH_MAX_FILES,
110
+ timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
111
+ skipBinary: true,
112
+ contextLines: 0,
113
+ wholeWord: false,
114
+ isLiteral: true,
115
+ includeHidden: false,
116
+ baseNameMatch: false,
117
+ caseSensitiveFileMatch: true,
118
+ };
119
+ const ERROR_SCAN_CANCELLED = 'Scan cancelled';
120
+ const ERROR_WORKER_POOL_CLOSED = 'Worker pool closed';
121
+ // --- Helpers ---
122
+ function resolveOptions(options) {
123
+ const normalizedOptions = omitOptionKeys(options, ['signal', 'onProgress']);
124
+ const merged = mergeOptions(DEFAULTS, normalizedOptions);
125
+ const result = SearchOptionsSchema.safeParse(merged);
126
+ if (!result.success) {
127
+ throw new McpError(ErrorCode.E_INVALID_INPUT, `Invalid search options: ${result.error.message}`, undefined, { errors: z.treeifyError(result.error) });
128
+ }
129
+ return result.data;
130
+ }
132
131
  /**
133
132
  * Manages a sliding window of lines and pending context-after buffers.
134
133
  */
@@ -321,7 +320,7 @@ function createScanSummary() {
321
320
  stoppedReason: undefined,
322
321
  };
323
322
  }
324
- function buildSearchResult(root, pattern, filePattern, matches, summary) {
323
+ function buildSearchContentResult(root, pattern, filePattern, matches, summary) {
325
324
  const baseSummary = {
326
325
  filesScanned: summary.filesScanned,
327
326
  filesMatched: summary.filesMatched,
@@ -562,13 +561,6 @@ function processScanResult(winner, summary, matches, maxResults) {
562
561
  }
563
562
  }
564
563
  }
565
- function reportSearchProgress(onProgress, current, total, force = false) {
566
- if (!onProgress || current === 0)
567
- return;
568
- if (!force && current % 25 !== 0)
569
- return;
570
- onProgress({ current, total });
571
- }
572
564
  async function waitForWinner(pending) {
573
565
  const raceCandidates = [];
574
566
  for (const task of pending) {
@@ -654,12 +646,12 @@ async function searchSingleFile(details, opts, pattern, signal) {
654
646
  }, signal, opts.maxResults);
655
647
  if (result.matched)
656
648
  summary.filesMatched = 1;
657
- return buildSearchResult(path.dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
649
+ return buildSearchContentResult(path.dirname(details.resolvedPath), pattern, opts.filePattern, result.matches, summary);
658
650
  }
659
651
  async function searchDirectory(details, opts, pattern, signal, onProgress) {
660
652
  const root = await validateExistingDirectory(details.resolvedPath, signal);
661
653
  const rootDirectories = [root];
662
- const stream = globEntries({
654
+ const stream = globEntries(buildGlobOptions({
663
655
  cwd: root,
664
656
  pattern: opts.filePattern,
665
657
  excludePatterns: opts.excludePatterns,
@@ -670,7 +662,7 @@ async function searchDirectory(details, opts, pattern, signal, onProgress) {
670
662
  onlyFiles: true,
671
663
  stats: false,
672
664
  suppressErrors: true,
673
- });
665
+ }));
674
666
  async function* fileGenerator() {
675
667
  let scanned = 0;
676
668
  for await (const entry of stream) {
@@ -686,10 +678,17 @@ async function searchDirectory(details, opts, pattern, signal, onProgress) {
686
678
  if (isSensitivePath(entry.path, normalized))
687
679
  continue;
688
680
  scanned++;
689
- reportSearchProgress(onProgress, scanned, opts.maxFilesScanned);
681
+ reportPeriodicProgress(onProgress, scanned, {
682
+ total: opts.maxFilesScanned,
683
+ throttleModulo: 25,
684
+ });
690
685
  yield { resolvedPath: normalized, requestedPath: entry.path };
691
686
  }
692
- reportSearchProgress(onProgress, scanned, opts.maxFilesScanned, true);
687
+ reportPeriodicProgress(onProgress, scanned, {
688
+ total: opts.maxFilesScanned,
689
+ throttleModulo: 25,
690
+ force: true,
691
+ });
693
692
  }
694
693
  const summary = createScanSummary();
695
694
  const resolvedStream = fileGenerator();
@@ -712,12 +711,12 @@ async function searchDirectory(details, opts, pattern, signal, onProgress) {
712
711
  const matches = shouldUseWorkers()
713
712
  ? await executeParallel(countingStream(), pattern, opts, signal, summary)
714
713
  : await executeSequential(countingStream(), pattern, opts, signal, summary);
715
- return buildSearchResult(root, pattern, opts.filePattern, matches, summary);
714
+ return buildSearchContentResult(root, pattern, opts.filePattern, matches, summary);
716
715
  }
717
716
  function buildTimeoutSearchResult(basePath, pattern, filePattern) {
718
717
  const timeoutSummary = createScanSummary();
719
718
  markTruncated(timeoutSummary, 'timeout');
720
- return buildSearchResult(basePath, pattern, filePattern, [], timeoutSummary);
719
+ return buildSearchContentResult(basePath, pattern, filePattern, [], timeoutSummary);
721
720
  }
722
721
  export async function searchContent(basePath, pattern, options = {}) {
723
722
  if (!basePath.trim())
@@ -725,17 +724,18 @@ export async function searchContent(basePath, pattern, options = {}) {
725
724
  if (typeof pattern !== 'string')
726
725
  throw new McpError(ErrorCode.E_INVALID_INPUT, 'pattern required');
727
726
  const opts = resolveOptions(options);
728
- const { signal, cleanup } = createTimedAbortSignal(options.signal, opts.timeoutMs);
729
727
  try {
730
- const details = await validateExistingPathDetailed(basePath, signal);
731
- const stats = await withAbort(fsp.stat(details.resolvedPath), signal);
732
- if (stats.isFile()) {
733
- return await searchSingleFile(details, opts, pattern, signal);
734
- }
735
- if (!stats.isDirectory()) {
736
- throw new McpError(ErrorCode.E_INVALID_INPUT, 'Path must be file or directory', basePath);
737
- }
738
- return await searchDirectory(details, opts, pattern, signal, options.onProgress);
728
+ return await withTimedAbortSignal(options.signal, opts.timeoutMs, async (signal) => {
729
+ const details = await validateExistingPathDetailed(basePath, signal);
730
+ const stats = await withAbort(fsp.stat(details.resolvedPath), signal);
731
+ if (stats.isFile()) {
732
+ return searchSingleFile(details, opts, pattern, signal);
733
+ }
734
+ if (!stats.isDirectory()) {
735
+ throw new McpError(ErrorCode.E_INVALID_INPUT, 'Path must be file or directory', basePath);
736
+ }
737
+ return searchDirectory(details, opts, pattern, signal, options.onProgress);
738
+ });
739
739
  }
740
740
  catch (error) {
741
741
  if (isTimeoutLikeError(error)) {
@@ -743,7 +743,334 @@ export async function searchContent(basePath, pattern, options = {}) {
743
743
  }
744
744
  throw error;
745
745
  }
746
+ }
747
+ // Internal default for find tool - not exposed to MCP users
748
+ const SEARCH_FILES_MAX_RESULTS = 1000;
749
+ function normalizeSearchFilesOptions(options) {
750
+ const normalized = {
751
+ maxResults: options.maxResults ?? SEARCH_FILES_MAX_RESULTS,
752
+ sortBy: options.sortBy ?? 'path',
753
+ maxFilesScanned: options.maxFilesScanned ?? DEFAULT_SEARCH_MAX_FILES,
754
+ timeoutMs: options.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS,
755
+ baseNameMatch: options.baseNameMatch ?? false,
756
+ skipSymlinks: options.skipSymlinks ?? true,
757
+ includeHidden: options.includeHidden ?? false,
758
+ respectGitignore: options.respectGitignore ?? false,
759
+ };
760
+ if (options.maxDepth !== undefined) {
761
+ normalized.maxDepth = options.maxDepth;
762
+ }
763
+ return normalized;
764
+ }
765
+ function buildSearchFilesResult(entry, entryType, needsStats) {
766
+ let resolvedType = 'other';
767
+ if (entryType === 'directory') {
768
+ resolvedType = 'directory';
769
+ }
770
+ else if (entryType === 'file') {
771
+ resolvedType = 'file';
772
+ }
773
+ const size = needsStats && entry.stats?.isFile() ? entry.stats.size : undefined;
774
+ const modified = needsStats ? entry.stats?.mtime : undefined;
775
+ return {
776
+ path: entry.path,
777
+ type: resolvedType,
778
+ ...(size !== undefined ? { size } : {}),
779
+ ...(modified !== undefined ? { modified } : {}),
780
+ };
781
+ }
782
+ function shouldStopCollecting(state, normalized, signal) {
783
+ const stopReason = resolveStopReason({
784
+ signal,
785
+ current: state.filesScanned,
786
+ max: normalized.maxFilesScanned,
787
+ abortedReason: 'timeout',
788
+ maxReason: 'maxFiles',
789
+ });
790
+ if (stopReason !== undefined) {
791
+ state.truncated = true;
792
+ state.stoppedReason = stopReason;
793
+ return true;
794
+ }
795
+ return false;
796
+ }
797
+ function shouldIncludeEntry(entryType, normalized) {
798
+ return !normalized.skipSymlinks || entryType !== 'symlink';
799
+ }
800
+ function createCollectState() {
801
+ return {
802
+ results: [],
803
+ filesScanned: 0,
804
+ truncated: false,
805
+ skippedInaccessible: 0,
806
+ };
807
+ }
808
+ function buildSearchStream(root, pattern, excludePatterns, normalized, needsStats) {
809
+ const options = buildGlobOptions({
810
+ cwd: root,
811
+ pattern,
812
+ excludePatterns,
813
+ includeHidden: normalized.includeHidden,
814
+ baseNameMatch: normalized.baseNameMatch,
815
+ caseSensitiveMatch: true,
816
+ followSymbolicLinks: false,
817
+ onlyFiles: true,
818
+ stats: needsStats,
819
+ ...(normalized.maxDepth !== undefined
820
+ ? { maxDepth: normalized.maxDepth }
821
+ : {}),
822
+ });
823
+ return globEntries(options);
824
+ }
825
+ function buildCollectResult(state) {
826
+ const outcome = {
827
+ results: state.results,
828
+ filesScanned: state.filesScanned,
829
+ truncated: state.truncated,
830
+ skippedInaccessible: state.skippedInaccessible,
831
+ };
832
+ if (state.stoppedReason !== undefined) {
833
+ outcome.stoppedReason = state.stoppedReason;
834
+ }
835
+ return outcome;
836
+ }
837
+ function handleEntry(entry, entryType, needsStats, normalized, state) {
838
+ state.results.push(buildSearchFilesResult(entry, entryType, needsStats));
839
+ if (state.results.length >= normalized.maxResults) {
840
+ state.truncated = true;
841
+ state.stoppedReason = 'maxResults';
842
+ }
843
+ }
844
+ async function collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress) {
845
+ for await (const entry of stream) {
846
+ if (shouldStopCollecting(state, normalized, signal))
847
+ break;
848
+ state.filesScanned++;
849
+ reportPeriodicProgress(onProgress, state.filesScanned, {
850
+ total: normalized.maxFilesScanned,
851
+ throttleModulo: 25,
852
+ });
853
+ if (isEntryIgnoredByGitignore(gitignoreMatcher, root, entry.path, entry.relativePath)) {
854
+ continue;
855
+ }
856
+ const entryType = resolveEntryType(entry.dirent);
857
+ if (!shouldIncludeEntry(entryType, normalized)) {
858
+ continue;
859
+ }
860
+ const isAccessible = await isEntryAccessibleByType(entry.path, entryType, rootDirectories, signal, accessDeps);
861
+ if (!isAccessible) {
862
+ state.skippedInaccessible++;
863
+ continue;
864
+ }
865
+ handleEntry(entry, entryType, needsStats, normalized, state);
866
+ if (state.truncated)
867
+ break;
868
+ }
869
+ reportPeriodicProgress(onProgress, state.filesScanned, {
870
+ total: normalized.maxFilesScanned,
871
+ throttleModulo: 25,
872
+ force: true,
873
+ });
874
+ }
875
+ function isEntryIgnoredByGitignore(matcher, root, entryPath, relativePath) {
876
+ if (!matcher)
877
+ return false;
878
+ return isIgnoredByGitignore(matcher, root, entryPath, relativePath ? { relativePath } : {});
879
+ }
880
+ async function collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress) {
881
+ const needsStats = needsStatsForSort(normalized.sortBy);
882
+ const stream = buildSearchStream(root, pattern, excludePatterns, normalized, needsStats);
883
+ const state = createCollectState();
884
+ const rootDirectories = [root];
885
+ const accessDeps = {
886
+ normalizePath,
887
+ isPathWithinDirectories,
888
+ isSensitivePath,
889
+ validateSymlinkPath: validateExistingPathDetailed,
890
+ };
891
+ const gitignoreMatcher = normalized.respectGitignore
892
+ ? await loadRootGitignore(root, signal)
893
+ : null;
894
+ await collectFromStream(stream, root, rootDirectories, gitignoreMatcher, normalized, needsStats, state, signal, accessDeps, onProgress);
895
+ return buildCollectResult(state);
896
+ }
897
+ function buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible) {
898
+ const summary = {
899
+ matched: results.length,
900
+ truncated,
901
+ skippedInaccessible,
902
+ filesScanned,
903
+ };
904
+ return withOptionalStoppedReason(summary, stoppedReason);
905
+ }
906
+ function compareNameThenPath(a, b) {
907
+ const nameCompare = compareStringValues(a.name, b.name);
908
+ if (nameCompare !== 0)
909
+ return nameCompare;
910
+ return compareStringValues(a.path, b.path);
911
+ }
912
+ function comparePathThenName(a, b) {
913
+ const pathCompare = compareStringValues(a.path, b.path);
914
+ if (pathCompare !== 0)
915
+ return pathCompare;
916
+ return compareStringValues(a.name, b.name);
917
+ }
918
+ const SORT_COMPARATORS = {
919
+ size: (a, b) => compareOptionalNumberDesc(a.size, b.size, () => compareNameThenPath(a, b)),
920
+ modified: (a, b) => compareOptionalNumberDesc(a.modified?.getTime(), b.modified?.getTime(), () => compareNameThenPath(a, b)),
921
+ path: (a, b) => comparePathThenName(a, b),
922
+ name: (a, b) => compareNameThenPath(a, b),
923
+ };
924
+ export function sortSearchResults(results, sortBy) {
925
+ if (sortBy === 'name') {
926
+ stableSortByDerivedString(results, (item) => path.basename(item.path ?? ''), (left, right) => comparePathThenName(left, right));
927
+ return;
928
+ }
929
+ const comparator = SORT_COMPARATORS[sortBy];
930
+ results.sort(comparator);
931
+ }
932
+ async function runSearchFiles(root, pattern, excludePatterns, normalized, signal, onProgress) {
933
+ const { results, filesScanned, truncated, stoppedReason, skippedInaccessible, } = await collectSearchResults(root, pattern, excludePatterns, normalized, signal, onProgress);
934
+ sortSearchResults(results, normalized.sortBy);
935
+ return {
936
+ results,
937
+ summary: buildSearchSummary(results, filesScanned, truncated, stoppedReason, skippedInaccessible),
938
+ };
939
+ }
940
+ export async function searchFiles(basePath, pattern, excludePatterns = [], options = {}) {
941
+ const normalized = normalizeSearchFilesOptions(options);
942
+ return withTimedAbortSignal(options.signal, normalized.timeoutMs, async (signal) => {
943
+ const root = await validateExistingDirectory(basePath, signal);
944
+ const { results, summary } = await runSearchFiles(root, pattern, excludePatterns, normalized, signal, options.onProgress);
945
+ return {
946
+ basePath: root,
947
+ pattern,
948
+ results,
949
+ summary,
950
+ };
951
+ });
952
+ }
953
+ const matcherCache = new Map();
954
+ const MAX_MATCHER_CACHE_SIZE = 100;
955
+ function getMatcherCacheKey(pattern, options) {
956
+ const cs = options.caseSensitive ? '1' : '0';
957
+ const ww = options.wholeWord ? '1' : '0';
958
+ const lit = options.isLiteral ? '1' : '0';
959
+ return `${pattern}|${cs}|${ww}|${lit}`;
960
+ }
961
+ function getCachedMatcher(pattern, options) {
962
+ const key = getMatcherCacheKey(pattern, options);
963
+ const cached = matcherCache.get(key);
964
+ if (cached) {
965
+ refreshMatcherCacheEntry(key, cached);
966
+ return cached;
967
+ }
968
+ const matcher = buildMatcher(pattern, options);
969
+ refreshMatcherCacheEntry(key, matcher);
970
+ evictOldestMatcherIfNeeded();
971
+ return matcher;
972
+ }
973
+ function refreshMatcherCacheEntry(key, matcher) {
974
+ matcherCache.delete(key);
975
+ matcherCache.set(key, matcher);
976
+ }
977
+ function evictOldestMatcherIfNeeded() {
978
+ if (matcherCache.size <= MAX_MATCHER_CACHE_SIZE)
979
+ return;
980
+ const firstKey = matcherCache.keys().next().value;
981
+ if (firstKey !== undefined) {
982
+ matcherCache.delete(firstKey);
983
+ }
984
+ }
985
+ const cancelledRequests = new Set();
986
+ const activeRequests = new Set();
987
+ let shuttingDown = false;
988
+ function maybeFinishShutdown() {
989
+ if (!shuttingDown)
990
+ return;
991
+ if (activeRequests.size > 0)
992
+ return;
993
+ parentPort?.close();
994
+ }
995
+ function consumeCancelled(id) {
996
+ if (!cancelledRequests.has(id)) {
997
+ return false;
998
+ }
999
+ cancelledRequests.delete(id);
1000
+ return true;
1001
+ }
1002
+ function markCancelledIfActive(id) {
1003
+ if (activeRequests.has(id)) {
1004
+ cancelledRequests.add(id);
1005
+ }
1006
+ }
1007
+ function buildScanResponse(id, result) {
1008
+ return {
1009
+ type: 'result',
1010
+ id,
1011
+ result,
1012
+ };
1013
+ }
1014
+ function buildErrorResponse(id, error) {
1015
+ return {
1016
+ type: 'error',
1017
+ id,
1018
+ error: formatUnknownErrorMessage(error),
1019
+ };
1020
+ }
1021
+ async function handleScanRequest(request) {
1022
+ const { id, resolvedPath, requestedPath, pattern, matcherOptions, scanOptions, maxMatches, } = request;
1023
+ if (consumeCancelled(id))
1024
+ return;
1025
+ activeRequests.add(id);
1026
+ const endMeasure = startPerfMeasure('searchWorker.scan', {
1027
+ maxMatches,
1028
+ });
1029
+ let ok = false;
1030
+ try {
1031
+ const matcher = getCachedMatcher(pattern, matcherOptions);
1032
+ const isCancelled = () => cancelledRequests.has(id);
1033
+ const result = await scanFileInWorker(resolvedPath, requestedPath, matcher, scanOptions, maxMatches, isCancelled, isProbablyBinary);
1034
+ if (consumeCancelled(id))
1035
+ return;
1036
+ parentPort?.postMessage(buildScanResponse(id, result));
1037
+ ok = true;
1038
+ }
1039
+ catch (err) {
1040
+ if (consumeCancelled(id))
1041
+ return;
1042
+ parentPort?.postMessage(buildErrorResponse(id, err));
1043
+ }
746
1044
  finally {
747
- cleanup();
1045
+ activeRequests.delete(id);
1046
+ cancelledRequests.delete(id);
1047
+ endMeasure?.(ok);
1048
+ maybeFinishShutdown();
1049
+ }
1050
+ }
1051
+ function handleMessage(message) {
1052
+ switch (message.type) {
1053
+ case 'scan':
1054
+ if (shuttingDown)
1055
+ return;
1056
+ void handleScanRequest(message);
1057
+ break;
1058
+ case 'cancel':
1059
+ markCancelledIfActive(message.id);
1060
+ break;
1061
+ case 'shutdown':
1062
+ shuttingDown = true;
1063
+ for (const id of activeRequests) {
1064
+ markCancelledIfActive(id);
1065
+ }
1066
+ maybeFinishShutdown();
1067
+ break;
1068
+ }
1069
+ }
1070
+ if (parentPort) {
1071
+ parentPort.on('message', handleMessage);
1072
+ const data = workerData;
1073
+ if (data?.debug) {
1074
+ console.error(`[SearchWorker] Started with threadId=${String(threadId)}`);
748
1075
  }
749
1076
  }
@@ -1,5 +1,5 @@
1
1
  import type { Stats } from 'node:fs';
2
- import type { DirentLike } from './common.js';
2
+ import type { DirentLike } from './core.js';
3
3
  interface GlobEntry {
4
4
  path: string;
5
5
  relativePath?: string;
@@ -20,4 +20,21 @@ export interface GlobEntriesOptions {
20
20
  suppressErrors?: boolean;
21
21
  }
22
22
  export declare function globEntries(options: GlobEntriesOptions): AsyncGenerator<GlobEntry>;
23
+ export interface GlobConfig {
24
+ cwd: string;
25
+ pattern: string;
26
+ excludePatterns?: readonly string[];
27
+ includeHidden?: boolean;
28
+ baseNameMatch?: boolean;
29
+ caseSensitiveMatch?: boolean;
30
+ followSymbolicLinks?: boolean;
31
+ onlyFiles?: boolean;
32
+ stats?: boolean;
33
+ maxDepth?: number;
34
+ suppressErrors?: boolean;
35
+ }
36
+ /**
37
+ * Builds standard options for globEntries to ensure consistency across search tools.
38
+ */
39
+ export declare function buildGlobOptions(config: GlobConfig): Parameters<typeof globEntries>[0];
23
40
  export {};