@vibe-agent-toolkit/utils 0.1.42 → 0.2.0-rc.10

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 (279) hide show
  1. package/README.md +81 -19
  2. package/dist/asset-reference.js +3 -11
  3. package/dist/asset-reference.js.map +1 -1
  4. package/dist/compare-code-units.d.ts +28 -0
  5. package/dist/compare-code-units.d.ts.map +1 -0
  6. package/dist/compare-code-units.js +33 -0
  7. package/dist/compare-code-units.js.map +1 -0
  8. package/dist/crawl-timing.d.ts +657 -0
  9. package/dist/crawl-timing.d.ts.map +1 -0
  10. package/dist/crawl-timing.js +765 -0
  11. package/dist/crawl-timing.js.map +1 -0
  12. package/dist/crawl.d.ts +2 -4
  13. package/dist/crawl.d.ts.map +1 -1
  14. package/dist/crawl.js +2 -4
  15. package/dist/crawl.js.map +1 -1
  16. package/dist/dirent-kind.d.ts +83 -0
  17. package/dist/dirent-kind.d.ts.map +1 -0
  18. package/dist/dirent-kind.js +129 -0
  19. package/dist/dirent-kind.js.map +1 -0
  20. package/dist/entrypoint.d.ts +54 -0
  21. package/dist/entrypoint.d.ts.map +1 -0
  22. package/dist/entrypoint.js +71 -0
  23. package/dist/entrypoint.js.map +1 -0
  24. package/dist/env-flag.d.ts +51 -0
  25. package/dist/env-flag.d.ts.map +1 -0
  26. package/dist/env-flag.js +62 -0
  27. package/dist/env-flag.js.map +1 -0
  28. package/dist/errors/errno.d.ts +51 -0
  29. package/dist/errors/errno.d.ts.map +1 -0
  30. package/dist/errors/errno.js +112 -0
  31. package/dist/errors/errno.js.map +1 -0
  32. package/dist/errors/vat-error.d.ts +60 -0
  33. package/dist/errors/vat-error.d.ts.map +1 -0
  34. package/dist/errors/vat-error.js +86 -0
  35. package/dist/errors/vat-error.js.map +1 -0
  36. package/dist/file-crawler.d.ts +60 -0
  37. package/dist/file-crawler.d.ts.map +1 -1
  38. package/dist/file-crawler.js +214 -24
  39. package/dist/file-crawler.js.map +1 -1
  40. package/dist/fs/file-hash.d.ts.map +1 -1
  41. package/dist/fs/file-hash.js +0 -1
  42. package/dist/fs/file-hash.js.map +1 -1
  43. package/dist/fs-utils.d.ts +628 -38
  44. package/dist/fs-utils.d.ts.map +1 -1
  45. package/dist/fs-utils.js +770 -71
  46. package/dist/fs-utils.js.map +1 -1
  47. package/dist/fs.d.ts +5 -1
  48. package/dist/fs.d.ts.map +1 -1
  49. package/dist/fs.js +9 -1
  50. package/dist/fs.js.map +1 -1
  51. package/dist/git-root-cache.d.ts +44 -0
  52. package/dist/git-root-cache.d.ts.map +1 -0
  53. package/dist/git-root-cache.js +68 -0
  54. package/dist/git-root-cache.js.map +1 -0
  55. package/dist/git-run.d.ts +167 -0
  56. package/dist/git-run.d.ts.map +1 -0
  57. package/dist/git-run.js +154 -0
  58. package/dist/git-run.js.map +1 -0
  59. package/dist/git-snapshot.d.ts +199 -0
  60. package/dist/git-snapshot.d.ts.map +1 -0
  61. package/dist/git-snapshot.js +250 -0
  62. package/dist/git-snapshot.js.map +1 -0
  63. package/dist/git-tracker.d.ts +166 -3
  64. package/dist/git-tracker.d.ts.map +1 -1
  65. package/dist/git-tracker.js +250 -22
  66. package/dist/git-tracker.js.map +1 -1
  67. package/dist/git-utils.d.ts +95 -1
  68. package/dist/git-utils.d.ts.map +1 -1
  69. package/dist/git-utils.js +227 -81
  70. package/dist/git-utils.js.map +1 -1
  71. package/dist/git.d.ts +13 -4
  72. package/dist/git.d.ts.map +1 -1
  73. package/dist/git.js +12 -4
  74. package/dist/git.js.map +1 -1
  75. package/dist/gitignore-checker.d.ts.map +1 -1
  76. package/dist/gitignore-checker.js +15 -6
  77. package/dist/gitignore-checker.js.map +1 -1
  78. package/dist/glob/glob-pattern.js +5 -5
  79. package/dist/glob/glob-pattern.js.map +1 -1
  80. package/dist/index.d.ts +33 -19
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +113 -33
  83. package/dist/index.js.map +1 -1
  84. package/dist/listing-refusal.d.ts +112 -0
  85. package/dist/listing-refusal.d.ts.map +1 -0
  86. package/dist/listing-refusal.js +105 -0
  87. package/dist/listing-refusal.js.map +1 -0
  88. package/dist/numeric-args.d.ts +24 -0
  89. package/dist/numeric-args.d.ts.map +1 -0
  90. package/dist/numeric-args.js +37 -0
  91. package/dist/numeric-args.js.map +1 -0
  92. package/dist/path-containment.d.ts +64 -0
  93. package/dist/path-containment.d.ts.map +1 -0
  94. package/dist/path-containment.js +111 -0
  95. package/dist/path-containment.js.map +1 -0
  96. package/dist/path-core.d.ts +143 -18
  97. package/dist/path-core.d.ts.map +1 -1
  98. package/dist/path-core.js +171 -23
  99. package/dist/path-core.js.map +1 -1
  100. package/dist/path-utils.d.ts.map +1 -1
  101. package/dist/path-utils.js +42 -47
  102. package/dist/path-utils.js.map +1 -1
  103. package/dist/path.d.ts +1 -1
  104. package/dist/path.d.ts.map +1 -1
  105. package/dist/path.js +1 -1
  106. package/dist/path.js.map +1 -1
  107. package/dist/process.d.ts +7 -0
  108. package/dist/process.d.ts.map +1 -1
  109. package/dist/process.js +7 -0
  110. package/dist/process.js.map +1 -1
  111. package/dist/project-utils.d.ts +7 -1
  112. package/dist/project-utils.d.ts.map +1 -1
  113. package/dist/project-utils.js +24 -10
  114. package/dist/project-utils.js.map +1 -1
  115. package/dist/project.d.ts +7 -5
  116. package/dist/project.d.ts.map +1 -1
  117. package/dist/project.js +7 -5
  118. package/dist/project.js.map +1 -1
  119. package/dist/safe-exec.d.ts +40 -11
  120. package/dist/safe-exec.d.ts.map +1 -1
  121. package/dist/safe-exec.js +111 -25
  122. package/dist/safe-exec.js.map +1 -1
  123. package/dist/skill-targets.d.ts +2 -2
  124. package/dist/skill-targets.js +2 -2
  125. package/dist/skill-test/auth-resolver.d.ts +10 -2
  126. package/dist/skill-test/auth-resolver.d.ts.map +1 -1
  127. package/dist/skill-test/auth-resolver.js +26 -11
  128. package/dist/skill-test/auth-resolver.js.map +1 -1
  129. package/dist/skill-test/index.d.ts +1 -1
  130. package/dist/skill-test/index.d.ts.map +1 -1
  131. package/dist/skill-test/index.js +1 -1
  132. package/dist/skill-test/index.js.map +1 -1
  133. package/dist/skill-test/spawn-claude.d.ts +30 -1
  134. package/dist/skill-test/spawn-claude.d.ts.map +1 -1
  135. package/dist/skill-test/spawn-claude.js +71 -5
  136. package/dist/skill-test/spawn-claude.js.map +1 -1
  137. package/dist/skill-test/transcript.d.ts +41 -2
  138. package/dist/skill-test/transcript.d.ts.map +1 -1
  139. package/dist/skill-test/transcript.js +55 -10
  140. package/dist/skill-test/transcript.js.map +1 -1
  141. package/dist/spawn-hardened.js +0 -1
  142. package/dist/spawn-hardened.js.map +1 -1
  143. package/dist/stdio-blocking.js +13 -14
  144. package/dist/stdio-blocking.js.map +1 -1
  145. package/dist/test-helpers.d.ts +130 -79
  146. package/dist/test-helpers.d.ts.map +1 -1
  147. package/dist/test-helpers.js +229 -124
  148. package/dist/test-helpers.js.map +1 -1
  149. package/dist/testing/executables.d.ts +32 -0
  150. package/dist/testing/executables.d.ts.map +1 -0
  151. package/dist/testing/executables.js +70 -0
  152. package/dist/testing/executables.js.map +1 -0
  153. package/dist/testing/hostile-tree.d.ts +79 -0
  154. package/dist/testing/hostile-tree.d.ts.map +1 -0
  155. package/dist/testing/hostile-tree.js +164 -0
  156. package/dist/testing/hostile-tree.js.map +1 -0
  157. package/dist/testing/platform-gates.d.ts +31 -0
  158. package/dist/testing/platform-gates.d.ts.map +1 -0
  159. package/dist/testing/platform-gates.js +31 -0
  160. package/dist/testing/platform-gates.js.map +1 -0
  161. package/dist/testing/temp-dir.d.ts +255 -0
  162. package/dist/testing/temp-dir.d.ts.map +1 -0
  163. package/dist/testing/temp-dir.js +363 -0
  164. package/dist/testing/temp-dir.js.map +1 -0
  165. package/dist/testing.d.ts +94 -3
  166. package/dist/testing.d.ts.map +1 -1
  167. package/dist/testing.js +119 -3
  168. package/dist/testing.js.map +1 -1
  169. package/dist/text-content.d.ts +197 -0
  170. package/dist/text-content.d.ts.map +1 -0
  171. package/dist/text-content.js +354 -0
  172. package/dist/text-content.js.map +1 -0
  173. package/dist/text-file.d.ts +45 -0
  174. package/dist/text-file.d.ts.map +1 -0
  175. package/dist/text-file.js +51 -0
  176. package/dist/text-file.js.map +1 -0
  177. package/dist/text.d.ts +16 -0
  178. package/dist/text.d.ts.map +1 -0
  179. package/dist/text.js +16 -0
  180. package/dist/text.js.map +1 -0
  181. package/dist/timing-dump.d.ts +136 -0
  182. package/dist/timing-dump.d.ts.map +1 -0
  183. package/dist/timing-dump.js +229 -0
  184. package/dist/timing-dump.js.map +1 -0
  185. package/dist/windows-shell.d.ts +0 -1
  186. package/dist/windows-shell.d.ts.map +1 -1
  187. package/dist/windows-shell.js +0 -1
  188. package/dist/windows-shell.js.map +1 -1
  189. package/dist/zod-introspection.js +2 -2
  190. package/dist/zod-introspection.js.map +1 -1
  191. package/eslint/README.md +224 -50
  192. package/eslint/index.cjs +111 -97
  193. package/eslint/index.d.cts +16 -6
  194. package/eslint/rules/callee-name.cjs +23 -0
  195. package/eslint/rules/commands-import-boundary.cjs +148 -0
  196. package/eslint/rules/dead-import.cjs +62 -12
  197. package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
  198. package/eslint/rules/eslint-rule-factory.cjs +21 -3
  199. package/eslint/rules/exempt-path-matcher.cjs +2 -1
  200. package/eslint/rules/explicit-zod-strictness.cjs +120 -0
  201. package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
  202. package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
  203. package/eslint/rules/no-bare-symlink-in-tests.cjs +213 -0
  204. package/eslint/rules/no-blind-catch.cjs +171 -0
  205. package/eslint/rules/no-child-process-execSync.cjs +8 -0
  206. package/eslint/rules/no-decaying-referent.cjs +217 -0
  207. package/eslint/rules/no-dotdot-containment.cjs +160 -0
  208. package/eslint/rules/no-file-url-string-concat.cjs +4 -1
  209. package/eslint/rules/no-fragile-entrypoint-guard.cjs +228 -0
  210. package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
  211. package/eslint/rules/no-fs-promises-cp.cjs +7 -0
  212. package/eslint/rules/no-fs-realpathSync.cjs +8 -0
  213. package/eslint/rules/no-hardcoded-path-split.cjs +29 -18
  214. package/eslint/rules/no-io-in-unit-tier.cjs +129 -0
  215. package/eslint/rules/no-literal-process-exit.cjs +129 -0
  216. package/eslint/rules/no-manual-path-normalize.cjs +146 -96
  217. package/eslint/rules/no-os-tmpdir.cjs +8 -0
  218. package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
  219. package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
  220. package/eslint/rules/no-path-startswith.cjs +7 -2
  221. package/eslint/rules/no-process-exit-in-phase.cjs +127 -0
  222. package/eslint/rules/no-raw-node-path.cjs +563 -0
  223. package/eslint/rules/no-raw-text-decode.cjs +305 -0
  224. package/eslint/rules/no-registry-count-pin.cjs +255 -0
  225. package/eslint/rules/no-self-package-import.cjs +168 -0
  226. package/eslint/rules/no-test-scoped-functions.cjs +10 -3
  227. package/eslint/rules/no-unix-shell-commands.cjs +6 -2
  228. package/eslint/rules/no-unsafe-root-join.cjs +23 -2
  229. package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
  230. package/eslint/rules/no-version-literal.cjs +151 -0
  231. package/eslint/rules/prefer-startswith-over-regex.cjs +33 -1
  232. package/eslint/rules/require-justified-skip.cjs +8 -2
  233. package/eslint/rules/safe-import.cjs +1 -1
  234. package/package.json +16 -16
  235. package/dist/link-auth/build-headers.d.ts +0 -34
  236. package/dist/link-auth/build-headers.d.ts.map +0 -1
  237. package/dist/link-auth/build-headers.js +0 -58
  238. package/dist/link-auth/build-headers.js.map +0 -1
  239. package/dist/link-auth/expand-macro.d.ts +0 -38
  240. package/dist/link-auth/expand-macro.d.ts.map +0 -1
  241. package/dist/link-auth/expand-macro.js +0 -133
  242. package/dist/link-auth/expand-macro.js.map +0 -1
  243. package/dist/link-auth/macros.yaml +0 -50
  244. package/dist/link-auth/resolve-token.d.ts +0 -83
  245. package/dist/link-auth/resolve-token.d.ts.map +0 -1
  246. package/dist/link-auth/resolve-token.js +0 -101
  247. package/dist/link-auth/resolve-token.js.map +0 -1
  248. package/dist/link-auth/resolve.d.ts +0 -102
  249. package/dist/link-auth/resolve.d.ts.map +0 -1
  250. package/dist/link-auth/resolve.js +0 -66
  251. package/dist/link-auth/resolve.js.map +0 -1
  252. package/dist/link-auth/rewrite.d.ts +0 -52
  253. package/dist/link-auth/rewrite.d.ts.map +0 -1
  254. package/dist/link-auth/rewrite.js +0 -102
  255. package/dist/link-auth/rewrite.js.map +0 -1
  256. package/dist/link-auth/select-provider.d.ts +0 -30
  257. package/dist/link-auth/select-provider.d.ts.map +0 -1
  258. package/dist/link-auth/select-provider.js +0 -55
  259. package/dist/link-auth/select-provider.js.map +0 -1
  260. package/dist/link-auth/template.d.ts +0 -40
  261. package/dist/link-auth/template.d.ts.map +0 -1
  262. package/dist/link-auth/template.js +0 -89
  263. package/dist/link-auth/template.js.map +0 -1
  264. package/dist/link-auth/transforms.d.ts +0 -46
  265. package/dist/link-auth/transforms.d.ts.map +0 -1
  266. package/dist/link-auth/transforms.js +0 -52
  267. package/dist/link-auth/transforms.js.map +0 -1
  268. package/dist/template-entry.d.ts +0 -10
  269. package/dist/template-entry.d.ts.map +0 -1
  270. package/dist/template-entry.js +0 -10
  271. package/dist/template-entry.js.map +0 -1
  272. package/dist/template.d.ts +0 -7
  273. package/dist/template.d.ts.map +0 -1
  274. package/dist/template.js +0 -18
  275. package/dist/template.js.map +0 -1
  276. package/eslint/rules/no-path-join.cjs +0 -14
  277. package/eslint/rules/no-path-relative.cjs +0 -14
  278. package/eslint/rules/no-path-resolve.cjs +0 -14
  279. package/eslint/rules/path-function-rule-factory.cjs +0 -417
