@j0hanz/filesystem-mcp 2.5.2 → 2.6.1

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 (147) hide show
  1. package/README.md +26 -19
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +12 -3
  4. package/dist/cli.js.map +1 -1
  5. package/dist/core/concurrency.d.ts +7 -4
  6. package/dist/core/concurrency.d.ts.map +1 -1
  7. package/dist/core/concurrency.js +9 -4
  8. package/dist/core/concurrency.js.map +1 -1
  9. package/dist/core/config.d.ts +1 -1
  10. package/dist/core/config.d.ts.map +1 -1
  11. package/dist/core/config.js.map +1 -1
  12. package/dist/core/cursor.d.ts.map +1 -1
  13. package/dist/core/cursor.js +11 -1
  14. package/dist/core/cursor.js.map +1 -1
  15. package/dist/core/diff.d.ts +15 -2
  16. package/dist/core/diff.d.ts.map +1 -1
  17. package/dist/core/diff.js +38 -7
  18. package/dist/core/diff.js.map +1 -1
  19. package/dist/core/errors.d.ts +4 -5
  20. package/dist/core/errors.d.ts.map +1 -1
  21. package/dist/core/errors.js +14 -14
  22. package/dist/core/errors.js.map +1 -1
  23. package/dist/core/file-uri.d.ts +23 -9
  24. package/dist/core/file-uri.d.ts.map +1 -1
  25. package/dist/core/file-uri.js +29 -14
  26. package/dist/core/file-uri.js.map +1 -1
  27. package/dist/core/fmt.d.ts +3 -1
  28. package/dist/core/fmt.d.ts.map +1 -1
  29. package/dist/core/fmt.js +3 -3
  30. package/dist/core/fmt.js.map +1 -1
  31. package/dist/core/fs.d.ts +2 -8
  32. package/dist/core/fs.d.ts.map +1 -1
  33. package/dist/core/fs.js +32 -20
  34. package/dist/core/fs.js.map +1 -1
  35. package/dist/core/glob.d.ts +1 -1
  36. package/dist/core/glob.d.ts.map +1 -1
  37. package/dist/core/glob.js +77 -80
  38. package/dist/core/glob.js.map +1 -1
  39. package/dist/core/mime.d.ts +1 -0
  40. package/dist/core/mime.d.ts.map +1 -1
  41. package/dist/core/mime.js +1 -1
  42. package/dist/core/mime.js.map +1 -1
  43. package/dist/core/observability.js +1 -1
  44. package/dist/core/observability.js.map +1 -1
  45. package/dist/core/path-completer.d.ts +2 -1
  46. package/dist/core/path-completer.d.ts.map +1 -1
  47. package/dist/core/path-completer.js +14 -13
  48. package/dist/core/path-completer.js.map +1 -1
  49. package/dist/core/path-discovery.d.ts.map +1 -1
  50. package/dist/core/path-discovery.js +29 -21
  51. package/dist/core/path-discovery.js.map +1 -1
  52. package/dist/core/path-utils.d.ts +8 -0
  53. package/dist/core/path-utils.d.ts.map +1 -1
  54. package/dist/core/path-utils.js +24 -17
  55. package/dist/core/path-utils.js.map +1 -1
  56. package/dist/core/path.d.ts +13 -1
  57. package/dist/core/path.d.ts.map +1 -1
  58. package/dist/core/path.js +27 -11
  59. package/dist/core/path.js.map +1 -1
  60. package/dist/core/read.d.ts +1 -0
  61. package/dist/core/read.d.ts.map +1 -1
  62. package/dist/core/read.js +23 -12
  63. package/dist/core/read.js.map +1 -1
  64. package/dist/core/schema.d.ts +10 -0
  65. package/dist/core/schema.d.ts.map +1 -1
  66. package/dist/core/schema.js +20 -6
  67. package/dist/core/schema.js.map +1 -1
  68. package/dist/core/search.d.ts +12 -4
  69. package/dist/core/search.d.ts.map +1 -1
  70. package/dist/core/search.js +42 -26
  71. package/dist/core/search.js.map +1 -1
  72. package/dist/core/sensitive.d.ts +7 -0
  73. package/dist/core/sensitive.d.ts.map +1 -1
  74. package/dist/core/sensitive.js +21 -2
  75. package/dist/core/sensitive.js.map +1 -1
  76. package/dist/core/watcher-registry.d.ts.map +1 -1
  77. package/dist/core/watcher-registry.js +24 -9
  78. package/dist/core/watcher-registry.js.map +1 -1
  79. package/dist/instructions.js +4 -4
  80. package/dist/instructions.js.map +1 -1
  81. package/dist/resources.d.ts.map +1 -1
  82. package/dist/resources.js +21 -3
  83. package/dist/resources.js.map +1 -1
  84. package/dist/server.d.ts +1 -1
  85. package/dist/server.d.ts.map +1 -1
  86. package/dist/server.js +16 -3
  87. package/dist/server.js.map +1 -1
  88. package/dist/tools/batch.d.ts +2 -5
  89. package/dist/tools/batch.d.ts.map +1 -1
  90. package/dist/tools/batch.js +1 -2
  91. package/dist/tools/batch.js.map +1 -1
  92. package/dist/tools/create.d.ts.map +1 -1
  93. package/dist/tools/create.js +32 -19
  94. package/dist/tools/create.js.map +1 -1
  95. package/dist/tools/define.d.ts +1 -1
  96. package/dist/tools/define.d.ts.map +1 -1
  97. package/dist/tools/define.js.map +1 -1
  98. package/dist/tools/delete.d.ts.map +1 -1
  99. package/dist/tools/delete.js +2 -7
  100. package/dist/tools/delete.js.map +1 -1
  101. package/dist/tools/diff.d.ts.map +1 -1
  102. package/dist/tools/diff.js +8 -5
  103. package/dist/tools/diff.js.map +1 -1
  104. package/dist/tools/edit.d.ts.map +1 -1
  105. package/dist/tools/edit.js +89 -46
  106. package/dist/tools/edit.js.map +1 -1
  107. package/dist/tools/find-files.d.ts.map +1 -1
  108. package/dist/tools/find-files.js +1 -2
  109. package/dist/tools/find-files.js.map +1 -1
  110. package/dist/tools/list-roots.d.ts.map +1 -1
  111. package/dist/tools/list-roots.js +1 -3
  112. package/dist/tools/list-roots.js.map +1 -1
  113. package/dist/tools/list.js +4 -4
  114. package/dist/tools/list.js.map +1 -1
  115. package/dist/tools/move.d.ts +2 -0
  116. package/dist/tools/move.d.ts.map +1 -1
  117. package/dist/tools/move.js +10 -4
  118. package/dist/tools/move.js.map +1 -1
  119. package/dist/tools/patch.d.ts.map +1 -1
  120. package/dist/tools/patch.js +11 -3
  121. package/dist/tools/patch.js.map +1 -1
  122. package/dist/tools/read.d.ts.map +1 -1
  123. package/dist/tools/read.js +7 -29
  124. package/dist/tools/read.js.map +1 -1
  125. package/dist/tools/replace-text.d.ts.map +1 -1
  126. package/dist/tools/replace-text.js +20 -17
  127. package/dist/tools/replace-text.js.map +1 -1
  128. package/dist/tools/search-text.d.ts.map +1 -1
  129. package/dist/tools/search-text.js +3 -1
  130. package/dist/tools/search-text.js.map +1 -1
  131. package/dist/tools/stat.d.ts.map +1 -1
  132. package/dist/tools/stat.js +4 -9
  133. package/dist/tools/stat.js.map +1 -1
  134. package/dist/transport/http-policy.d.ts +3 -3
  135. package/dist/transport/http-policy.d.ts.map +1 -1
  136. package/dist/transport/http-policy.js +10 -9
  137. package/dist/transport/http-policy.js.map +1 -1
  138. package/dist/transport/http.d.ts.map +1 -1
  139. package/dist/transport/http.js +38 -14
  140. package/dist/transport/http.js.map +1 -1
  141. package/dist/transport/shared.d.ts +2 -2
  142. package/dist/transport/shared.d.ts.map +1 -1
  143. package/dist/transport/shared.js.map +1 -1
  144. package/dist/transport/stdio.d.ts +1 -1
  145. package/dist/transport/stdio.js +1 -1
  146. package/dist/transport/stdio.js.map +1 -1
  147. package/package.json +1 -1
