@openclaw/feishu 2026.9.8 → 2026.10.1-beta.2

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 (225) hide show
  1. package/dist/.setup/{channel-DV4hM2Uf.mjs → channel-CX62T71S.mjs} +146 -245
  2. package/dist/.setup/{channel.runtime-CXQcH5zZ.mjs → channel.runtime-COiaq-Rv.mjs} +6 -12
  3. package/dist/.setup/{chat-D8MlFAZi.mjs → chat-BdyvImsY.mjs} +6 -9
  4. package/dist/.setup/{client-BYG-_IZl.mjs → client-BsBVWJzN.mjs} +1 -1
  5. package/dist/.setup/{doctor-contract-3XC0vbIw.mjs → doctor-contract-B1B9ogdd.mjs} +212 -27
  6. package/dist/.setup/{monitor-D_rsHBZ6.mjs → monitor-7DvwUs4Y.mjs} +5 -6
  7. package/dist/.setup/{monitor.account-DvvY7i6k.mjs → monitor.account-7xVu7XWY.mjs} +190 -393
  8. package/dist/.setup/{probe-_ViqSi0j.mjs → probe-B9o7iey6.mjs} +13 -39
  9. package/dist/.setup/{reply-delivery-result-COAjaukc.mjs → reply-delivery-result-Bt-BWBEA.mjs} +151 -297
  10. package/dist/.setup/{setup-api-C4S5I3ac.mjs → setup-api-DmzX9S01.mjs} +1 -1
  11. package/dist/.setup/{subagent-hooks-BaFoMu8W.mjs → subagent-hooks-CUXg0pC-.mjs} +5 -11
  12. package/dist/.setup/{thread-bindings-EdLXPXzw.mjs → thread-bindings-jW2x5VuN.mjs} +23 -41
  13. package/dist/api.js +234 -297
  14. package/dist/channel-plugin-api.js +1 -1
  15. package/dist/doctor-contract-api.js +2 -2
  16. package/dist/session-binding-contract-api.js +1 -1
  17. package/dist/setup-api.js +1 -1
  18. package/dist/setup-entry.js +1 -1
  19. package/dist/subagent-hooks-api.js +1 -1
  20. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +95 -0
  21. package/node_modules/@openclaw/fs-safe/README.md +70 -379
  22. package/node_modules/@openclaw/fs-safe/dist/absolute-path.js +35 -51
  23. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -0
  24. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  25. package/node_modules/@openclaw/fs-safe/dist/archive-input.js +2 -0
  26. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  27. package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
  28. package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +9 -10
  29. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -5
  30. package/node_modules/@openclaw/fs-safe/dist/atomic-io.d.ts +51 -0
  31. package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +242 -0
  32. package/node_modules/@openclaw/fs-safe/dist/config.d.ts +1 -1
  33. package/node_modules/@openclaw/fs-safe/dist/config.js +1 -1
  34. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
  35. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +2 -2
  36. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +2 -0
  37. package/node_modules/@openclaw/fs-safe/dist/create.js +12 -5
  38. package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +11 -0
  39. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +4 -7
  40. package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.d.ts +55 -0
  41. package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.js +1 -0
  42. package/node_modules/@openclaw/fs-safe/dist/entry-publication.d.ts +8 -0
  43. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +262 -0
  44. package/node_modules/@openclaw/fs-safe/dist/exclusive-create.d.ts +3 -0
  45. package/node_modules/@openclaw/fs-safe/dist/exclusive-create.js +22 -0
  46. package/node_modules/@openclaw/fs-safe/dist/file-cleanup.d.ts +1 -6
  47. package/node_modules/@openclaw/fs-safe/dist/file-cleanup.js +3 -6
  48. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +3 -6
  49. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +2 -2
  50. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-mutation.js +9 -0
  51. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.js +4 -6
  52. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-stale-admission.js +1 -1
  53. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -0
  54. package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +2 -0
  55. package/node_modules/@openclaw/fs-safe/dist/file-store-sync-write.js +20 -18
  56. package/node_modules/@openclaw/fs-safe/dist/file-store.js +4 -10
  57. package/node_modules/@openclaw/fs-safe/dist/fs.d.ts +2 -10
  58. package/node_modules/@openclaw/fs-safe/dist/fs.js +2 -10
  59. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +3 -4
  60. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +6 -18
  61. package/node_modules/@openclaw/fs-safe/dist/guest-dispatch-python.js +1 -1
  62. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +0 -2
  63. package/node_modules/@openclaw/fs-safe/dist/guest.js +3 -9
  64. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
  65. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
  66. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-directory.js +1 -5
  67. package/node_modules/@openclaw/fs-safe/dist/json.js +19 -37
  68. package/node_modules/@openclaw/fs-safe/dist/move-path.js +2 -0
  69. package/node_modules/@openclaw/fs-safe/dist/mutation-authority.d.ts +1 -0
  70. package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +9 -1
  71. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +47 -2
  72. package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +1 -9
  73. package/node_modules/@openclaw/fs-safe/dist/native-config.js +6 -40
  74. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +2 -0
  75. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +5 -2
  76. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +11 -316
  77. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.d.ts +7 -7
  78. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.js +28 -175
  79. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +15 -0
  80. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +418 -0
  81. package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +9 -4
  82. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +17 -13
  83. package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +6 -24
  84. package/node_modules/@openclaw/fs-safe/dist/native.js +5 -1
  85. package/node_modules/@openclaw/fs-safe/dist/path-case.js +10 -9
  86. package/node_modules/@openclaw/fs-safe/dist/path-scope-lexical.js +4 -1
  87. package/node_modules/@openclaw/fs-safe/dist/path-segment-route.d.ts +1 -0
  88. package/node_modules/@openclaw/fs-safe/dist/path-segment-route.js +3 -0
  89. package/node_modules/@openclaw/fs-safe/dist/path.js +4 -1
  90. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +0 -1
  91. package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +1 -3
  92. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +2 -0
  93. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +9 -10
  94. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +3 -6
  95. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +2 -2
  96. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +4 -3
  97. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +3 -4
  98. package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +18 -32
  99. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +5 -17
  100. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +92 -230
  101. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.d.ts +4 -15
  102. package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.js +22 -63
  103. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +9 -34
  104. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +78 -83
  105. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +18 -20
  106. package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +64 -98
  107. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +16 -33
  108. package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +61 -200
  109. package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +1 -6
  110. package/node_modules/@openclaw/fs-safe/dist/replace-file.js +76 -216
  111. package/node_modules/@openclaw/fs-safe/dist/root-boundary.js +14 -24
  112. package/node_modules/@openclaw/fs-safe/dist/root-context.d.ts +1 -4
  113. package/node_modules/@openclaw/fs-safe/dist/root-context.js +4 -19
  114. package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +52 -0
  115. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +350 -0
  116. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +35 -0
  117. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.js +1 -0
  118. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -27
  119. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +31 -28
  120. package/node_modules/@openclaw/fs-safe/dist/root-directory.js +1 -4
  121. package/node_modules/@openclaw/fs-safe/dist/root-errors.js +34 -0
  122. package/node_modules/@openclaw/fs-safe/dist/root-file.js +9 -21
  123. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +105 -37
  124. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +7 -2
  125. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +67 -38
  126. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +1 -1
  127. package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -2
  128. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -3
  129. package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +3 -1
  130. package/node_modules/@openclaw/fs-safe/dist/root-path-stat.js +68 -85
  131. package/node_modules/@openclaw/fs-safe/dist/root-path.js +6 -1
  132. package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.d.ts +4 -0
  133. package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.js +1 -1
  134. package/node_modules/@openclaw/fs-safe/dist/root-paths.js +4 -9
  135. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.d.ts +4 -0
  136. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +311 -0
  137. package/node_modules/@openclaw/fs-safe/dist/root-remove.d.ts +11 -0
  138. package/node_modules/@openclaw/fs-safe/dist/root-remove.js +3 -1
  139. package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +2 -2
  140. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +1 -3
  141. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.d.ts +3 -3
  142. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -10
  143. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +0 -1
  144. package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +0 -4
  145. package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -9
  146. package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.d.ts +0 -2
  147. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +2 -0
  148. package/node_modules/@openclaw/fs-safe/dist/sibling-temp.js +7 -4
  149. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +13 -6
  150. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +0 -3
  151. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +5 -23
  152. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.d.ts +2 -1
  153. package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +11 -6
  154. package/node_modules/@openclaw/fs-safe/dist/temp-cleanup.js +1 -8
  155. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
  156. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +2 -9
  157. package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +1 -6
  158. package/node_modules/@openclaw/fs-safe/dist/trash.js +6 -2
  159. package/node_modules/@openclaw/fs-safe/dist/walk.js +100 -46
  160. package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +7 -0
  161. package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +235 -19
  162. package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +21 -2
  163. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +21 -3
  164. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +6 -0
  165. package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +111 -0
  166. package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +13 -0
  167. package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +27 -4
  168. package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +2 -0
  169. package/node_modules/@openclaw/fs-safe/dist/watch.js +131 -40
  170. package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.d.ts +8 -0
  171. package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.js +24 -6
  172. package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.d.ts +13 -0
  173. package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.js +51 -0
  174. package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -0
  175. package/node_modules/@openclaw/fs-safe/docs/atomic.md +4 -1
  176. package/node_modules/@openclaw/fs-safe/docs/config.md +6 -14
  177. package/node_modules/@openclaw/fs-safe/docs/contributing.md +4 -14
  178. package/node_modules/@openclaw/fs-safe/docs/durability.md +6 -29
  179. package/node_modules/@openclaw/fs-safe/docs/entry-publication.md +146 -0
  180. package/node_modules/@openclaw/fs-safe/docs/errors.md +16 -0
  181. package/node_modules/@openclaw/fs-safe/docs/file-store.md +28 -5
  182. package/node_modules/@openclaw/fs-safe/docs/index.md +2 -31
  183. package/node_modules/@openclaw/fs-safe/docs/install.md +10 -31
  184. package/node_modules/@openclaw/fs-safe/docs/json.md +1 -1
  185. package/node_modules/@openclaw/fs-safe/docs/local-roots.md +0 -1
  186. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +12 -12
  187. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +35 -96
  188. package/node_modules/@openclaw/fs-safe/docs/native.md +22 -20
  189. package/node_modules/@openclaw/fs-safe/docs/path.md +1 -1
  190. package/node_modules/@openclaw/fs-safe/docs/permissions.md +1 -2
  191. package/node_modules/@openclaw/fs-safe/docs/public-api.md +1 -2
  192. package/node_modules/@openclaw/fs-safe/docs/reading.md +1 -2
  193. package/node_modules/@openclaw/fs-safe/docs/root.md +26 -184
  194. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +1 -1
  195. package/node_modules/@openclaw/fs-safe/docs/security-model.md +115 -3
  196. package/node_modules/@openclaw/fs-safe/docs/store.md +2 -2
  197. package/node_modules/@openclaw/fs-safe/docs/temp.md +29 -65
  198. package/node_modules/@openclaw/fs-safe/docs/testing.md +90 -58
  199. package/node_modules/@openclaw/fs-safe/docs/types.md +3 -16
  200. package/node_modules/@openclaw/fs-safe/docs/walk.md +33 -5
  201. package/node_modules/@openclaw/fs-safe/docs/watch.md +96 -14
  202. package/node_modules/@openclaw/fs-safe/docs/writing.md +172 -16
  203. package/node_modules/@openclaw/fs-safe/package.json +10 -10
  204. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  205. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  206. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  207. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  208. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  209. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  210. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  211. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  212. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  213. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  214. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  215. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  216. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  217. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  218. package/package.json +5 -5
  219. package/skills/feishu-wiki/SKILL.md +1 -1
  220. package/dist/.setup/accounts-wRqItHug.mjs +0 -206
  221. package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +0 -6
  222. package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +0 -88
  223. package/node_modules/@openclaw/fs-safe/docs/mutation-policy-proof.md +0 -69
  224. package/node_modules/@openclaw/fs-safe/docs/private-file-store.md +0 -68
  225. package/node_modules/@openclaw/fs-safe/docs/test-hooks.md +0 -110