package/README.md CHANGED
@@ -16,38 +16,47 @@ bun add @vibe-agent-toolkit/utils
16
16
 
17
17
  Every area has its own subpath. Import the one you need.
18
18
 
19
- The package sets `"sideEffects": false`, so a modern bundler will tree-shake unused code out of the `.` barrel — importing `safePath` from `.` and from `./path` produce near-identical bundles. **Subpaths are not primarily a size optimization.** What they control is what your build has to *resolve* and what your module graph *reaches*: the `.` barrel reaches `yaml`, `handlebars`, and `node:fs` no matter what you destructure from it, so it cannot be bundled for a browser target and requires every dependency to be installed. A narrow entry reaches only what it needs.
19
+ The package sets `"sideEffects": false`, so a modern bundler will tree-shake unused code out of the `.` barrel — importing `safePath` from `.` and from `./path` produce near-identical bundles. **Subpaths are not primarily a size optimization.** What they control is what your build has to *resolve* and what your module graph *reaches*. A narrow entry reaches only what it needs.
20
+
21
+ **The `.` barrel reaches no third-party package at all.** Every domain that carries a dependency — directly or transitively — is a subpath: `./crawl`, `./git`, `./process`, `./skill-test`, `./yaml`. That is asserted by equality in `test/subpath-purity.test.ts`, so the barrel cannot silently regain one. It still reaches `node:fs` and friends, so it is Node-only; what it does not do is make a consumer of `safePath` install a template engine.
20
22
 