package/dist/core/fs.js CHANGED
@@ -2,9 +2,10 @@ import { randomUUID } from 'node:crypto';
2
2
  import { chmod as fsChmod, cp as fsCp, lstat as fsLstat, mkdir as fsMkdir, open as fsOpen, opendir as fsOpendir, readFile as fsReadFile, readlink as fsReadlink, rename as fsRename, rm as fsRm, rmdir as fsRmdir, stat as fsStat, unlink as fsUnlink, writeFile as fsWriteFile, } from 'node:fs/promises';
3
3
  import { dirname, isAbsolute, resolve } from 'node:path';
4
4
  import { withAbort } from "./concurrency.js";
5
- import { ErrorCode, formatUnknownErrorMessage, FsError, isFsError, isNodeError } from "./errors.js";
5
+ import { ErrorCode, formatUnknownErrorMessage, FsError, isFsError, isNodeError, isNotFoundErrno, } from "./errors.js";
6
6
  import { detectMimeFromContent } from "./mime.js";
7
7
  import { Logger } from "./observability.js";
8
+ import { respellCaseOnlyTarget } from "./path-utils.js";
8
9
  import { assertFileStats, createTooLargeError, readFileWithStats } from "./read.js";
9
10
  import { getMaxTextFileSize } from "./util.js";
10
11
  // ─── Domain primitives ────────────────────────────────────────────────────────
@@ -24,14 +25,14 @@ async function resolveForWrite(pathGuard, filePath) {
24
25
  }
25
26
  }
26
27
  catch (error) {
27
- if (!isNodeError(error) || error.code !== 'ENOENT') {
28
+ if (!isNotFoundErrno(error)) {
28
29
  throw error;
29
30
  }
30
31
  }
31
32
  return validPath;
32
33
  }