@@ -39,7 +39,7 @@ function buildEntry(params) {
39
39
  path: fullPath,
40
40
  relativePath: params.relativePath,
41
41
  depth: params.depth,
42
- kind: params.kind ?? kindForDirent(params.dirent),
42
+ kind: params.kind,
43
43
  dirent: params.dirent,
44
44
  };
45
45
  }
@@ -101,35 +101,62 @@ export function walkDirectorySync(rootDir, options = {}) {
101
101
  return;
102
102
  visitedDirs.add(realDir);
103
103
  let entries;
104
+ let handle;
104
105
  try {
105
- entries = fsSync.readdirSync(operationPath, { withFileTypes: true });
106
+ if (options.maxEntries === undefined) {
107
+ entries = fsSync.readdirSync(operationPath, { withFileTypes: true });
108
+ }
109
+ else {
110
+ handle = fsSync.opendirSync(operationPath);
111
+ }
106
112
  }
107
113
  catch (error) {
108
114
  recordFailedDir(result, root, dir, depth, error);
109
115
  return;
110
116
  }
111
117
  const childPrefix = dir.endsWith(path.sep) ? dir : `${dir}${path.sep}`;
112
- for (const dirent of entries) {
113
- if (shouldStop(result, options)) {
114
- result.truncated = true;
115
- return;
118
+ let index = 0;
119
+ try {
120
+ while (true) {
121
+ let dirent;
122
+ try {
123
+ dirent = handle ? handle.readSync() : entries[index++];
124
+ }
125
+ catch (error) {
126
+ recordFailedDir(result, root, dir, depth, error);
127
+ return;
128
+ }
129
+ if (!dirent)
130
+ return;
131
+ if (shouldStop(result, options)) {
132
+ result.truncated = true;
133
+ return;
134
+ }
135
+ result.scannedEntryCount += 1;
136
+ const fullPath = childPrefix + dirent.name;
137
+ const kind = resolveKind(fullPath, dirent, symlinks);
138
+ if (!kind)
139
+ continue;
140
+ const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
141
+ const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
142
+ if (options.include?.(entry) ?? true) {
143
+ result.entries.push(entry);
144
+ }
145
+ if (kind === "directory" &&
146
+ (options.maxDepth === undefined || depth < options.maxDepth) &&
147
+ (options.descend?.(entry) ?? true)) {
148
+ visit(fullPath, relativePath, depth + 1);
149
+ if (result.truncated)
150
+ return;
151
+ }
116
152
  }
117
- result.scannedEntryCount += 1;
118
- const fullPath = childPrefix + dirent.name;
119
- const kind = resolveKind(fullPath, dirent, symlinks);
120
- if (!kind)
121
- continue;
122
- const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
123
- const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
124
- if (options.include?.(entry) ?? true) {
125
- result.entries.push(entry);
153
+ }
154
+ finally {
155
+ try {
156
+ handle?.closeSync();
126
157
  }
127
- if (kind === "directory" &&
128
- (options.maxDepth === undefined || depth < options.maxDepth) &&
129
- (options.descend?.(entry) ?? true)) {
130
- visit(fullPath, relativePath, depth + 1);
131
- if (result.truncated)
132
- return;
158
+ catch (error) {
159
+ recordFailedDir(result, root, dir, depth, error);
133
160
  }
134
161
  }
135
162
  }
@@ -165,40 +192,67 @@ export async function walkDirectory(rootDir, options = {}) {
165
192
  return;
166
193
  visitedDirs.add(realDir);
167
194
  let entries;
195
+ let handle;
168
196
  try {
169
- entries = await fs.readdir(operationPath, { withFileTypes: true });
197
+ if (options.maxEntries === undefined) {
198
+ entries = await fs.readdir(operationPath, { withFileTypes: true });
199
+ }
200
+ else {
201
+ handle = await fs.opendir(operationPath);
202
+ }
170
203
  }
171
204
  catch (error) {
172
205
  recordFailedDir(result, root, dir, depth, error);
173
206
  return;
174
207
  }
175
208
  const childPrefix = dir.endsWith(path.sep) ? dir : `${dir}${path.sep}`;
176
- for (const dirent of entries) {
177
- if (shouldStop(result, options)) {
178
- result.truncated = true;
179
- return;
209
+ let index = 0;
210
+ try {
211
+ while (true) {
212
+ let dirent;
213
+ try {
214
+ dirent = handle ? await handle.read() : entries[index++];
215
+ }
216
+ catch (error) {
217
+ recordFailedDir(result, root, dir, depth, error);
218
+ return;
219
+ }
220
+ if (!dirent)
221
+ return;
222
+ if (shouldStop(result, options)) {
223
+ result.truncated = true;
224
+ return;
225
+ }
226
+ result.scannedEntryCount += 1;
227
+ const fullPath = childPrefix + dirent.name;
228
+ const kind = resolveKind(fullPath, dirent, symlinks);
229
+ if (!kind)
230
+ continue;
231
+ const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
232
+ const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
233
+ const include = options.include;
234
+ const included = include == null ? true : Reflect.apply(include, options, [entry]);
235
+ if ((isObjectResult(included) ? await included : included) ?? true) {
236
+ result.entries.push(entry);
237
+ }
238
+ if (kind === "directory" &&
239
+ (options.maxDepth === undefined || depth < options.maxDepth)) {
240
+ const descend = options.descend;
241
+ const descended = descend == null ? true : Reflect.apply(descend, options, [entry]);
242
+ if ((isObjectResult(descended) ? await descended : descended) ?? true) {
243
+ await visit(fullPath, relativePath, depth + 1);
244
+ if (result.truncated)
245
+ return;
246
+ }
247
+ }
180
248
  }
181
- result.scannedEntryCount += 1;
182
- const fullPath = childPrefix + dirent.name;
183
- const kind = resolveKind(fullPath, dirent, symlinks);
184
- if (!kind)
185
- continue;
186
- const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
187
- const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
188
- const include = options.include;
189
- const included = include == null ? true : Reflect.apply(include, options, [entry]);
190
- if ((isObjectResult(included) ? await included : included) ?? true) {
191
- result.entries.push(entry);
249
+ }
250
+ finally {
251
+ try {
252
+ await handle?.close();
192
253
  }
193
- if (kind === "directory" &&
194
- (options.maxDepth === undefined || depth < options.maxDepth)) {
195
- const descend = options.descend;
196
- const descended = descend == null ? true : Reflect.apply(descend, options, [entry]);
197
- if ((isObjectResult(descended) ? await descended : descended) ?? true) {
198
- await visit(fullPath, relativePath, depth + 1);
199
- if (result.truncated)
200
- return;
201
- }
254
+ catch (error) {
255
+ recordFailedDir(result, root, dir, depth, error);
202
256
  }
203
257
  }
204
258
  }
@@ -1,9 +1,16 @@
1
+ import { type RootContext } from "./root-context.js";
1
2
  import type { NativeWatchBatch } from "./watch-native.js";
2
3
  import type { WatchSnapshot } from "./watch-scan.js";
3
4
  import type { WatchChange, WatchScope } from "./watch-types.js";
4
5
  export declare function excludedWatchPath(snapshot: WatchSnapshot | undefined, name: string): boolean;
5
6
  export declare function scopedChanges(scopes: readonly WatchScope[], change: WatchChange): WatchChange[];
7
+ /** An undecodable child cannot equal a validated literal scope component. */
8
+ export declare function selectedWatchChildren(scopes: readonly WatchScope[], directory: string): boolean;
9
+ /** A folded directory can contain a missing scope component, unlike a nameless child. */
10
+ export declare function selectedWatchSubtree(scopes: readonly WatchScope[], directory: string): boolean;
6
11
  export declare function nativeChanges(scopes: readonly WatchScope[], snapshot: WatchSnapshot | undefined, batch: NativeWatchBatch, limit?: number, after?: WatchSnapshot): WatchChange[] | undefined;
7
12
  export declare function changedEntries(before: WatchSnapshot | undefined, after: WatchSnapshot, limit: number): WatchChange[] | undefined;
8
13
  /** Backend names never establish authority to publish a pathname. */
9
14
  export declare function guardedHintChanges(scopes: readonly WatchScope[], before: WatchSnapshot | undefined, after: WatchSnapshot, hints: readonly WatchChange[] | undefined, observed: readonly WatchChange[] | undefined, limit: number): WatchChange[] | undefined;
15
+ /** Resolve native spelling aliases without treating case folding as identity. */
16
+ export declare function admittedNativeChanges(root: RootContext, scopes: readonly WatchScope[], before: WatchSnapshot | undefined, after: WatchSnapshot, batch: NativeWatchBatch, signal: AbortSignal, limit: number, scheduling?: boolean): Promise<WatchChange[] | undefined>;
@@ -1,9 +1,22 @@
1
1
  import path from "node:path";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { isNotFoundPathError } from "./path.js";
4
+ import { assertRootIdentityCurrent, resolvePathInRoot } from "./root-context.js";
5
+ import { createRootDirectoryObservationGuard, assertRootDirectoryObservationGuard } from "./root-directory-list.js";
6
+ import { lookupRootDirectoryEntry } from "./root-directory-entry.js";
2
7
  function below(parent, child) {
3
8
  return parent === "" ? child !== "" : child.startsWith(parent + path.sep);
4
9
  }
5
- function distance(parent, child) {
6
- return (parent === "" ? child : child.slice(parent.length + 1)).split(path.sep).length;
10
+ function selectedByTree(scope, name, childDepth = 0) {
11
+ return scope.kind === "tree" && (scope.path === name ? childDepth <= scope.depth : below(scope.path, name) &&
12
+ (scope.path === "" ? name : name.slice(scope.path.length + 1)).split(path.sep).length + childDepth <= scope.depth);
13
+ }
14
+ function addChange(result, change, limit) {
15
+ if (!result.has(change.path) && result.size >= limit)
16
+ return false;
17
+ const prior = result.get(change.path);
18
+ result.set(change.path, prior?.type === "structural" ? prior : change);
19
+ return true;
7
20
  }
8
21
  export function excludedWatchPath(snapshot, name) {
9
22
  const exclusions = snapshot?.excluded;
@@ -20,7 +33,7 @@ export function excludedWatchPath(snapshot, name) {
20
33
  export function scopedChanges(scopes, change) {
21
34
  const result = new Map();
22
35
  for (const scope of scopes) {
23
- if (scope.path === change.path || (scope.kind === "tree" && below(scope.path, change.path) && distance(scope.path, change.path) <= scope.depth)) {
36
+ if (scope.path === change.path || selectedByTree(scope, change.path)) {
24
37
  result.set(change.path, change);
25
38
  }
26
39
  else if (below(change.path, scope.path)) {
@@ -30,6 +43,19 @@ export function scopedChanges(scopes, change) {
30
43
  }
31
44
  return [...result.values()];
32
45
  }
46
+ /** An undecodable child cannot equal a validated literal scope component. */
47
+ export function selectedWatchChildren(scopes, directory) {
48
+ return scopes.some(scope => selectedByTree(scope, directory, 1));
49
+ }
50
+ /** A folded directory can contain a missing scope component, unlike a nameless child. */
51
+ export function selectedWatchSubtree(scopes, directory) {
52
+ return scopes.some(scope => scope.path === directory || below(directory, scope.path) ||
53
+ selectedByTree(scope, directory));
54
+ }
55
+ function literalName(name) {
56
+ return typeof name === "string" && !!name && name !== "." && name !== ".." && !name.includes("\0") && !name.includes("/") &&
57
+ (process.platform !== "win32" || !/[\\:]/.test(name));
58
+ }
33
59
  export function nativeChanges(scopes, snapshot, batch, limit = 256, after) {
34
60
  if (batch.overflow)
35
61
  return undefined;
@@ -37,19 +63,25 @@ export function nativeChanges(scopes, snapshot, batch, limit = 256, after) {
37
63
  for (const hint of batch.hints) {
38
64
  const name = hint.name;
39
65
  // Backend filenames are untrusted hints. Never resolve or perform I/O on them.
40
- if (typeof name !== "string" || !name || name === "." || name === ".." || name.includes("\0") || name.includes("/") || (process.platform === "win32" && /[\\:]/.test(name)))
66
+ if (typeof hint.directory !== "string" || (hint.directory && !hint.directory.split(path.sep).every(literalName)))
41
67
  return undefined;
42
- const relative = hint.directory ? path.join(hint.directory, name) : name;
68
+ const children = hint.event === "children";
69
+ const subtree = hint.event === "subtree";
70
+ if (children || subtree ? name !== "" : !literalName(name))
71
+ return undefined;
72
+ if (subtree)
73
+ continue; // Folded hints only schedule guarded diff passes; never publish their spelling.
74
+ if (children && !selectedWatchChildren(scopes, hint.directory))
75
+ continue;
76
+ const relative = children ? hint.directory : hint.directory ? path.join(hint.directory, name) : name;
43
77
  if (excludedWatchPath(snapshot, relative) || excludedWatchPath(after, relative))
44
78
  continue;
45
79
  for (const change of scopedChanges(scopes, {
46
80
  path: relative,
47
81
  type: hint.event === "change" && snapshot?.entries.get(relative)?.startsWith("file:") ? "content" : "structural",
48
82
  })) {
49
- if (!result.has(change.path) && result.size >= limit)
83
+ if (!addChange(result, change, limit))
50
84
  return undefined;
51
- const prior = result.get(change.path);
52
- result.set(change.path, prior?.type === "structural" ? prior : change);
53
85
  }
54
86
  }
55
87
  return [...result.values()];
@@ -58,17 +90,24 @@ export function changedEntries(before, after, limit) {
58
90
  if (!before)
59
91
  return undefined;
60
92
  const changes = [];
61
- for (const name of new Set([...before.entries.keys(), ...after.entries.keys()])) {
93
+ const compare = (name) => {
62
94
  const left = before.entries.get(name);
63
95
  const right = after.entries.get(name);
64
96
  if (left === right)
65
- continue;
97
+ return true;
66
98
  if (changes.length >= limit)
67
- return undefined;
99
+ return false;
68
100
  const sameFile = left?.startsWith("file:") && right?.startsWith("file:") &&
69
101
  left.split(":").slice(0, 3).join(":") === right.split(":").slice(0, 3).join(":");
70
102
  changes.push(Object.freeze({ path: name, type: sameFile ? "content" : "structural" }));
71
- }
103
+ return true;
104
+ };
105
+ for (const name of before.entries.keys())
106
+ if (!compare(name))
107
+ return undefined;
108
+ for (const name of after.entries.keys())
109
+ if (!before.entries.has(name) && !compare(name))
110
+ return undefined;
72
111
  return changes;
73
112
  }
74
113
  /** Backend names never establish authority to publish a pathname. */
@@ -77,17 +116,194 @@ export function guardedHintChanges(scopes, before, after, hints, observed, limit
77
116
  return undefined;
78
117
  const result = new Map(observed.map(change => [change.path, change]));
79
118
  for (const hint of hints) {
80
- // Only publish an independently observed name (including a deletion from
81
- // the previous guarded snapshot), or a target explicitly supplied by caller.
82
- // A stale/misdirected inode watch may report outside names: erase its detail.
83
- if (!before?.entries.has(hint.path) && !after.entries.has(hint.path) && !scopes.some(scope => scope.path === hint.path))
119
+ // Drop unobserved non-target names only under a parent with the same guarded
120
+ // identity in both passes; otherwise a stale/misdirected watch erases detail.
121
+ if (!before?.entries.has(hint.path) && !after.entries.has(hint.path) && !scopes.some(scope => scope.path === hint.path)) {
122
+ const parent = path.dirname(hint.path);
123
+ const directory = parent === "." ? "" : parent;
124
+ const left = before?.directories.get(directory), right = after.directories.get(directory);
125
+ if (left && right && left.dev === right.dev && left.ino === right.ino)
126
+ continue;
84
127
  return undefined;
128
+ }
85
129
  // Equal snapshots cannot exclude an intermediate change and restoration (ABA).
86
130
  // Preserve admitted hints, including setup activity delivered after ready.
87
- if (!result.has(hint.path) && result.size >= limit)
131
+ if (!addChange(result, hint, limit))
88
132
  return undefined;
89
- const prior = result.get(hint.path);
90
- result.set(hint.path, Object.freeze(prior?.type === "structural" ? prior : hint));
133
+ Object.freeze(result.get(hint.path));
134
+ }
135
+ return [...result.values()];
136
+ }
137
+ /** Resolve native spelling aliases without treating case folding as identity. */
138
+ export async function admittedNativeChanges(root, scopes, before, after, batch, signal, limit, scheduling = false) {
139
+ if (!nativeChanges(scopes, before, batch, limit, after))
140
+ return undefined;
141
+ if (scheduling && !batch.hints.length)
142
+ return undefined;
143
+ const result = new Map();
144
+ const candidates = new Map([...before?.targets ?? [], ...after.targets, ...after.directories]);
145
+ if (scheduling) {
146
+ const identities = new Map();
147
+ for (const { dev, ino } of candidates.values()) {
148
+ let inodes = identities.get(dev);
149
+ if (!inodes)
150
+ identities.set(dev, inodes = new Set());
151
+ // One spelling cannot discard activity selected through another spelling,
152
+ // including entry targets that alias a tree's directory without registering it.
153
+ if (inodes.has(ino))
154
+ return undefined;
155
+ inodes.add(ino);
156
+ }
157
+ }
158
+ const guards = new Map();
159
+ const admittedParent = async (parent) => {
160
+ let guard = guards.get(parent);
161
+ if (!guard) {
162
+ const resolved = await resolvePathInRoot(root, parent ? "./" + parent : ".", { rejectSymlinks: true });
163
+ guard = await createRootDirectoryObservationGuard(root, resolved.resolved);
164
+ const expected = after.directories.get(parent);
165
+ if (expected && (guard.stat.dev !== expected.dev || guard.stat.ino !== expected.ino)) {
166
+ throw new FsSafeError("path-mismatch", "watch hint parent changed during reconciliation");
167
+ }
168
+ guards.set(parent, guard);
169
+ }
170
+ await assertRootDirectoryObservationGuard(root, guard);
171
+ signal.throwIfAborted();
172
+ return guard;
173
+ };
174
+ if (scheduling) {
175
+ // A sibling hint cannot hide a replaced scope anchor or its identity chain.
176
+ for (const name of after.directories.keys())
177
+ if (scopes.some(scope => !name || scope.path === name || scope.path.startsWith(name + path.sep)))
178
+ await admittedParent(name);
179
+ for (const scope of scopes) {
180
+ const anchor = after.scopeAnchors?.get(scope.path);
181
+ if (!anchor)
182
+ return undefined;
183
+ if (!anchor.name)
184
+ continue;
185
+ const guard = await admittedParent(anchor.directory);
186
+ const found = await lookupRootDirectoryEntry(root, guard, anchor.name);
187
+ signal.throwIfAborted();
188
+ const expected = anchor.target;
189
+ // Missing targets and vanished/replaced spelling aliases have no hint inode
190
+ // to match. Recheck the selected spelling before declaring a batch unrelated.
191
+ if (!found) {
192
+ if (expected)
193
+ return undefined;
194
+ }
195
+ else if (!expected || found.identity.dev !== expected.dev || found.identity.ino !== expected.ino ||
196
+ (found.entry.isSymbolicLink ? "symlink" : found.entry.isDirectory ? "directory" : found.entry.isFile ? "file" : "other") !== expected.kind)
197
+ return undefined;
198
+ }
199
+ }
200
+ for (const hint of batch.hints) {
201
+ signal.throwIfAborted();
202
+ if (hint.event === "subtree") {
203
+ if (!scheduling)
204
+ continue; // Only the full guarded snapshot supplies detail.
205
+ if (selectedWatchSubtree(scopes, hint.directory))
206
+ return undefined;
207
+ const guard = await admittedParent(hint.directory);
208
+ for (const [name, identity] of candidates) {
209
+ if (identity.dev === guard.stat.dev && identity.ino === guard.stat.ino && selectedWatchSubtree(scopes, name))
210
+ return undefined;
211
+ }
212
+ continue;
213
+ }
214
+ const name = hint.name; // nativeChanges rejected unknown or non-literal names.
215
+ let parent = hint.directory;
216
+ const relative = parent ? path.join(parent, name) : name;
217
+ if (excludedWatchPath(before, relative) || excludedWatchPath(after, relative)) {
218
+ if (scheduling)
219
+ return undefined;
220
+ continue;
221
+ }
222
+ let guard;
223
+ let expected = after.directories.get(parent);
224
+ if (!expected) {
225
+ // Native recursion observes one Root handle and may report descendants of
226
+ // unselected/entry-only directories. Admit a parent alias by exact identity,
227
+ // not by lowercasing, and do not turn unselected descendants into events.
228
+ try {
229
+ guard = await admittedParent(parent);
230
+ }
231
+ catch (error) {
232
+ await assertRootIdentityCurrent(root);
233
+ if (scheduling)
234
+ throw error;
235
+ if (isNotFoundPathError(error) || (error instanceof FsSafeError && ["not-found", "path-alias", "outside-workspace", "symlink", "not-file"].includes(error.code)))
236
+ continue;
237
+ throw error;
238
+ }
239
+ const admitted = [...after.directories].find(([, identity]) => identity.dev === guard.stat.dev && identity.ino === guard.stat.ino);
240
+ if (!admitted) {
241
+ // A newly created directory may contain selected entries absent from the baseline.
242
+ if (scheduling)
243
+ return undefined;
244
+ continue;
245
+ }
246
+ [parent, expected] = admitted;
247
+ }
248
+ if (scheduling)
249
+ guard = await admittedParent(parent);
250
+ if (hint.event === "children") {
251
+ // There is no child spelling to look up. Only guarded scans may discover it.
252
+ if (selectedWatchChildren(scopes, parent) && !excludedWatchPath(before, parent) && !excludedWatchPath(after, parent) &&
253
+ !addChange(result, { path: parent, type: "structural" }, limit))
254
+ return undefined;
255
+ continue;
256
+ }
257
+ const candidate = parent ? path.join(parent, name) : name;
258
+ if (excludedWatchPath(before, candidate) || excludedWatchPath(after, candidate)) {
259
+ if (scheduling)
260
+ return undefined;
261
+ continue;
262
+ }
263
+ const inspected = scheduling ? await lookupRootDirectoryEntry(root, guard, name) : undefined;
264
+ signal.throwIfAborted();
265
+ // A vanished or multiply linked leaf may have changed selected entries in
266
+ // another directory. Its current spelling cannot prove unrelatedness.
267
+ if (scheduling && (!inspected || (!inspected.entry.isDirectory && inspected.entry.nlink !== 1)))
268
+ return undefined;
269
+ const selected = scopedChanges(scopes, { path: candidate,
270
+ type: hint.event === "change" && before?.entries.get(candidate)?.startsWith("file:") ? "content" : "structural" });
271
+ if (selected.length) {
272
+ for (const change of selected)
273
+ if (!addChange(result, change, limit))
274
+ return undefined;
275
+ continue;
276
+ }
277
+ guard ??= await admittedParent(parent);
278
+ if (guard.stat.dev !== expected.dev || guard.stat.ino !== expected.ino) {
279
+ throw new FsSafeError("path-mismatch", "watch hint parent changed during reconciliation");
280
+ }
281
+ const found = scheduling ? inspected : await lookupRootDirectoryEntry(root, guard, name);
282
+ signal.throwIfAborted();
283
+ // A missing unselected sibling is not evidence of lost selected detail.
284
+ // Previously observed selected deletions remain visible in the snapshot diff;
285
+ // an unseen, already-gone name has no identity proving a selected alias.
286
+ if (!found)
287
+ continue;
288
+ if (scheduling && found.entry.isSymbolicLink)
289
+ return undefined;
290
+ for (const [relative, identity] of candidates) {
291
+ if (!relative || identity.dev !== found.identity.dev || identity.ino !== found.identity.ino)
292
+ continue;
293
+ if ((path.dirname(relative) === "." ? "" : path.dirname(relative)) !== parent) {
294
+ if (scheduling)
295
+ return undefined;
296
+ continue;
297
+ }
298
+ for (const change of scopedChanges(scopes, { path: relative, type: "structural" }))
299
+ if (!addChange(result, change, limit))
300
+ return undefined;
301
+ }
302
+ await assertRootDirectoryObservationGuard(root, guard);
91
303
  }
304
+ for (const guard of guards.values())
305
+ await assertRootDirectoryObservationGuard(root, guard);
306
+ await assertRootIdentityCurrent(root);
307
+ signal.throwIfAborted();
92
308
  return [...result.values()];
93
309
  }
@@ -1,11 +1,11 @@
1
1
  import { type NativeBinding } from "./native.js";
2
2
  import type { RootContext } from "./root-context.js";
3
- import type { DirectoryIdentity } from "./watch-scan.js";
3
+ import type { DirectoryIdentity, WatchSnapshot } from "./watch-scan.js";
4
4
  import type { WatchStreamPaths } from "./watch-stream.js";
5
5
  export type NativeWatchHint = {
6
6
  directory: string;
7
7
  name: string;
8
- event: "rename" | "change";
8
+ event: "rename" | "change" | "children" | "subtree";
9
9
  };
10
10
  export type NativeWatchBatch = {
11
11
  hints: NativeWatchHint[];
@@ -18,19 +18,38 @@ export type NativeWatchWireBatch = {
18
18
  name: string;
19
19
  structural: boolean;
20
20
  flags?: number;
21
+ namelessChild?: boolean;
22
+ subtree?: boolean;
21
23
  }[];
22
24
  overflow: boolean;
23
25
  error?: string;
24
26
  };
27
+ export type NativeWatchEntry = {
28
+ scope: string;
29
+ directory: {
30
+ root: string;
31
+ relative: string;
32
+ rootDev: bigint;
33
+ rootIno: bigint;
34
+ dev: bigint;
35
+ ino: bigint;
36
+ };
37
+ name: string;
38
+ target?: DirectoryIdentity & {
39
+ kind: string;
40
+ };
41
+ };
25
42
  export declare function watchBinding(mode: "auto" | "events" | "poll"): NativeBinding | undefined;
26
43
  export declare class NativeWatchBackend {
27
44
  private binding;
28
45
  private root;
29
46
  private id;
30
47
  private streamPaths;
48
+ directories: number | undefined;
31
49
  constructor(binding: NativeBinding, root: RootContext, callback: (batch: NativeWatchBatch) => void, limit: number, persistent: boolean);
32
50
  add(name: string, identity: DirectoryIdentity): void;
33
51
  testEvent(path: string, flags: number): void;
52
+ entries(snapshot: WatchSnapshot): boolean;
34
53
  configure(paths: WatchStreamPaths): boolean;
35
54
  close(): void;
36
55
  }
@@ -6,7 +6,7 @@ export function watchBinding(mode) {
6
6
  return;
7
7
  const binding = getNativeBinding(); // Preserves require + missing-addon failure.
8
8
  // Bun TSFN teardown remains unqualified; guarded native scans remain usable.
9
- if (binding?.watchRegister && (process.platform !== "darwin" || binding.watchConfigure) && !process.versions.bun && !process.versions.deno && ["linux", "darwin", "win32"].includes(process.platform))
9
+ if (binding?.watchRegister && (process.platform !== "darwin" || (binding.watchConfigure && binding.watchEntries)) && !process.versions.bun && !process.versions.deno && ["linux", "darwin", "win32"].includes(process.platform))
10
10
  return binding;
11
11
  if (mode === "events" || getFsSafeNativeConfig().mode === "require") {
12
12
  throw new FsSafeError("helper-unavailable", "native watch events are unavailable", { details: { operation: "watch" } });
@@ -17,15 +17,16 @@ export class NativeWatchBackend {
17
17
  root;
18
18
  id;
19
19
  streamPaths;
20
+ directories;
20
21
  constructor(binding, root, callback, limit, persistent) {
21
22
  this.binding = binding;
22
23
  this.root = root;
23
- this.streamPaths = JSON.stringify({ anchors: [root.rootReal], exclusions: [] });
24
+ this.streamPaths = JSON.stringify({ anchors: [], exclusions: [] });
24
25
  try {
25
26
  this.id = binding.watchRegister(root.rootReal, limit, batch => {
26
27
  if (this.id !== undefined)
27
28
  callback({ overflow: batch.overflow, error: batch.error, hints: batch.hints.map(hint => ({
28
- directory: hint.directory, name: hint.name, event: hint.structural ? "rename" : "change",
29
+ directory: hint.directory, name: hint.name, event: hint.subtree ? "subtree" : hint.namelessChild ? "children" : hint.structural ? "rename" : "change",
29
30
  })) });
30
31
  }, persistent);
31
32
  }
@@ -43,6 +44,23 @@ export class NativeWatchBackend {
43
44
  }
44
45
  }
45
46
  testEvent(path, flags) { this.binding.watchTestEvent(this.id, path, flags); }
47
+ entries(snapshot) {
48
+ if (process.platform !== "darwin")
49
+ return false;
50
+ const entries = [...snapshot.entryAnchors ?? []].map(([scope, anchor]) => ({
51
+ scope, name: anchor.name, target: anchor.target,
52
+ directory: { root: this.root.rootReal, relative: anchor.directory,
53
+ rootDev: BigInt(this.root.rootIdentity.dev), rootIno: BigInt(this.root.rootIdentity.ino), ...snapshot.directories.get(anchor.directory) },
54
+ }));
55
+ try {
56
+ const result = this.binding.watchEntries(this.id, entries);
57
+ this.directories = result.directories;
58
+ return result.changed;
59
+ }
60
+ catch (cause) {
61
+ throw watchError(cause);
62
+ }
63
+ }
46
64
  configure(paths) {
47
65
  if (process.platform !== "darwin")
48
66
  return false;
@@ -0,0 +1,6 @@
1
+ import type { WatchChange, WatchScope } from "./watch-types.js";
2
+ import type { WatchSnapshot } from "./watch-scan.js";
3
+ /** Only previously enumerated directories can be reconciled without discovering topology. */
4
+ export declare function watchRescanScopes(scopes: readonly WatchScope[], snapshot: WatchSnapshot, changes: readonly WatchChange[]): readonly WatchScope[] | undefined;
5
+ /** Merge only freshly observed slices. Any topology/anchor ambiguity requires a full pass. */
6
+ export declare function mergeWatchRescan(scopes: readonly WatchScope[], before: WatchSnapshot, slice: WatchSnapshot, requested: readonly WatchScope[]): WatchSnapshot | undefined;