21
23
  The last two columns are the ones that matter when choosing. **"Resolves with zero deps installed?"** is the sharper of the two: it separates an entry that is merely *heavy* from one that is *unbuildable* in an environment where the package's third-party dependencies are absent or unresolvable.
22
24
 
23
25
  | Subpath | Contents | Node builtins reached | Third-party | Resolves with zero deps installed? |
24
26
  |---|---|---|---|---|
25
- | `./path` | `safePath`, `toForwardSlash`, `isAbsolutePath`, `isAbsoluteAnyPlatform`, `hasParentTraversalSegment`, `toAbsolutePath`, `getRelativePath`, `issueLocation` | `path` only | — | **yes** |
27
+ | `./path` | `safePath`, `toForwardSlash`, `toForwardSlashAnyPlatform`, `toNfc`, `isAbsolutePath`, `isAbsoluteAnyPlatform`, `hasParentTraversalSegment`, `relativeEscapesRoot`, `isSingleFsSegment`, `toAbsolutePath`, `getRelativePath`, `issueLocation` | `path` only | — | **yes** |
28
+ | `./text` | `decodeTextContent` — the one bytes-to-text seam: BOM-announced UTF-8/UTF-16LE/UTF-16BE/UTF-32LE/UTF-32BE, BOM stripped, UTF-8 assumed otherwise; reports the encoding, whether it was a BOM fact or an assumption, and how many U+FFFD the decode substituted | **none** | — | **yes** |
26
29
  | `./zod` | `ZodTypeNames`, `getZodTypeName`, `isZodType`, `unwrapZodType`, `isZodOptional`, `isZodNullable` | **none** | — | **yes** |