33
34
  async function atomicWriteFile(filePath, content, pathGuard, options = {}) {
34
- const { encoding = 'utf-8', signal } = options;
35
+ const { signal } = options;
35
36
  const validPath = await resolveForWrite(pathGuard, filePath);
36
37
  const tempSuffix = randomUUID().replace(/-/g, '').slice(0, 12);
37
38
  const tempPath = `${validPath}.${tempSuffix}.tmp`;
@@ -47,17 +48,30 @@ async function atomicWriteFile(filePath, content, pathGuard, options = {}) {
47
48
  // Anything else (EACCES, EIO) means the mode about to be overwritten could
48
49
  // not be read, and the write will silently widen the file — say so rather
49
50
  // than swallowing it.
50
- if (!isNodeError(error) || error.code !== 'ENOENT') {
51
+ if (!isNotFoundErrno(error)) {
51
52
  Logger.warn(`atomicWriteFile: cannot read the existing mode of ${validPath}; the write will use the default mode: ${formatUnknownErrorMessage(error)}`);
52
53
  }
53
54
  }
54
55
  try {
55
56
  signal?.throwIfAborted();
56
- await fsWriteFile(tempPath, content, { encoding, signal });
57
+ // Create the temp file already narrowed to the target's mode: a 0600
58
+ // file's new bytes must never sit at 0644 while the write is in flight.
59
+ // `wx` fails on a name collision instead of overwriting; the chmod below
60
+ // still restores bits the umask masked off at creation.
61
+ await fsWriteFile(tempPath, content, {
62
+ signal,
63
+ flag: 'wx',
64
+ ...(existingMode !== undefined ? { mode: existingMode } : {}),
65
+ });
57
66
  if (existingMode !== undefined) {
58
67
  await fsChmod(tempPath, existingMode);
59
68
  }
60
- await withAbort(fsRename(tempPath, validPath), signal);
69
+ // Last cancellation point. The rename IS the commit: once it starts the
70
+ // target may already be replaced, so it is never raced against the
71
+ // signal — a withAbort race would report a finished write as failed, and
72
+ // a client retry would apply it twice (same reasoning as appendFile).
73
+ signal?.throwIfAborted();
74
+ await fsRename(tempPath, validPath);
61
75
  }
62
76
  catch (error) {
63
77
  try {
@@ -70,9 +84,6 @@ async function atomicWriteFile(filePath, content, pathGuard, options = {}) {
70
84
  }
71
85
  return { validPath };
72
86
  }
73
- export function isHidden(name) {
74
- return name.startsWith('.');
75
- }
76
87
  const CHAR_LF = 10;
77
88
  const LINE_COUNT_CHUNK_BYTES = 64 * 1024;
78
89
  /**
@@ -84,7 +95,7 @@ const LINE_COUNT_CHUNK_BYTES = 64 * 1024;
84
95
  * Matches read.ts's countLines semantics: a trailing newline does not open
85
96
  * a new line, an unterminated final line counts, an empty file is 0.
86
97
  */
87
- export async function countFileLines(filePath, signal) {
98
+ export async function countFileLines(filePath) {
88
99
  const handle = await fsOpen(filePath, 'r');
89
100
  try {
90
101
  let newlines = 0;
@@ -92,7 +103,6 @@ export async function countFileLines(filePath, signal) {
92
103
  let totalBytes = 0;
93
104
  const chunk = Buffer.alloc(LINE_COUNT_CHUNK_BYTES);
94
105
  for (;;) {
95
- signal?.throwIfAborted();
96
106
  const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
97
107
  if (bytesRead === 0)
98
108
  break;
@@ -144,7 +154,10 @@ export class GuardedFileSystem {
144
154
  // Not validateExistingPath: that resolves through a symlink, so renaming a
145
155
  // link would rename its target and leave the link dangling.
146
156
  const validOld = await this.pathGuard.validatePathForDelete(oldPath);
147
- const validNew = await this.pathGuard.validatePathForWrite(newPath);
157
+ let validNew = await this.pathGuard.validatePathForWrite(newPath);
158
+ // Renaming a path onto itself is only meaningful as a case-only rename.
159
+ if (validOld === validNew)
160
+ validNew = respellCaseOnlyTarget(newPath, validNew);
148
161
  await fsRename(validOld, validNew);
149
162
  }
150
163
  async writeFile(filePath, content, options = {}) {
@@ -157,7 +170,7 @@ export class GuardedFileSystem {
157
170
  * there — inode and mode survive by construction.
158
171
  */
159
172
  async appendFile(filePath, content, options = {}) {
160
- const { encoding = 'utf-8', signal } = options;
173
+ const { signal } = options;
161
174
  const validPath = await resolveForWrite(this.pathGuard, filePath);
162
175
  // Non-regular targets must be rejected before the open: 'a' on a FIFO
163
176
  // (POSIX) blocks until a reader appears, and fsOpen takes no signal, so
@@ -170,7 +183,7 @@ export class GuardedFileSystem {
170
183
  assertFileStats(validPath, await fsLstat(validPath));
171
184
  }
172
185
  catch (error) {
173
- if (!isNodeError(error) || error.code !== 'ENOENT')
186
+ if (!isNotFoundErrno(error))
174
187
  throw error;
175
188
  existed = false;
176
189
  }
@@ -209,7 +222,7 @@ export class GuardedFileSystem {
209
222
  // signal would trade that for a silently-completed write; a client
210
223
  // that retries is double-appending either way, so the abortable
211
224
  // write stays.
212
- await handle.writeFile(content, { encoding, signal });
225
+ await handle.writeFile(content, { signal });
213
226
  }
214
227
  catch (error) {
215
228
  if (created) {
@@ -229,11 +242,10 @@ export class GuardedFileSystem {
229
242
  * file — an append's result must describe the resulting file (which may be
230
243
  * a multi-GB log), not the chunk that was appended.
231
244
  */
232
- async readLeadingSample(filePath, size, options = {}) {
233
- const { validPath } = await this.stat(filePath, options);
245
+ async readLeadingSample(filePath, size) {
246
+ const { validPath } = await this.stat(filePath);
234
247
  const handle = await fsOpen(validPath, 'r');
235
248
  try {
236
- options.signal?.throwIfAborted();
237
249
  const buffer = Buffer.alloc(size);
238
250
  const { bytesRead } = await handle.read(buffer, 0, size, 0);
239
251
  return buffer.subarray(0, bytesRead);
@@ -272,6 +284,7 @@ export class GuardedFileSystem {
272
284
  }
273
285
  const { content } = await readFileWithStats(filePath, validPath, stats, {
274
286
  kind: 'full',
287
+ strictUtf8: true,
275
288
  ...(options?.signal ? { signal: options.signal } : {}),
276
289
  });
277
290
  return { validPath, content, stats };
@@ -330,8 +343,7 @@ export async function destExists(fs, path, label) {
330
343
  return true;
331
344
  }
332
345
  catch (err) {
333
- const missing = (isNodeError(err) && err.code === 'ENOENT') ||
334
- (isFsError(err) && err.code === ErrorCode.NOT_FOUND);
346
+ const missing = isNotFoundErrno(err) || (isFsError(err) && err.code === ErrorCode.NOT_FOUND);
335
347
  if (!missing) {
336
348
  Logger.warn(`${label}: dest stat failed unexpectedly for "${path}": ${String(err)}`);
337
349
  }
@@ -1 +1 @@
1
- {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/core/fs.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { Stats } from 'node:fs';\nimport type { FileHandle } from 'node:fs/promises';\nimport {\n chmod as fsChmod,\n cp as fsCp,\n lstat as fsLstat,\n mkdir as fsMkdir,\n open as fsOpen,\n opendir as fsOpendir,\n readFile as fsReadFile,\n readlink as fsReadlink,\n rename as fsRename,\n rm as fsRm,\n rmdir as fsRmdir,\n stat as fsStat,\n unlink as fsUnlink,\n writeFile as fsWriteFile,\n} from 'node:fs/promises';\nimport { dirname, isAbsolute, resolve } from 'node:path';\n\nimport { withAbort } from './concurrency.ts';\nimport { ErrorCode, formatUnknownErrorMessage, FsError, isFsError, isNodeError } from './errors.ts';\nimport { detectMimeFromContent } from './mime.ts';\nimport { Logger } from './observability.ts';\nimport type { EntryType as FileType } from './path-utils.ts';\nimport type { PathGuard } from './path.ts';\nimport type { ReadFileResult, ReadSpec } from './read.ts';\nimport { assertFileStats, createTooLargeError, readFileWithStats } from './read.ts';\nimport { getMaxTextFileSize } from './util.ts';\n\nexport type { FileType };\nexport type { Stats };\nexport type { FileHandle };\n\n// ─── Domain primitives ────────────────────────────────────────────────────────\n\n/**\n * validatePathForWrite + symlink resolution: a write aimed at a link must land\n * on its revalidated target, not create a new file beside the link. ENOENT is\n * the normal new-file case, so it falls through with the original path.\n */\nasync function resolveForWrite(pathGuard: PathGuard, filePath: string): Promise<string> {\n let validPath = await pathGuard.validatePathForWrite(filePath);\n\n try {\n const stats = await fsLstat(validPath);\n if (stats.isSymbolicLink()) {\n const target = await fsReadlink(validPath);\n const resolvedTarget = isAbsolute(target) ? target : resolve(dirname(validPath), target);\n validPath = await pathGuard.validatePathForWrite(resolvedTarget);\n }\n } catch (error) {\n if (!isNodeError(error) || error.code !== 'ENOENT') {\n throw error;\n }\n }\n\n return validPath;\n}\n\nasync function atomicWriteFile(\n filePath: string,\n content: string,\n pathGuard: PathGuard,\n options: { encoding?: BufferEncoding; signal?: AbortSignal | undefined } = {},\n): Promise<{ validPath: string }> {\n const { encoding = 'utf-8', signal } = options;\n const validPath = await resolveForWrite(pathGuard, filePath);\n\n const tempSuffix = randomUUID().replace(/-/g, '').slice(0, 12);\n const tempPath = `${validPath}.${tempSuffix}.tmp`;\n\n // The rename below swaps in the temp file's inode, so the target would\n // inherit fsWriteFile's default 0o666 & ~umask — silently widening a 0600\n // file to 0644 on every write. Carry the existing mode across instead.\n let existingMode: number | undefined;\n try {\n existingMode = (await fsStat(validPath)).mode & 0o777;\n } catch (error) {\n // ENOENT is the normal new-file case: the default mode is correct there.\n // Anything else (EACCES, EIO) means the mode about to be overwritten could\n // not be read, and the write will silently widen the file — say so rather\n // than swallowing it.\n if (!isNodeError(error) || error.code !== 'ENOENT') {\n Logger.warn(\n `atomicWriteFile: cannot read the existing mode of ${validPath}; the write will use the default mode: ${formatUnknownErrorMessage(error)}`,\n );\n }\n }\n\n try {\n signal?.throwIfAborted();\n await fsWriteFile(tempPath, content, { encoding, signal });\n if (existingMode !== undefined) {\n await fsChmod(tempPath, existingMode);\n }\n await withAbort(fsRename(tempPath, validPath), signal);\n } catch (error) {\n try {\n await fsUnlink(tempPath);\n } catch (cleanupError) {\n Logger.warn(\n `Failed to clean up temp file ${tempPath} after write error (${formatUnknownErrorMessage(error)}): ${formatUnknownErrorMessage(cleanupError)}`,\n );\n }\n throw error;\n }\n return { validPath };\n}\n\nexport function isHidden(name: string): boolean {\n return name.startsWith('.');\n}\n\nconst CHAR_LF = 10;\nconst LINE_COUNT_CHUNK_BYTES = 64 * 1024;\n\n/**\n * Line count for a file on disk, counting newline bytes in fixed 64 KiB\n * chunks so an arbitrarily large file (one append's result metadata) costs\n * bounded memory. NOT FileHandle.readLines: it decodes each physical line\n * whole, so a multi-GB single-line log OOMs the server after the append has\n * already landed — a reported failure invites a retry that appends twice.\n * Matches read.ts's countLines semantics: a trailing newline does not open\n * a new line, an unterminated final line counts, an empty file is 0.\n */\nexport async function countFileLines(filePath: string, signal?: AbortSignal): Promise<number> {\n const handle = await fsOpen(filePath, 'r');\n try {\n let newlines = 0;\n let lastByte = 0;\n let totalBytes = 0;\n const chunk = Buffer.alloc(LINE_COUNT_CHUNK_BYTES);\n for (;;) {\n signal?.throwIfAborted();\n const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);\n if (bytesRead === 0) break;\n totalBytes += bytesRead;\n for (let i = 0; i < bytesRead; i++) {\n const byte = chunk[i] ?? 0;\n if (byte === CHAR_LF) newlines++;\n lastByte = byte;\n }\n }\n // totalBytes is the empty-file sentinel: a NUL (0x00) final byte is real\n // content and must count as an unterminated final line, not \"empty\".\n return totalBytes === 0 ? 0 : lastByte === CHAR_LF ? newlines : newlines + 1;\n } finally {\n await handle.close();\n }\n}\n\nexport class GuardedFileSystem {\n readonly pathGuard: PathGuard;\n\n constructor(pathGuard: PathGuard) {\n this.pathGuard = pathGuard;\n }\n\n async stat(\n filePath: string,\n options?: { signal?: AbortSignal | undefined },\n ): Promise<{ stats: Stats; validPath: string }> {\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n const stats = await withAbort(fsStat(validPath), options?.signal);\n return { stats, validPath };\n }\n\n async lstat(\n filePath: string,\n options?: { signal?: AbortSignal },\n ): Promise<{ stats: Stats; validPath: string }> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n const stats = await withAbort(fsLstat(validPath), options?.signal);\n return { stats, validPath };\n }\n\n async readlink(\n filePath: string,\n options?: Parameters<typeof fsReadlink>[1],\n ): Promise<{ linkString: string; validPath: string }> {\n // validateExistingPath resolves through the symlink, so readlink would always\n // be handed the final target — a regular file — and fail EINVAL. This guard\n // keeps the link itself while still enforcing containment and the denylist.\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n const raw = await fsReadlink(validPath, options);\n const linkString = Buffer.isBuffer(raw) ? raw.toString('utf-8') : raw;\n return { linkString, validPath };\n }\n\n async mkdir(filePath: string, options?: Parameters<typeof fsMkdir>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForWrite(filePath);\n await fsMkdir(validPath, options);\n }\n\n async rename(oldPath: string, newPath: string): Promise<void> {\n // Not validateExistingPath: that resolves through a symlink, so renaming a\n // link would rename its target and leave the link dangling.\n const validOld = await this.pathGuard.validatePathForDelete(oldPath);\n const validNew = await this.pathGuard.validatePathForWrite(newPath);\n await fsRename(validOld, validNew);\n }\n\n async writeFile(\n filePath: string,\n content: string,\n options: { encoding?: BufferEncoding; signal?: AbortSignal | undefined } = {},\n ): Promise<{ validPath: string }> {\n return atomicWriteFile(filePath, content, this.pathGuard, options);\n }\n\n /**\n * Append to the existing file (created if missing). Deliberately NOT the\n * atomicWriteFile temp+rename dance: the rename would swap in a fresh inode,\n * and the whole point of appending is to extend the file that's already\n * there — inode and mode survive by construction.\n */\n async appendFile(\n filePath: string,\n content: string,\n options: { encoding?: BufferEncoding; signal?: AbortSignal | undefined } = {},\n ): Promise<{ validPath: string }> {\n const { encoding = 'utf-8', signal } = options;\n const validPath = await resolveForWrite(this.pathGuard, filePath);\n // Non-regular targets must be rejected before the open: 'a' on a FIFO\n // (POSIX) blocks until a reader appears, and fsOpen takes no signal, so\n // the call would hang unabortable and pin its concurrency slot. The\n // overwrite path can't hang — it writes a temp file and renames over.\n // resolveForWrite has already resolved any symlink to its target, so the\n // lstat here doubles as the new-vs-existing probe for the cleanup below.\n let existed = true;\n try {\n assertFileStats(validPath, await fsLstat(validPath));\n } catch (error) {\n if (!isNodeError(error) || error.code !== 'ENOENT') throw error;\n existed = false;\n }\n // fs.promises.appendFile takes no signal, so a withAbort race would\n // report failure while the append still lands — a client retry then\n // appends twice. A handle opened with 'a' gets a genuinely abortable\n // FileHandle.writeFile instead, same as the writeFile path. 'a' on a\n // missing target CREATES it, so: check the signal before the open, and\n // if the call then fails after creating the file, remove what it made —\n // an aborted or errored call must not leave a phantom file behind.\n // The pre-open lstat only proves the file was missing a moment ago:\n // 'ax' makes creation exclusive, so a racer that created it in between\n // fails EEXIST, reopens plainly, and never claims cleanup rights over\n // the winner's file.\n signal?.throwIfAborted();\n let created = false;\n let handle: FileHandle;\n if (existed) {\n handle = await fsOpen(validPath, 'a');\n } else {\n try {\n handle = await fsOpen(validPath, 'ax');\n created = true;\n } catch (error) {\n if (!isNodeError(error) || error.code !== 'EEXIST') throw error;\n handle = await fsOpen(validPath, 'a');\n }\n }\n try {\n signal?.throwIfAborted();\n // Ceiling: an append is not transactional, so an abort landing\n // mid-write can leave a prefix of the content on disk. Unwiring the\n // signal would trade that for a silently-completed write; a client\n // that retries is double-appending either way, so the abortable\n // write stays.\n await handle.writeFile(content, { encoding, signal });\n } catch (error) {\n if (created) {\n await fsUnlink(validPath).catch((unlinkError: unknown) => {\n Logger.warn(\n `Failed to remove newly created ${validPath} after a failed append: ${formatUnknownErrorMessage(unlinkError)}`,\n );\n });\n }\n throw error;\n } finally {\n await handle.close();\n }\n return { validPath };\n }\n\n /**\n * Leading bytes of a file, for MIME sniffing without loading the whole\n * file — an append's result must describe the resulting file (which may be\n * a multi-GB log), not the chunk that was appended.\n */\n async readLeadingSample(\n filePath: string,\n size: number,\n options: { signal?: AbortSignal | undefined } = {},\n ): Promise<Buffer> {\n const { validPath } = await this.stat(filePath, options);\n const handle = await fsOpen(validPath, 'r');\n try {\n options.signal?.throwIfAborted();\n const buffer = Buffer.alloc(size);\n const { bytesRead } = await handle.read(buffer, 0, size, 0);\n return buffer.subarray(0, bytesRead);\n } finally {\n await handle.close();\n }\n }\n\n async rm(filePath: string, options?: Parameters<typeof fsRm>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n await fsRm(validPath, options);\n }\n\n async rmdir(filePath: string, options?: Parameters<typeof fsRmdir>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n await fsRmdir(validPath, options);\n }\n\n async cp(\n source: string,\n destination: string,\n options?: Parameters<typeof fsCp>[2],\n ): Promise<void> {\n // As in rename: keep the link itself so callers passing verbatimSymlinks\n // actually copy the link rather than a dereferenced target.\n const validSource = await this.pathGuard.validatePathForDelete(source);\n const validDest = await this.pathGuard.validatePathForWrite(destination);\n await fsCp(validSource, validDest, options);\n }\n\n async readFile(filePath: string, spec: ReadSpec): Promise<ReadFileResult> {\n spec.signal?.throwIfAborted();\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n const stats = await withAbort(fsStat(validPath), spec.signal);\n return readFileWithStats(filePath, validPath, stats, spec);\n }\n\n async readEditableText(\n filePath: string,\n options?: { signal?: AbortSignal; tool?: string },\n ): Promise<{ validPath: string; content: string; stats: Stats }> {\n const { stats, validPath } = await this.stat(filePath, options);\n const maxSize = getMaxTextFileSize();\n if (stats.size > maxSize) {\n // `tool` labels the TOO_LARGE message so the client sees which tool refused.\n throw new FsError(\n ErrorCode.TOO_LARGE,\n `File too large for ${options?.tool ?? 'edit'} (${stats.size} bytes > ${maxSize} bytes)`,\n filePath,\n { size: stats.size, maxFileSize: maxSize },\n );\n }\n const { content } = await readFileWithStats(filePath, validPath, stats, {\n kind: 'full',\n ...(options?.signal ? { signal: options.signal } : {}),\n });\n return { validPath, content, stats };\n }\n\n async readRaw(\n filePath: string,\n options?: { signal?: AbortSignal },\n ): Promise<{ content: Buffer; mimeType: string; isBinary: boolean }> {\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n const stats = await withAbort(fsStat(validPath), options?.signal);\n assertFileStats(filePath, stats);\n // Enforce size limit before reading to avoid loading large files into memory.\n // Binary detection is best-effort, but this is a hard limit.\n const maxTextFileSize = getMaxTextFileSize();\n if (stats.size > maxTextFileSize) {\n throw createTooLargeError(stats.size, maxTextFileSize, filePath);\n }\n const content = await withAbort(fsReadFile(validPath), options?.signal);\n const mimeInfo = detectMimeFromContent(validPath, content);\n return {\n content,\n mimeType: mimeInfo.mimeType,\n isBinary: mimeInfo.kind !== 'text',\n };\n }\n\n async open(filePath: string): Promise<FileHandle> {\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n return fsOpen(validPath, 'r');\n }\n\n // Single resolution + stat: validateExistingPathDetailed resolves the real\n // path (following symlinks, re-checking sensitivity) once, then we stat the\n // resolved target. Replaces the tool-side validateExistingPathDetailed +\n // fs.stat pair that re-validated through the free stat() guard.\n async statDetailed(\n filePath: string,\n options?: { signal?: AbortSignal },\n ): Promise<{ requestedPath: string; isSymlink: boolean; stats: Stats }> {\n const { requestedPath, resolvedPath, isSymlink } =\n await this.pathGuard.validateExistingPathDetailed(filePath);\n const stats = await withAbort(fsStat(resolvedPath), options?.signal);\n return { requestedPath, isSymlink, stats };\n }\n\n async hasChildrenUnchecked(dirPath: string): Promise<boolean> {\n const dir = await fsOpendir(dirPath);\n try {\n const entry = await dir.read();\n return entry !== null;\n } finally {\n await dir.close().catch((closeErr: unknown) => {\n Logger.warn(\n `Failed to close dir handle for ${dirPath}: ${formatUnknownErrorMessage(closeErr)}`,\n );\n });\n }\n }\n}\n\n/**\n * Returns whether `path` exists, treating \"not found\" as absent and logging any\n * other stat failure. Used for TOCTOU re-checks between plan and execute phases.\n */\nexport async function destExists(\n fs: Pick<GuardedFileSystem, 'stat'>,\n path: string,\n label: string,\n): Promise<boolean> {\n try {\n await fs.stat(path);\n return true;\n } catch (err) {\n const missing =\n (isNodeError(err) && err.code === 'ENOENT') ||\n (isFsError(err) && err.code === ErrorCode.NOT_FOUND);\n if (!missing) {\n Logger.warn(`${label}: dest stat failed unexpectedly for \"${path}\": ${String(err)}`);\n }\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"fs.js","sourceRoot":"","sources":["../../src/core/fs.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from 'node:crypto';\nimport type { Stats } from 'node:fs';\nimport type { FileHandle } from 'node:fs/promises';\nimport {\n chmod as fsChmod,\n cp as fsCp,\n lstat as fsLstat,\n mkdir as fsMkdir,\n open as fsOpen,\n opendir as fsOpendir,\n readFile as fsReadFile,\n readlink as fsReadlink,\n rename as fsRename,\n rm as fsRm,\n rmdir as fsRmdir,\n stat as fsStat,\n unlink as fsUnlink,\n writeFile as fsWriteFile,\n} from 'node:fs/promises';\nimport { dirname, isAbsolute, resolve } from 'node:path';\n\nimport { withAbort } from './concurrency.ts';\nimport {\n ErrorCode,\n formatUnknownErrorMessage,\n FsError,\n isFsError,\n isNodeError,\n isNotFoundErrno,\n} from './errors.ts';\nimport { detectMimeFromContent } from './mime.ts';\nimport { Logger } from './observability.ts';\nimport type { EntryType as FileType } from './path-utils.ts';\nimport { respellCaseOnlyTarget } from './path-utils.ts';\nimport type { PathGuard } from './path.ts';\nimport type { ReadFileResult, ReadSpec } from './read.ts';\nimport { assertFileStats, createTooLargeError, readFileWithStats } from './read.ts';\nimport { getMaxTextFileSize } from './util.ts';\n\nexport type { FileType };\nexport type { Stats };\n\n// ─── Domain primitives ────────────────────────────────────────────────────────\n\n/**\n * validatePathForWrite + symlink resolution: a write aimed at a link must land\n * on its revalidated target, not create a new file beside the link. ENOENT is\n * the normal new-file case, so it falls through with the original path.\n */\nasync function resolveForWrite(pathGuard: PathGuard, filePath: string): Promise<string> {\n let validPath = await pathGuard.validatePathForWrite(filePath);\n\n try {\n const stats = await fsLstat(validPath);\n if (stats.isSymbolicLink()) {\n const target = await fsReadlink(validPath);\n const resolvedTarget = isAbsolute(target) ? target : resolve(dirname(validPath), target);\n validPath = await pathGuard.validatePathForWrite(resolvedTarget);\n }\n } catch (error) {\n if (!isNotFoundErrno(error)) {\n throw error;\n }\n }\n\n return validPath;\n}\n\nasync function atomicWriteFile(\n filePath: string,\n content: string,\n pathGuard: PathGuard,\n options: { signal?: AbortSignal | undefined } = {},\n): Promise<{ validPath: string }> {\n const { signal } = options;\n const validPath = await resolveForWrite(pathGuard, filePath);\n\n const tempSuffix = randomUUID().replace(/-/g, '').slice(0, 12);\n const tempPath = `${validPath}.${tempSuffix}.tmp`;\n\n // The rename below swaps in the temp file's inode, so the target would\n // inherit fsWriteFile's default 0o666 & ~umask — silently widening a 0600\n // file to 0644 on every write. Carry the existing mode across instead.\n let existingMode: number | undefined;\n try {\n existingMode = (await fsStat(validPath)).mode & 0o777;\n } catch (error) {\n // ENOENT is the normal new-file case: the default mode is correct there.\n // Anything else (EACCES, EIO) means the mode about to be overwritten could\n // not be read, and the write will silently widen the file — say so rather\n // than swallowing it.\n if (!isNotFoundErrno(error)) {\n Logger.warn(\n `atomicWriteFile: cannot read the existing mode of ${validPath}; the write will use the default mode: ${formatUnknownErrorMessage(error)}`,\n );\n }\n }\n\n try {\n signal?.throwIfAborted();\n // Create the temp file already narrowed to the target's mode: a 0600\n // file's new bytes must never sit at 0644 while the write is in flight.\n // `wx` fails on a name collision instead of overwriting; the chmod below\n // still restores bits the umask masked off at creation.\n await fsWriteFile(tempPath, content, {\n signal,\n flag: 'wx',\n ...(existingMode !== undefined ? { mode: existingMode } : {}),\n });\n if (existingMode !== undefined) {\n await fsChmod(tempPath, existingMode);\n }\n // Last cancellation point. The rename IS the commit: once it starts the\n // target may already be replaced, so it is never raced against the\n // signal — a withAbort race would report a finished write as failed, and\n // a client retry would apply it twice (same reasoning as appendFile).\n signal?.throwIfAborted();\n await fsRename(tempPath, validPath);\n } catch (error) {\n try {\n await fsUnlink(tempPath);\n } catch (cleanupError) {\n Logger.warn(\n `Failed to clean up temp file ${tempPath} after write error (${formatUnknownErrorMessage(error)}): ${formatUnknownErrorMessage(cleanupError)}`,\n );\n }\n throw error;\n }\n return { validPath };\n}\n\nconst CHAR_LF = 10;\n\nconst LINE_COUNT_CHUNK_BYTES = 64 * 1024;\n\n/**\n * Line count for a file on disk, counting newline bytes in fixed 64 KiB\n * chunks so an arbitrarily large file (one append's result metadata) costs\n * bounded memory. NOT FileHandle.readLines: it decodes each physical line\n * whole, so a multi-GB single-line log OOMs the server after the append has\n * already landed — a reported failure invites a retry that appends twice.\n * Matches read.ts's countLines semantics: a trailing newline does not open\n * a new line, an unterminated final line counts, an empty file is 0.\n */\nexport async function countFileLines(filePath: string): Promise<number> {\n const handle = await fsOpen(filePath, 'r');\n try {\n let newlines = 0;\n let lastByte = 0;\n let totalBytes = 0;\n const chunk = Buffer.alloc(LINE_COUNT_CHUNK_BYTES);\n for (;;) {\n const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);\n if (bytesRead === 0) break;\n totalBytes += bytesRead;\n for (let i = 0; i < bytesRead; i++) {\n const byte = chunk[i] ?? 0;\n if (byte === CHAR_LF) newlines++;\n lastByte = byte;\n }\n }\n // totalBytes is the empty-file sentinel: a NUL (0x00) final byte is real\n // content and must count as an unterminated final line, not \"empty\".\n return totalBytes === 0 ? 0 : lastByte === CHAR_LF ? newlines : newlines + 1;\n } finally {\n await handle.close();\n }\n}\n\nexport class GuardedFileSystem {\n readonly pathGuard: PathGuard;\n\n constructor(pathGuard: PathGuard) {\n this.pathGuard = pathGuard;\n }\n\n async stat(\n filePath: string,\n options?: { signal?: AbortSignal | undefined },\n ): Promise<{ stats: Stats; validPath: string }> {\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n const stats = await withAbort(fsStat(validPath), options?.signal);\n return { stats, validPath };\n }\n\n async lstat(\n filePath: string,\n options?: { signal?: AbortSignal },\n ): Promise<{ stats: Stats; validPath: string }> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n const stats = await withAbort(fsLstat(validPath), options?.signal);\n return { stats, validPath };\n }\n\n async readlink(\n filePath: string,\n options?: Parameters<typeof fsReadlink>[1],\n ): Promise<{ linkString: string; validPath: string }> {\n // validateExistingPath resolves through the symlink, so readlink would always\n // be handed the final target — a regular file — and fail EINVAL. This guard\n // keeps the link itself while still enforcing containment and the denylist.\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n const raw = await fsReadlink(validPath, options);\n const linkString = Buffer.isBuffer(raw) ? raw.toString('utf-8') : raw;\n return { linkString, validPath };\n }\n\n async mkdir(filePath: string, options?: Parameters<typeof fsMkdir>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForWrite(filePath);\n await fsMkdir(validPath, options);\n }\n\n async rename(oldPath: string, newPath: string): Promise<void> {\n // Not validateExistingPath: that resolves through a symlink, so renaming a\n // link would rename its target and leave the link dangling.\n const validOld = await this.pathGuard.validatePathForDelete(oldPath);\n let validNew = await this.pathGuard.validatePathForWrite(newPath);\n // Renaming a path onto itself is only meaningful as a case-only rename.\n if (validOld === validNew) validNew = respellCaseOnlyTarget(newPath, validNew);\n await fsRename(validOld, validNew);\n }\n\n async writeFile(\n filePath: string,\n content: string,\n options: { signal?: AbortSignal | undefined } = {},\n ): Promise<{ validPath: string }> {\n return atomicWriteFile(filePath, content, this.pathGuard, options);\n }\n\n /**\n * Append to the existing file (created if missing). Deliberately NOT the\n * atomicWriteFile temp+rename dance: the rename would swap in a fresh inode,\n * and the whole point of appending is to extend the file that's already\n * there — inode and mode survive by construction.\n */\n async appendFile(\n filePath: string,\n content: string,\n options: { signal?: AbortSignal | undefined } = {},\n ): Promise<{ validPath: string }> {\n const { signal } = options;\n const validPath = await resolveForWrite(this.pathGuard, filePath);\n // Non-regular targets must be rejected before the open: 'a' on a FIFO\n // (POSIX) blocks until a reader appears, and fsOpen takes no signal, so\n // the call would hang unabortable and pin its concurrency slot. The\n // overwrite path can't hang — it writes a temp file and renames over.\n // resolveForWrite has already resolved any symlink to its target, so the\n // lstat here doubles as the new-vs-existing probe for the cleanup below.\n let existed = true;\n try {\n assertFileStats(validPath, await fsLstat(validPath));\n } catch (error) {\n if (!isNotFoundErrno(error)) throw error;\n existed = false;\n }\n // fs.promises.appendFile takes no signal, so a withAbort race would\n // report failure while the append still lands — a client retry then\n // appends twice. A handle opened with 'a' gets a genuinely abortable\n // FileHandle.writeFile instead, same as the writeFile path. 'a' on a\n // missing target CREATES it, so: check the signal before the open, and\n // if the call then fails after creating the file, remove what it made —\n // an aborted or errored call must not leave a phantom file behind.\n // The pre-open lstat only proves the file was missing a moment ago:\n // 'ax' makes creation exclusive, so a racer that created it in between\n // fails EEXIST, reopens plainly, and never claims cleanup rights over\n // the winner's file.\n signal?.throwIfAborted();\n let created = false;\n let handle: FileHandle;\n if (existed) {\n handle = await fsOpen(validPath, 'a');\n } else {\n try {\n handle = await fsOpen(validPath, 'ax');\n created = true;\n } catch (error) {\n if (!isNodeError(error) || error.code !== 'EEXIST') throw error;\n handle = await fsOpen(validPath, 'a');\n }\n }\n try {\n signal?.throwIfAborted();\n // Ceiling: an append is not transactional, so an abort landing\n // mid-write can leave a prefix of the content on disk. Unwiring the\n // signal would trade that for a silently-completed write; a client\n // that retries is double-appending either way, so the abortable\n // write stays.\n await handle.writeFile(content, { signal });\n } catch (error) {\n if (created) {\n await fsUnlink(validPath).catch((unlinkError: unknown) => {\n Logger.warn(\n `Failed to remove newly created ${validPath} after a failed append: ${formatUnknownErrorMessage(unlinkError)}`,\n );\n });\n }\n throw error;\n } finally {\n await handle.close();\n }\n return { validPath };\n }\n\n /**\n * Leading bytes of a file, for MIME sniffing without loading the whole\n * file — an append's result must describe the resulting file (which may be\n * a multi-GB log), not the chunk that was appended.\n */\n async readLeadingSample(filePath: string, size: number): Promise<Buffer> {\n const { validPath } = await this.stat(filePath);\n const handle = await fsOpen(validPath, 'r');\n try {\n const buffer = Buffer.alloc(size);\n const { bytesRead } = await handle.read(buffer, 0, size, 0);\n return buffer.subarray(0, bytesRead);\n } finally {\n await handle.close();\n }\n }\n\n async rm(filePath: string, options?: Parameters<typeof fsRm>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n await fsRm(validPath, options);\n }\n\n async rmdir(filePath: string, options?: Parameters<typeof fsRmdir>[1]): Promise<void> {\n const validPath = await this.pathGuard.validatePathForDelete(filePath);\n await fsRmdir(validPath, options);\n }\n\n async cp(\n source: string,\n destination: string,\n options?: Parameters<typeof fsCp>[2],\n ): Promise<void> {\n // As in rename: keep the link itself so callers passing verbatimSymlinks\n // actually copy the link rather than a dereferenced target.\n const validSource = await this.pathGuard.validatePathForDelete(source);\n const validDest = await this.pathGuard.validatePathForWrite(destination);\n await fsCp(validSource, validDest, options);\n }\n\n async readFile(filePath: string, spec: ReadSpec): Promise<ReadFileResult> {\n spec.signal?.throwIfAborted();\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n const stats = await withAbort(fsStat(validPath), spec.signal);\n return readFileWithStats(filePath, validPath, stats, spec);\n }\n\n async readEditableText(\n filePath: string,\n options?: { signal?: AbortSignal; tool?: string },\n ): Promise<{ validPath: string; content: string; stats: Stats }> {\n const { stats, validPath } = await this.stat(filePath, options);\n const maxSize = getMaxTextFileSize();\n if (stats.size > maxSize) {\n // `tool` labels the TOO_LARGE message so the client sees which tool refused.\n throw new FsError(\n ErrorCode.TOO_LARGE,\n `File too large for ${options?.tool ?? 'edit'} (${stats.size} bytes > ${maxSize} bytes)`,\n filePath,\n { size: stats.size, maxFileSize: maxSize },\n );\n }\n const { content } = await readFileWithStats(filePath, validPath, stats, {\n kind: 'full',\n strictUtf8: true,\n ...(options?.signal ? { signal: options.signal } : {}),\n });\n return { validPath, content, stats };\n }\n\n async readRaw(\n filePath: string,\n options?: { signal?: AbortSignal },\n ): Promise<{ content: Buffer; mimeType: string; isBinary: boolean }> {\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n const stats = await withAbort(fsStat(validPath), options?.signal);\n assertFileStats(filePath, stats);\n // Enforce size limit before reading to avoid loading large files into memory.\n // Binary detection is best-effort, but this is a hard limit.\n const maxTextFileSize = getMaxTextFileSize();\n if (stats.size > maxTextFileSize) {\n throw createTooLargeError(stats.size, maxTextFileSize, filePath);\n }\n const content = await withAbort(fsReadFile(validPath), options?.signal);\n const mimeInfo = detectMimeFromContent(validPath, content);\n return {\n content,\n mimeType: mimeInfo.mimeType,\n isBinary: mimeInfo.kind !== 'text',\n };\n }\n\n async open(filePath: string): Promise<FileHandle> {\n const validPath = await this.pathGuard.validateExistingPath(filePath);\n return fsOpen(validPath, 'r');\n }\n\n // Single resolution + stat: validateExistingPathDetailed resolves the real\n // path (following symlinks, re-checking sensitivity) once, then we stat the\n // resolved target. Replaces the tool-side validateExistingPathDetailed +\n // fs.stat pair that re-validated through the free stat() guard.\n async statDetailed(\n filePath: string,\n options?: { signal?: AbortSignal },\n ): Promise<{ requestedPath: string; isSymlink: boolean; stats: Stats }> {\n const { requestedPath, resolvedPath, isSymlink } =\n await this.pathGuard.validateExistingPathDetailed(filePath);\n const stats = await withAbort(fsStat(resolvedPath), options?.signal);\n return { requestedPath, isSymlink, stats };\n }\n\n async hasChildrenUnchecked(dirPath: string): Promise<boolean> {\n const dir = await fsOpendir(dirPath);\n try {\n const entry = await dir.read();\n return entry !== null;\n } finally {\n await dir.close().catch((closeErr: unknown) => {\n Logger.warn(\n `Failed to close dir handle for ${dirPath}: ${formatUnknownErrorMessage(closeErr)}`,\n );\n });\n }\n }\n}\n\n/**\n * Returns whether `path` exists, treating \"not found\" as absent and logging any\n * other stat failure. Used for TOCTOU re-checks between plan and execute phases.\n */\nexport async function destExists(\n fs: Pick<GuardedFileSystem, 'stat'>,\n path: string,\n label: string,\n): Promise<boolean> {\n try {\n await fs.stat(path);\n return true;\n } catch (err) {\n const missing = isNotFoundErrno(err) || (isFsError(err) && err.code === ErrorCode.NOT_FOUND);\n if (!missing) {\n Logger.warn(`${label}: dest stat failed unexpectedly for \"${path}\": ${String(err)}`);\n }\n return false;\n }\n}\n"]}
@@ -12,7 +12,7 @@ export interface GlobEntriesOptions {
12
12
  maxDepth?: number;
13
13
  onlyFiles?: boolean;
14
14
  suppressErrors?: boolean;
15
- /** Skip what a walk should not surface: DEFAULT_EXCLUDE_PATTERNS and .gitignore. */
15
+ /** Skip what a walk should not surface: DEFAULT_EXCLUDED_NAMES and .gitignore. */
16
16
  skipIgnored?: boolean;
17
17
  /** Bounds the `.gitignore` discovery `skipIgnored` runs before the walk. */
18
18
  signal?: AbortSignal;
@@ -1 +1 @@
1
- {"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../../src/core/glob.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGlD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAiBzD;AAwID,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oFAAoF;IACpF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AA4MD,wBAAuB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAAC,SAAS,CAAC,CAazF"}
1
+ {"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../../src/core/glob.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAIlD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAiBzD;AAoJD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kFAAkF;IAClF,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAoND,wBAAuB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,cAAc,CAAC,SAAS,CAAC,CAkBzF"}
package/dist/core/glob.js CHANGED
@@ -1,10 +1,11 @@
1
- import { glob as fsGlob, readFile as fsReadFile } from 'node:fs/promises';
2
- import { basename, dirname, isAbsolute, join, posix, relative, resolve } from 'node:path';
1
+ import { glob as fsGlob, readFile as fsReadFile, lstat } from 'node:fs/promises';
2
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
3
3
  import ignore from 'ignore';
4
4
  import { processInParallel } from "./concurrency.js";
5
5
  import { formatUnknownErrorMessage } from "./errors.js";
6
6
  import { Logger } from "./observability.js";
7
7
  import { isWindowsDriveRelativePath, toPosixPath } from "./path-utils.js";
8
+ import { MIB } from "./util.js";
8
9
  export function isSafeGlobSyntax(pattern) {
9
10
  if (!pattern || pattern.trim().length === 0) {
10
11
  return false;
@@ -23,10 +24,18 @@ export function isSafeGlobSyntax(pattern) {
23
24
  }
24
25
  return true;
25
26
  }
27
+ /** A .gitignore past this size is not a .gitignore; skip it rather than read it. */
28
+ const MAX_GITIGNORE_BYTES = MIB;
26
29
  async function loadGitignoreFiles(root, gitignorePaths, manager, signal) {
27
30
  await processInParallel(gitignorePaths, async (relPath) => {
28
31
  const absPath = join(root, relPath);
29
32
  try {
33
+ // lstat, not stat: a symlinked .gitignore may point at a device, a
34
+ // FIFO or a huge file outside the root. Only a plain, small file is a
35
+ // rule set worth reading.
36
+ const info = await lstat(absPath);
37
+ if (!info.isFile() || info.size > MAX_GITIGNORE_BYTES)
38
+ return;
30
39
  const contents = await fsReadFile(absPath, { encoding: 'utf-8', signal });
31
40
  const matcher = ignore();
32
41
  matcher.add(contents);
@@ -46,18 +55,19 @@ class GitignoreManager {
46
55
  this.matchers.set(dir, matcher);
47
56
  }
48
57
  /** `null` when no `.gitignore` was found, so callers skip the filter entirely. */
49
- static async load(root, signal) {
58
+ static async load(root, signal, maxDepth) {
50
59
  const manager = new GitignoreManager();
51
60
  try {
52
61
  const gitignorePaths = [];
53
62
  const gitignoreEntries = fsGlob('**/.gitignore', {
54
63
  cwd: root,
64
+ // Skip what the walk itself skips, and stop one level past its depth
65
+ // bound: a .gitignore at depth d only affects entries at depth >= d.
55
66
  exclude: (entry) => {
56
67
  const name = basename(entry);
57
- if (name === 'node_modules' || name === '.git' || name === '.hg' || name === '.svn') {
68
+ if (name === '.hg' || name === '.svn' || DEFAULT_EXCLUDED_NAMES.has(name))
58
69
  return true;
59
- }
60
- return false;
70
+ return maxDepth !== undefined && toPosixPath(entry).split('/').length - 1 > maxDepth;
61
71
  },
62
72
  });
63
73
  for await (const match of gitignoreEntries) {
@@ -215,7 +225,6 @@ function normalizeGlobOptions(options) {
215
225
  const normalized = {
216
226
  cwd,
217
227
  patterns,
218
- exclude: options.skipIgnored ? DEFAULT_EXCLUDE_PATTERNS.map(toPosixPath) : [],
219
228
  suppressErrors: options.suppressErrors ?? false,
220
229
  };
221
230
  if (options.maxDepth !== undefined) {
@@ -224,7 +233,7 @@ function normalizeGlobOptions(options) {
224
233
  return normalized;
225
234
  }
226
235
  function* processDirentMatch(match, cwd, maxDepth, seen, onlyFiles) {
227
- const absolutePath = resolve(match.parentPath, match.name);
236
+ const absolutePath = resolve(cwd, match.parentPath, match.name);
228
237
  if (maxDepth !== undefined) {
229
238
  const rel = relative(cwd, absolutePath);
230
239
  if (rel.split(/[/\\]/u).length - 1 > maxDepth)
@@ -237,37 +246,42 @@ function* processDirentMatch(match, cwd, maxDepth, seen, onlyFiles) {
237
246
  return;
238
247
  yield { path: absolutePath, dirent: match };
239
248
  }
240
- function createExcludeFilter(cwd, excludePatterns, gitignoreMatcher) {
241
- if (!gitignoreMatcher) {
242
- return excludePatterns;
243
- }
244
- return (match) => {
245
- const relPath = match.parentPath
246
- ? relative(cwd, join(match.parentPath, match.name))
247
- : match.name;
248
- const posixRel = toPosixPath(relPath);
249
- // Gitignore check
250
- const isDir = match.isDirectory();
251
- if (gitignoreMatcher.isIgnored(posixRel, isDir)) {
252
- return true;
253
- }
254
- // Also check explicit exclude patterns
255
- if (excludePatterns.length > 0) {
256
- for (const ex of excludePatterns) {
257
- if (posix.matchesGlob(posixRel, ex))
258
- return true;
259
- }
260
- }
261
- return false;
249
+ function createWalkFilter(cwd, skipIgnored, gitignoreMatcher, maxDepth) {
250
+ if (!skipIgnored && maxDepth === undefined)
251
+ return undefined;
252
+ // fs.glob can hand the predicate a dirent whose parentPath is "." — the walk
253
+ // root, not the process cwd — when it re-visits a directory under `**` whose
254
+ // name also exists in the process cwd. Resolving against `cwd` names the
255
+ // entry the dirent describes; `join` would resolve it against the process.
256
+ const relativeOf = (match) => toPosixPath(relative(cwd, resolve(cwd, match.parentPath, match.name)));
257
+ // Every default exclude is a bare name that applies at any depth, so a Set
258
+ // lookup per segment replaces compiling dozens of globs per entry.
259
+ const excluded = (posixRel, isDir) => skipIgnored &&
260
+ (posixRel.split('/').some((segment) => DEFAULT_EXCLUDED_NAMES.has(segment)) ||
261
+ (gitignoreMatcher?.isIgnored(posixRel, isDir) ?? false));
262
+ return {
263
+ prune: (match) => {
264
+ const posixRel = relativeOf(match);
265
+ const isDir = match.isDirectory();
266
+ if (excluded(posixRel, isDir))
267
+ return true;
268
+ // Stop descent one level past the depth bound. Pruning a directory *at*
269
+ // the bound would drop it where fs.glob treats it as top-level, so prune
270
+ // its children instead; processDirentMatch drops that extra level.
271
+ if (maxDepth === undefined || !isDir)
272
+ return false;
273
+ return posixRel.split('/').length - 1 > maxDepth;
274
+ },
275
+ isExcluded: (match) => excluded(relativeOf(match), match.isDirectory()),
262
276
  };
263
277
  }
264
- async function* processGlobPattern(pattern, plan, seen, onlyFiles, excludeFunc) {
278
+ async function* processGlobPattern(pattern, plan, seen, onlyFiles, filter) {
265
279
  const { cwd, maxDepth, suppressErrors } = plan;
266
280
  let iterable;
267
281
  try {
268
282
  iterable = fsGlob(pattern, {
269
283
  cwd,
270
- exclude: excludeFunc,
284
+ ...(filter ? { exclude: filter.prune } : {}),
271
285
  withFileTypes: true,
272
286
  });
273
287
  }
@@ -280,8 +294,8 @@ async function* processGlobPattern(pattern, plan, seen, onlyFiles, excludeFunc)
280
294
  for await (const match of iterable) {
281
295
  // A function `exclude` only prunes descent in fs.glob — it still yields
282
296
  // the rejected dirent itself, and any rejected entry below the top level.
283
- // An array `exclude` drops both. Re-apply the predicate so the two agree.
284
- if (typeof excludeFunc === 'function' && excludeFunc(match))
297
+ // Re-apply the ignore rules (not the depth bound) to drop those.
298
+ if (filter?.isExcluded(match))
285
299
  continue;
286
300
  yield* processDirentMatch(match, cwd, maxDepth, seen, onlyFiles);
287
301
  }
@@ -294,57 +308,40 @@ async function* processGlobPattern(pattern, plan, seen, onlyFiles, excludeFunc)
294
308
  }
295
309
  export async function* globEntries(options) {
296
310
  const gitignoreMatcher = options.skipIgnored
297
- ? await GitignoreManager.load(options.cwd, options.signal)
311
+ ? await GitignoreManager.load(options.cwd, options.signal, options.maxDepth)
298
312
  : null;
299
313
  const plan = normalizeGlobOptions(options);
300
314
  const seen = new Set();
301
315
  const onlyFiles = options.onlyFiles ?? true;
302
- const excludeFunc = createExcludeFilter(plan.cwd, plan.exclude, gitignoreMatcher);
316
+ const filter = createWalkFilter(plan.cwd, options.skipIgnored ?? false, gitignoreMatcher, plan.maxDepth);
303
317
  for (const pattern of plan.patterns) {
304
- yield* processGlobPattern(pattern, plan, seen, onlyFiles, excludeFunc);
318
+ yield* processGlobPattern(pattern, plan, seen, onlyFiles, filter);
305
319
  }
306
320
  }
307
- const DEFAULT_EXCLUDE_PATTERNS = [
308
- '**/node_modules',
309
- '**/node_modules/**',
310
- '**/dist',
311
- '**/dist/**',
312
- '**/build',
313
- '**/build/**',
314
- '**/coverage',
315
- '**/coverage/**',
316
- '**/.git',
317
- '**/.git/**',
318
- '**/.vscode',
319
- '**/.vscode/**',
320
- '**/.idea',
321
- '**/.idea/**',
322
- '**/.DS_Store',
323
- '**/.next',
324
- '**/.next/**',
325
- '**/.nuxt',
326
- '**/.nuxt/**',
327
- '**/.output',
328
- '**/.output/**',
329
- '**/.svelte-kit',
330
- '**/.svelte-kit/**',
331
- '**/.cache',
332
- '**/.cache/**',
333
- '**/.yarn',
334
- '**/.yarn/**',
335
- '**/jspm_packages',
336
- '**/jspm_packages/**',
337
- '**/bower_components',
338
- '**/bower_components/**',
339
- '**/out',
340
- '**/out/**',
341
- '**/tmp',
342
- '**/tmp/**',
343
- '**/.temp',
344
- '**/.temp/**',
345
- '**/npm-debug.log',
346
- '**/yarn-debug.log',
347
- '**/yarn-error.log',
348
- '**/Thumbs.db',
349
- ];
321
+ /** Names a `skipIgnored` walk never enters or yields, at any depth. */
322
+ const DEFAULT_EXCLUDED_NAMES = new Set([
323
+ 'node_modules',
324
+ 'dist',
325
+ 'build',
326
+ 'coverage',
327
+ '.git',
328
+ '.vscode',
329
+ '.idea',
330
+ '.DS_Store',
331
+ '.next',
332
+ '.nuxt',
333
+ '.output',
334
+ '.svelte-kit',
335
+ '.cache',
336
+ '.yarn',
337
+ 'jspm_packages',
338
+ 'bower_components',
339
+ 'out',
340
+ 'tmp',
341
+ '.temp',
342
+ 'npm-debug.log',
343
+ 'yarn-debug.log',
344
+ 'yarn-error.log',
345
+ 'Thumbs.db',
346
+ ]);
350
347
  //# sourceMappingURL=glob.js.map