27
30
  | `./glob` | `isGlob`, static base extraction, magic remainder | `path` only | — | **yes** |
28
- | `./fs` | `normalizePath`, `normalizedTmpdir`, `mkdirSyncReal`, `resolveFromImportMeta`, `dynamicImportPath`, `copyDirectory`, `verifyCaseSensitiveFilename`, `FsLookupCache` | `fs`, `fs/promises`, `os`, `path`, `url` | — | **yes** |
29
- | `./testing` | `getTestOutputDir`, `getTestOutputBase`, `setupAsyncTempDirSuite`, `setupSyncTempDirSuite` | `crypto`, `fs`, `fs/promises`, `os`, `path`, `url` | — | **yes** |
31
+ | `./fs` | `normalizePath`, `normalizedTmpdir`, `mkdirSyncReal`, `resolveFromImportMeta`, `dynamicImportPath`, `copyDirectory`, `fillPathSpellings`, `pathSpellingFrom`, `DirectorySpellingIndex`, `spellingWalkRoot`, `fillRealpaths`, `realpathFrom`, `FsLookupCache`, `readTextContent`, `readTextContentSync` | `fs`, `fs/promises`, `os`, `path`, `url`, `util` | — | **yes** |
32
+ | `./testing` | `getTestOutputDir`, `getTestOutputBase`, `setupAsyncTempDirSuite`, `setupSyncTempDirSuite`, `removeScratchDir`, `createTempDir`, `removeTempDir`, `tempDirTracker`, `CANNOT_DENY_READS`, `buildHostileTree`, `HOSTILE_NAMES`, `symlinkCapability`, `createSymlink`, `createSymlinkAsync` | `crypto`, `fs`, `fs/promises`, `os`, `path`, `url` | — | **yes** |
30
33
  | `./asset` | `resolveAssetReference` — paths and npm bare specifiers | `fs`, `module`, `os`, `path`, `url` | — | **yes** |
31
34
  | `./yaml` | `updateYamlIn`, `verifyConfinedYamlEdit` — byte-surgical YAML edits | **none** | `yaml` | no — needs `yaml` |
32
- | `./template` | `renderTemplate` — cached Handlebars | **none** | `handlebars` | no — needs `handlebars` |
33
- | `./process` | `safeExecSync`, `safeExecResult`, `safeExecFromString`, `isToolAvailable`, `getToolVersion`, `hasShellSyntax`, `CommandExecutionError`, `spawnHardened`, `shouldUseShell`, `windowsShellQuote`, `buildWindowsShellLine`, `resolveShellCommandToken`, `isPathLike`, `makeStdioBlocking`, `describeStdioBlocking` | `child_process`, `path` | `which` | no — needs `which` |
34
- | `./git` | `gitFindRoot`, `gitLsFiles`, `isGitIgnored`, `loadGitignoreRules`, `GitTracker`, `parseGitUrl`, `isGitUrl`, `nonInteractiveGitOverrides` | `child_process`, `fs`, `os`, `path`, `url` | `ignore`, `which` | no — needs `which`, `ignore` |
35
- | `./crawl` | `crawlDirectory`, `crawlDirectorySync`, `NEVER_CRAWL_GLOBS`, `BUILD_OUTPUT_GLOBS` | `child_process`, `fs`, `os`, `path`, `url` | `picomatch`, `which` | no — needs `picomatch`, `which` |
36
- | `./project` | `findProjectRoot`, `findConfigFile`, `findNodeWorkspaceRoot`, `resetProjectRootCaches` | `fs`, `path` | — | **yes** |
37
- | `./eslint` | the 21 ESLint rules that enforce everything above — see [ESLint rules](#eslint-rules--vibe-agent-toolkitutilseslint) | **none** | — | **yes** |
38
- | `.` | every runtime entry above (not `./eslint`) | all of the above, plus `stream` | `handlebars`, `ignore`, `picomatch`, `which`, `yaml` | no — needs all of them |
35
+ | `./process` | `safeExecSync`, `safeExecResult`, `safeExecFromString`, `isToolAvailable`, `getToolVersion`, `hasShellSyntax`, `CommandExecutionError`, `spawnHardened`, `shouldUseShell`, `windowsShellQuote`, `buildWindowsShellLine`, `resolveShellCommandToken`, `isPathLike`, `makeStdioBlocking`, `describeStdioBlocking` | `child_process`, `path` | `@vibe-validate/git`, `which` | no — needs both |
36
+ | `./git` | `runGit`, `runGitOrThrow`, `gitFindRoot`, `gitLsFiles`, `gitLsOthers`, `isGitIgnored`, `loadGitignoreRules`, `GitTracker`, `gitTreeSnapshot`, `peekGitTreeSnapshot`, `withGitSnapshotCache`, `parseGitUrl`, `isGitUrl`, `nonInteractiveGitOverrides` | `async_hooks`, `fs`, `fs/promises`, `os`, `path`, `url` | `@vibe-validate/git`, `ignore` | no — needs both |
37
+ | `./crawl` | `crawlDirectory`, `crawlDirectorySync`, `crawlPathFilter`, `NEVER_CRAWL_GLOBS`, `BUILD_OUTPUT_GLOBS` | `fs`, `os`, `path`, `url` | `@vibe-validate/git`, `picomatch` | no — needs both |
38
+ | `./skill-test` | `spawnHeadlessClaude`, `assembleClaudeArgs`, `killAllActiveClaudeChildren`, `resolveAuth`, `probeAuthStatus`, `AuthPreflightError`, `applyDeclaredEnv`, `buildForwardedEnv`, `formatForwardedEnvLine`, `isProtectedName`, `protectedEnvNames`, `parseStreamJsonTranscript`, `detectInvocationFromTranscript` | `child_process`, `path`, `stream` | `@vibe-validate/git`, `which` | no — needs both |
39
+ | `./project` | `findProjectRoot`, `findConfigFile`, `findNodeWorkspaceRoot`, `resetProjectRootCaches` | `fs`, `fs/promises`, `os`, `path`, `url` | — | **yes** |
40
+ | `./eslint` | the 22 ESLint rules that enforce everything above — see [ESLint rules](#eslint-rules--vibe-agent-toolkitutilseslint) | **none** | — | **yes** |
41
+ | `.` | the dependency-free entries above — path, text, fs, asset, project, testing, zod, glob, plus the crawl-timing seam | `async_hooks`, `crypto`, `fs`, `fs/promises`, `module`, `os`, `path`, `url`, `util` | — | **yes** |
39
42
  | `./package.json` | the manifest itself, for version reporting and resolution assertions | — | — | **yes** |
40
43
 
41
44
  Note `./zod` reaches nothing at all: it detects Zod types by duck-typing `_def.typeName` rather than importing Zod, which is exactly why it works across Zod v3 and v4.
42
45
 
43
- `./crawl` is the only *subpath* that reaches `picomatch` (the `.` barrel also reaches it, via linkAuth's host-pattern matching), and it is deliberately *not* folded into `./glob` — `./glob` is guarded as portable (`node:path`, no third-party), and directory crawling would break both halves of that guarantee.
46
+ `./crawl` is the only entry that reaches `picomatch`, and it is deliberately *not* folded into `./glob` — `./glob` is guarded as portable (`node:path`, no third-party), and directory crawling would break both halves of that guarantee.
47
+
48
+ `./skill-test` declares no dependency of its own; it is a separate entry because of what it *reaches*. Spawning a headless agent goes through `./process`, which costs `which` and — because `safeExecSync` refuses the `git` binary and delegates — `@vibe-validate/git`. Reachability is the criterion, not the import a module happens to write.
49
+
50
+ `./git` is the only published route to `runGit`. `safeExecSync` and `safeExecResult` on `./process` refuse `git` outright and point here, so a caller that wants git gets the scrubbed environment by construction rather than by remembering to ask.
44
51
 
45
52
  ```typescript
46
53
  // Reaches node:path and nothing else
47
54
  import { safePath, toForwardSlash } from '@vibe-agent-toolkit/utils/path';
55
+
56
+ // Reaches `which` and @vibe-validate/git — a real install cost, so it is its own entry
48
57
  import { safeExecSync, spawnHardened } from '@vibe-agent-toolkit/utils/process';
49
58
 
50
- // Reaches yaml, handlebars, and node:fs regardless of what you destructure
59
+ // Reaches node builtins only — no third-party package, whatever you destructure
51
60
  import { safePath } from '@vibe-agent-toolkit/utils';
52
61
  ```
53
62
 
@@ -61,8 +70,8 @@ This package targets **Node >= 22** and is not published for browsers. Most entr
61
70
 
62
71
  A guard test in `test/subpath-purity.test.ts` walks each entry's transitive source graph and enforces **both** of the table's last two columns:
63
72
 
64
- - **Third-party reach** — the "Resolves with zero deps installed?" column. Every entry's expected third-party set is asserted exactly, so every **yes** row above is a tested claim rather than a documented intention, and adding a dependency to any entry is a deliberate, reviewed edit.
65
- - **Builtin reach** — five entries are held to a stricter contract still: `./zod`, `./yaml`, `./template` reach **no Node builtin at all**, and `./path`, `./glob` reach **`node:path` and nothing else** — the one builtin every bundler shims.
73
+ - **Third-party reach** — the "Resolves with zero deps installed?" column. Every entry's expected third-party set is asserted exactly, so every **yes** row above is a tested claim rather than a documented intention, and adding a dependency to any entry is a deliberate, reviewed edit. The `.` row asserts `[]`, which is the load-bearing one: a dependency arriving on the barrel — through any module it exports, at any depth — reddens that assertion, and the fix is a new subpath rather than a longer expected list.
74
+ - **Builtin reach** — five entries are held to a stricter contract still: `./zod`, `./yaml`, `./text` reach **no Node builtin at all**, and `./path`, `./glob` reach **`node:path` and nothing else** — the one builtin every bundler shims.
66
75
 
67
76
  That is an enforced invariant, not a browser-support commitment: there are no browser export conditions and no browser test lane. The guard exists so the property can't regress silently — it fails loudly if it cannot resolve a module, so it can't pass vacuously; `test/fixtures/dangling-import/` exercises that failure so the guarantee is demonstrated, not just claimed. If you add a new entry, add it to that test or nothing protects it.
68
77
 
@@ -119,13 +128,28 @@ FUNCTION, LAZY, PROMISE, and more...
119
128
  These always return forward slashes on every platform, so they are safe for comparisons, `Map` keys, globs, and display.
120
129
 
121
130
  - `safePath.join()` / `.resolve()` / `.relative()` - forward-slash equivalents of the `node:path` functions
122
- - `toForwardSlash()` - explicit converter for any path string
131
+ - `toForwardSlash()` - converter for a NATIVE path (fs, `path.*`, git output): converts only where the host separator is a backslash (win32); on POSIX a backslash is a filename character and is kept
132
+ - `toForwardSlashAnyPlatform()` - converter for AUTHOR-WRITTEN text (hrefs, globs, config values, CLI arguments, archive entry names): converts every backslash on every host. Never for a path read from disk — on POSIX it would turn the one file `docs/x\y.md` into a phantom `docs/x/y.md`
133
+ - `toNfc()` - Unicode-NFC normalizer for filename **comparison keys** (see the warning below)
123
134
  - `toAbsolutePath()` - resolve a path relative to a base directory
124
135
  - `getRelativePath()` - relative path between two absolute paths
125
136
  - `isAbsolutePath()` / `isAbsoluteAnyPlatform()` - absolute-path predicates
126
137
  - `hasParentTraversalSegment()` - detect `..` segments before using a caller-supplied path
138
+ - `relativeEscapesRoot()` - classify a root-relative path (`safePath.relative(root, p)`) as escaping the root: `..`, `../x`, or an absolute answer (Windows cross-drive). Lexical, for identities and reports; a sink asks `isUnderRoot()` instead
139
+ - `isSingleFsSegment()` - true when a caller-supplied NAME can only ever be one entry directly under whatever it is joined to (no separators, not `.`/`..`, no NUL, no drive spelling). `..cache` passes; `../victim` does not
127
140
  - `issueLocation()` - format a `file:line`-style location relative to a project root
128
141
 
142
+ ⚠️ **`toNfc()` produces a comparison key, never a path to open.** The same visible filename has two
143
+ Unicode encodings — precomposed NFC (`é` = `U+00E9`) and decomposed NFD (`e` + `U+0301`). They are
144
+ different strings, so `===`, `toLowerCase()`, `Map.get()` and `Set.has()` all call them different,
145
+ and `readdir` returns whichever form is on disk (commonly decomposed on macOS) while a markdown link
146
+ typed in an editor carries the composed one. Normalize wherever two such strings are **compared** —
147
+ a `Map` key derived from enumeration and queried from link text, a basename checked against a
148
+ directory listing. Do **not** normalize a path on its way to `fs.*`: macOS would not notice (its
149
+ lookup is normalization-insensitive), but on Linux the two forms are different byte sequences naming
150
+ different files, so opening the normalized form of a decomposed filename fails outright. That is why
151
+ this is a separate helper rather than something `safePath.resolve()` does.
152
+
129
153
  ### Filesystem — `@vibe-agent-toolkit/utils/fs`
130
154
 
131
155
  These return **OS-native** separators, because they resolve real filesystem identity via `realpathSync.native()` (which is what resolves Windows 8.3 short names). Wrap with `toForwardSlash()` if you need forward slashes.
@@ -137,8 +161,24 @@ These return **OS-native** separators, because they resolve real filesystem iden
137
161
  - `mkdirSyncReal()` - create a directory and return its real path
138
162
  - `resolveFromImportMeta()` - resolve paths relative to an `import.meta.url`
139
163
  - `dynamicImportPath()` - `import()` an absolute path (works on Windows, which rejects bare absolute paths)
140
- - `copyDirectory()`
141
- - `verifyCaseSensitiveFilename(filePath, fsCache)` - case-exact existence check; takes a per-run `FsLookupCache`
164
+ - `copyDirectory()` - recursive copy that follows a symlinked directory into the tree it points at — contained: a link whose target is outside the source throws `CopyLinkEscapesSourceError`, and a link back into the tree throws `DirectoryWalkRevisitedError`
165
+ - `isUnderRoot(root, candidate)` - the containment verdict a delete/copy/uninstall sink asks, answered by the filesystem: `'inside'` (a strict descendant, realpath-judged), `'outside'` (including the root itself and any symlink that leaves it), or `'absent'` (nothing there yet, and creating it would land inside). Both sides are canonicalized from their deepest existing ancestor, so a root reached through a symlink still contains its members; a refused path throws rather than reading as absent
166
+ - `direntKind(entry)` - what a `Dirent` itself is: `'file' | 'directory' | 'symlink' | 'other'` — the link question asked first, for walks that must not follow (a delete, a size count)
167
+ - `direntKindFollowing(dir, entry)` / `direntKindFollowingSync()` - what a `Dirent` resolves to, one `stat` for a link: `'file' | 'directory' | 'dangling' | 'other'` — for walks over a trusted tree where a link is how the entry got there. Both exist because `isFile()` and `isDirectory()` are BOTH false for a symlink, so a walk that tests only those drops every link silently (`local/dirent-type-needs-symlink-check` refuses that shape)
168
+ - `FollowedWalk` - the cycle guard every following walk holds: `enter(dir)` on the root and each directory recursed into; a directory reached again under a second spelling (a link back into the tree) throws `DirectoryWalkRevisitedError` instead of recursing until the path length runs out
169
+ - `fillPathSpellings(requests, fsCache)` - pass 1 of the case-exact existence check: walk each
170
+ `{ referrer, target }` from the deepest directory the two share, listing every directory on the way
171
+ down exactly once. The only I/O in the pair
172
+ - `pathSpellingFrom(table, referrer, target)` - pass 2: pure judgement against the table pass 1
173
+ returned, reporting the path as disk spells it when the asked-for spelling differs only by case or
174
+ by Unicode normalization. Fill **once** over the whole set and then judge each reference — a fill
175
+ per reference reinstates the serialized `readdir` this shape removes.
176
+ ⚠️ It judges **every component**, not just the basename: judging only the last one hands each
177
+ directory component back to the host filesystem's own folding, so a link that resolves on
178
+ macOS/APFS 404s on a case-sensitive filesystem — and a basename-only correction still leaves the
179
+ author with a broken path
180
+ - `DirectorySpellingIndex` / `spellingWalkRoot(referrer, target)` - the index the pair walks over, and
181
+ the root it walks from, for a caller that cannot enumerate its targets up front
142
182
  - `FsLookupCache` - per-run memo for `realpath`/`readdir`, sharing in-flight promises. Construct one per
143
183
  validation run and let it die with the run — never a module-level singleton, or a long-lived process
144
184
  answers from a stale directory listing.
@@ -166,7 +206,13 @@ These return **OS-native** separators, because they resolve real filesystem iden
166
206
  ### Test helpers — `@vibe-agent-toolkit/utils/testing`
167
207
 
168
208
  - `setupAsyncTempDirSuite()` / `setupSyncTempDirSuite()` - per-suite temp directories with cleanup
209
+ - `createTempDir()` / `createTempDirAsync()` / `removeTempDir()` / `tempDirTracker()` - the per-call temp-directory primitives; `removeTempDir` refuses, by name, anything not inside the host tmpdir as the filesystem sees it
210
+ - `CANNOT_DENY_READS` / `PERMISSIONS_ENFORCED` - whether a `chmod 000` denies anything on this host (Windows and uid 0 read everything); route through `skip()` so the skip is visible
211
+ - `NODE_EXECUTABLE` / `gitExecutable()` / `resolveExecutable(name)` / `executableCandidates(name, platform, pathext)` - absolute paths for the binaries a fixture spawns: the running node, and `git` found by walking `PATH` once for the first regular executable file (`PATHEXT` names on Windows) and cached. A fixture spawns these instead of a bare `'git'` or `'node'`, so no writable directory on `PATH` decides what runs.
212
+ - `buildHostileTree(base)` / `hostileTreePerTest(prefix)` / `HOSTILE_NAMES` - the one hostile fixture every sink is tested against: a root with a member, a `..`-named member, a symlink pointing out, one pointing in, a dangling one, an alias link to the root, an unreadable directory, a 200-character name, plus a sibling `victim/secret.txt` outside the root; and the table of names (`../victim`, `..`, `.`, `''`, `/victim`, `C:\victim`, a NUL byte, …) a sink must refuse. Fields the host cannot build are `null`
169
213
  - `getTestOutputDir()` / `getTestOutputBase()` - isolated test output paths
214
+ - `symlinkCapability()` - probes once (memoized per process) whether this host can create symlinks (Windows needs Developer Mode or `SeCreateSymbolicLinkPrivilege`), returning a `SymlinkCapability` token or `null`
215
+ - `createSymlink()` / `createSymlinkAsync()` - the sanctioned way to create a symlink in test code; both require a `SymlinkCapability` from `symlinkCapability()`, so a test cannot reach the raw syscall without first proving the host supports it (or explicitly skipping via vitest's `skip()`)
170
216
 
171
217
  ### Project roots — `@vibe-agent-toolkit/utils` (barrel only)
172
218
 
@@ -178,12 +224,28 @@ These are CLI-boundary functions: inner libraries should take a root as a parame
178
224
 
179
225
  **These four are VAT-shaped — read this before reaching for them.** `findProjectRoot()` looks for `vibe-agent-toolkit.config.yaml` and then `.git/`; if your repo's notion of "root" is a `pnpm-workspace.yaml`, a `turbo.json`, or a lockfile, that ladder is not your ladder — and for a *published* package, keying anything on `.git/` is a bug, since it will not be there at install time. `findNodeWorkspaceRoot()` is narrower still: it needs a `package.json` carrying a `"workspaces"` key, which pnpm and Bun workspaces do not have. `findConfigFile()` hardcodes VAT's config filename. If you want a git root, take `gitFindRoot()` from [`./git`](#git--vibe-agent-toolkitutilsgit); if you want your own marker, a six-line walk-up is more honest than a helper whose ladder you have to work around.
180
226
 
181
- They are nonetheless on their own [`./project`](#import-narrowly) entry rather than the barrel alone. The entry was briefly withdrawn on the grounds that the functions fit few repos — which is true, and is what the paragraph above says — but that answered the wrong question. What decides whether an *entry* exists is how heavy the only remaining door is: barrel-only, these four cost `handlebars`, `yaml`, `picomatch`, `ignore` and `which` to reach, while their own code imports nothing but `node:fs` and `node:path`. Publishing the entry is not a claim that the ladder fits you — only that finding out shouldn't cost five dependencies.
227
+ They are nonetheless on their own [`./project`](#import-narrowly) entry rather than the barrel alone. The entry was briefly withdrawn on the grounds that the functions fit few repos — which is true, and is what the paragraph above says — but that answered the wrong question. What decides whether an *entry* exists is how heavy the only remaining door is, and barrel-only these four once cost five third-party packages to reach while their own code imports nothing but `node:fs` and `node:path`. Publishing the entry is not a claim that the ladder fits you — only that finding out shouldn't cost a dependency graph. The barrel is dependency-free now, which is that same rule applied everywhere rather than a reason to fold `./project` back in.
228
+
229
+ ### Errors — `@vibe-agent-toolkit/utils` (barrel only)
230
+
231
+ - `VatError` - the base of every error VAT throws on purpose: `new VatError(code, message, { cause })`. `code` is a stable `SCREAMING_SNAKE` identity a catch block dispatches on; `name` is taken from the subclass.
232
+ - `isVatError(error, code?)` - the dispatch predicate. Reads a `Symbol.for('vat.error')` brand plus `code`, never the prototype chain, so a `dist` copy of a class still matches a `src` instance. A foreign error carrying a `code` (every `node:fs` errno) answers `false`.
233
+ - `prefixMessageOnce(error, prefix)` - prefix a message in place, once per error object — for a seam a retried item passes through more than once, without re-reading the message to find out.
234
+ - `PathEscapesRootError` (on `./path` too) - what `safePath.joinUnderRoot` throws; recognise it with `isVatError(error, PathEscapesRootError.code)`.
235
+
236
+ Never dispatch on `error.message` — the repo's ESLint config refuses `.message.includes(…)` and friends under `packages/*/src`. Prose is for humans and changes when it is improved; a code changes only when the meaning does.
182
237
 
183
238
  ### Directory crawling — `@vibe-agent-toolkit/utils/crawl`
184
239
 
185
240
  - `crawlDirectory()` / `crawlDirectorySync()` - gitignore-aware directory walks
186
241
  - `NEVER_CRAWL_GLOBS` / `BUILD_OUTPUT_GLOBS` - the standard exclusion sets
242
+ - `UnreadablePolicy` - the REQUIRED `unreadable` option on every crawl (and on `gitLsFiles` /
243
+ `gitLsOthers` under `./git`): `{ refuse: { root, remedy } }` throws `DirectoryListingRefusedError`
244
+ with an adopter-facing, root-relative sentence when a directory cannot be listed;
245
+ `{ degrade: (refusal) => … }` keeps walking and hands the gap to you to report. There is no
246
+ default — a shorter list nothing can tell from a complete one is the failure this option exists
247
+ to prevent, so the caller states which answer is honest for its lane, and an omitted policy is
248
+ refused up front by name.
187
249
 
188
250
  Glob *pattern inspection* is a separate entry, `./glob`, and stays that way: `./glob` is dependency-free and reaches only `node:path`, whereas crawling reaches the filesystem, `git`, and `picomatch`.
189
251
 
@@ -74,8 +74,9 @@ function formatActionableError(specifier, baseDir, cause) {
74
74
  // but a build step in the target package didn't run (or produced different
75
75
  // output). Name the missing file explicitly and point at the publisher.
76
76
  if (code === 'MODULE_NOT_FOUND' && missingPath && missingPath !== specifier && isAbsolutePath(missingPath)) {
77
- const fileExists = safeExistsSync(missingPath);
78
- if (!fileExists) {
77
+ // `existsSync` answers false for every failure and never throws, so no
78
+ // guard around it: the question here is only whether the file is there.
79
+ if (!existsSync(missingPath)) {
79
80
  return (`Failed to resolve asset reference '${specifier}': ` +
80
81
  `the package's "exports" map points to '${missingPath}', but that file does not exist on disk.\n` +
81
82
  `Hint: the target package was found, but a build step did not produce this file. ` +
@@ -108,15 +109,6 @@ function extractMissingModulePath(cause) {
108
109
  const match = CANNOT_FIND_MODULE_RE.exec(cause.message);
109
110
  return match?.[1];
110
111
  }
111
- function safeExistsSync(filePath) {
112
- try {
113
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- filePath is a Node-resolved exports target, used only to refine the error message
114
- return existsSync(filePath);
115
- }
116
- catch {
117
- return false;
118
- }
119
- }
120
112
  function isBareSpecifier(value) {
121
113
  return BARE_SPECIFIER_RE.test(value);
122
114
  }
@@ -1 +1 @@
1
- {"version":3,"file":"asset-reference.js","sourceRoot":"","sources":["../src/asset-reference.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"asset-reference.js","sourceRoot":"","sources":["../src/asset-reference.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Host-independent string ordering, for sorts whose output is hashed or compared
3
+ * across machines.
4
+ *
5
+ * ## Why this is not `localeCompare`
6
+ *
7
+ * `sonarjs/no-alphabetical-sort` fires on a bare `strings.sort()` and its suggested fix is a
8
+ * `localeCompare` comparator. **That fix is wrong for anything whose order feeds a digest, a
9
+ * content key, a serialized document or a golden file.** Collation is locale-dependent, so two
10
+ * machines running the same code over the same corpus can order the same strings differently —
11
+ * destroying exactly the cross-run comparability those artifacts exist to provide. The rule only
12
+ * requires *a* comparator; this is the one to give it.
13
+ *
14
+ * Code-unit order is total, stable and machine-independent. It is not alphabetical in any human
15
+ * sense (uppercase sorts before lowercase, `10` before `2`), which is fine — callers using it are
16
+ * ordering bytes for a machine, not a list for a reader. Sort numbers numerically instead.
17
+ *
18
+ * This lives in `utils` because it had independently grown three identical private copies (in
19
+ * `resources`' projection digest and blob population, and in `claude-marketplace`' plugin extent)
20
+ * and a fourth was nearly written. Three copies of a "never use `localeCompare`" rule is three
21
+ * chances for someone to helpfully replace one of them.
22
+ *
23
+ * @param left - First string
24
+ * @param right - Second string
25
+ * @returns Negative, zero or positive per the `Array.prototype.sort` contract
26
+ */
27
+ export declare function compareCodeUnits(left: string, right: string): number;
28
+ //# sourceMappingURL=compare-code-units.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare-code-units.d.ts","sourceRoot":"","sources":["../src/compare-code-units.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKpE"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Host-independent string ordering, for sorts whose output is hashed or compared
3
+ * across machines.
4
+ *
5
+ * ## Why this is not `localeCompare`
6
+ *
7
+ * `sonarjs/no-alphabetical-sort` fires on a bare `strings.sort()` and its suggested fix is a
8
+ * `localeCompare` comparator. **That fix is wrong for anything whose order feeds a digest, a
9
+ * content key, a serialized document or a golden file.** Collation is locale-dependent, so two
10
+ * machines running the same code over the same corpus can order the same strings differently —
11
+ * destroying exactly the cross-run comparability those artifacts exist to provide. The rule only
12
+ * requires *a* comparator; this is the one to give it.
13
+ *
14
+ * Code-unit order is total, stable and machine-independent. It is not alphabetical in any human
15
+ * sense (uppercase sorts before lowercase, `10` before `2`), which is fine — callers using it are
16
+ * ordering bytes for a machine, not a list for a reader. Sort numbers numerically instead.
17
+ *
18
+ * This lives in `utils` because it had independently grown three identical private copies (in
19
+ * `resources`' projection digest and blob population, and in `claude-marketplace`' plugin extent)
20
+ * and a fourth was nearly written. Three copies of a "never use `localeCompare`" rule is three
21
+ * chances for someone to helpfully replace one of them.
22
+ *
23
+ * @param left - First string
24
+ * @param right - Second string
25
+ * @returns Negative, zero or positive per the `Array.prototype.sort` contract
26
+ */
27
+ export function compareCodeUnits(left, right) {
28
+ if (left === right) {
29
+ return 0;
30
+ }
31
+ return left < right ? -1 : 1;
32
+ }
33
+ //# sourceMappingURL=compare-code-units.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare-code-units.js","sourceRoot":"","sources":["../src/compare-code-units.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa;IAC1D,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;QACnB,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/B,CAAC"}