@vibe-agent-toolkit/utils 0.2.0-rc.1 → 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 (270) hide show
  1. package/README.md +69 -23
  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 +133 -29
  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 +422 -191
  44. package/dist/fs-utils.d.ts.map +1 -1
  45. package/dist/fs-utils.js +534 -204
  46. package/dist/fs-utils.js.map +1 -1
  47. package/dist/fs.d.ts +5 -2
  48. package/dist/fs.d.ts.map +1 -1
  49. package/dist/fs.js +9 -7
  50. package/dist/fs.js.map +1 -1
  51. package/dist/git-run.d.ts +167 -0
  52. package/dist/git-run.d.ts.map +1 -0
  53. package/dist/git-run.js +154 -0
  54. package/dist/git-run.js.map +1 -0
  55. package/dist/git-snapshot.d.ts +199 -0
  56. package/dist/git-snapshot.d.ts.map +1 -0
  57. package/dist/git-snapshot.js +250 -0
  58. package/dist/git-snapshot.js.map +1 -0
  59. package/dist/git-tracker.d.ts +166 -3
  60. package/dist/git-tracker.d.ts.map +1 -1
  61. package/dist/git-tracker.js +250 -22
  62. package/dist/git-tracker.js.map +1 -1
  63. package/dist/git-utils.d.ts +84 -1
  64. package/dist/git-utils.d.ts.map +1 -1
  65. package/dist/git-utils.js +208 -82
  66. package/dist/git-utils.js.map +1 -1
  67. package/dist/git.d.ts +13 -4
  68. package/dist/git.d.ts.map +1 -1
  69. package/dist/git.js +12 -4
  70. package/dist/git.js.map +1 -1
  71. package/dist/gitignore-checker.d.ts.map +1 -1
  72. package/dist/gitignore-checker.js +15 -6
  73. package/dist/gitignore-checker.js.map +1 -1
  74. package/dist/glob/glob-pattern.js +5 -5
  75. package/dist/glob/glob-pattern.js.map +1 -1
  76. package/dist/index.d.ts +32 -20
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +95 -50
  79. package/dist/index.js.map +1 -1
  80. package/dist/listing-refusal.d.ts +112 -0
  81. package/dist/listing-refusal.d.ts.map +1 -0
  82. package/dist/listing-refusal.js +105 -0
  83. package/dist/listing-refusal.js.map +1 -0
  84. package/dist/path-containment.d.ts +64 -0
  85. package/dist/path-containment.d.ts.map +1 -0
  86. package/dist/path-containment.js +111 -0
  87. package/dist/path-containment.js.map +1 -0
  88. package/dist/path-core.d.ts +113 -18
  89. package/dist/path-core.d.ts.map +1 -1
  90. package/dist/path-core.js +139 -23
  91. package/dist/path-core.js.map +1 -1
  92. package/dist/path-utils.d.ts.map +1 -1
  93. package/dist/path-utils.js +42 -47
  94. package/dist/path-utils.js.map +1 -1
  95. package/dist/path.d.ts +1 -1
  96. package/dist/path.d.ts.map +1 -1
  97. package/dist/path.js +1 -1
  98. package/dist/path.js.map +1 -1
  99. package/dist/process.d.ts +7 -0
  100. package/dist/process.d.ts.map +1 -1
  101. package/dist/process.js +7 -0
  102. package/dist/process.js.map +1 -1
  103. package/dist/project-utils.d.ts.map +1 -1
  104. package/dist/project-utils.js +15 -9
  105. package/dist/project-utils.js.map +1 -1
  106. package/dist/project.d.ts +7 -5
  107. package/dist/project.d.ts.map +1 -1
  108. package/dist/project.js +7 -5
  109. package/dist/project.js.map +1 -1
  110. package/dist/safe-exec.d.ts +40 -11
  111. package/dist/safe-exec.d.ts.map +1 -1
  112. package/dist/safe-exec.js +111 -25
  113. package/dist/safe-exec.js.map +1 -1
  114. package/dist/skill-targets.d.ts +2 -2
  115. package/dist/skill-targets.js +2 -2
  116. package/dist/skill-test/auth-resolver.d.ts +10 -2
  117. package/dist/skill-test/auth-resolver.d.ts.map +1 -1
  118. package/dist/skill-test/auth-resolver.js +26 -11
  119. package/dist/skill-test/auth-resolver.js.map +1 -1
  120. package/dist/skill-test/index.d.ts +1 -1
  121. package/dist/skill-test/index.d.ts.map +1 -1
  122. package/dist/skill-test/index.js +1 -1
  123. package/dist/skill-test/index.js.map +1 -1
  124. package/dist/skill-test/spawn-claude.d.ts +30 -1
  125. package/dist/skill-test/spawn-claude.d.ts.map +1 -1
  126. package/dist/skill-test/spawn-claude.js +71 -5
  127. package/dist/skill-test/spawn-claude.js.map +1 -1
  128. package/dist/skill-test/transcript.d.ts +41 -2
  129. package/dist/skill-test/transcript.d.ts.map +1 -1
  130. package/dist/skill-test/transcript.js +55 -10
  131. package/dist/skill-test/transcript.js.map +1 -1
  132. package/dist/spawn-hardened.js +0 -1
  133. package/dist/spawn-hardened.js.map +1 -1
  134. package/dist/stdio-blocking.js +13 -14
  135. package/dist/stdio-blocking.js.map +1 -1
  136. package/dist/test-helpers.d.ts +135 -100
  137. package/dist/test-helpers.d.ts.map +1 -1
  138. package/dist/test-helpers.js +224 -146
  139. package/dist/test-helpers.js.map +1 -1
  140. package/dist/testing/executables.d.ts +32 -0
  141. package/dist/testing/executables.d.ts.map +1 -0
  142. package/dist/testing/executables.js +70 -0
  143. package/dist/testing/executables.js.map +1 -0
  144. package/dist/testing/hostile-tree.d.ts +79 -0
  145. package/dist/testing/hostile-tree.d.ts.map +1 -0
  146. package/dist/testing/hostile-tree.js +164 -0
  147. package/dist/testing/hostile-tree.js.map +1 -0
  148. package/dist/testing/platform-gates.d.ts +31 -0
  149. package/dist/testing/platform-gates.d.ts.map +1 -0
  150. package/dist/testing/platform-gates.js +31 -0
  151. package/dist/testing/platform-gates.js.map +1 -0
  152. package/dist/testing/temp-dir.d.ts +255 -0
  153. package/dist/testing/temp-dir.d.ts.map +1 -0
  154. package/dist/testing/temp-dir.js +363 -0
  155. package/dist/testing/temp-dir.js.map +1 -0
  156. package/dist/testing.d.ts +94 -3
  157. package/dist/testing.d.ts.map +1 -1
  158. package/dist/testing.js +119 -3
  159. package/dist/testing.js.map +1 -1
  160. package/dist/text-content.d.ts +197 -0
  161. package/dist/text-content.d.ts.map +1 -0
  162. package/dist/text-content.js +354 -0
  163. package/dist/text-content.js.map +1 -0
  164. package/dist/text-file.d.ts +45 -0
  165. package/dist/text-file.d.ts.map +1 -0
  166. package/dist/text-file.js +51 -0
  167. package/dist/text-file.js.map +1 -0
  168. package/dist/text.d.ts +16 -0
  169. package/dist/text.d.ts.map +1 -0
  170. package/dist/text.js +16 -0
  171. package/dist/text.js.map +1 -0
  172. package/dist/timing-dump.d.ts +136 -0
  173. package/dist/timing-dump.d.ts.map +1 -0
  174. package/dist/timing-dump.js +229 -0
  175. package/dist/timing-dump.js.map +1 -0
  176. package/dist/windows-shell.d.ts +0 -1
  177. package/dist/windows-shell.d.ts.map +1 -1
  178. package/dist/windows-shell.js +0 -1
  179. package/dist/windows-shell.js.map +1 -1
  180. package/dist/zod-introspection.js +2 -2
  181. package/dist/zod-introspection.js.map +1 -1
  182. package/eslint/README.md +223 -49
  183. package/eslint/index.cjs +111 -97
  184. package/eslint/index.d.cts +16 -6
  185. package/eslint/rules/callee-name.cjs +23 -0
  186. package/eslint/rules/commands-import-boundary.cjs +148 -0
  187. package/eslint/rules/dead-import.cjs +1 -1
  188. package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
  189. package/eslint/rules/eslint-rule-factory.cjs +5 -2
  190. package/eslint/rules/exempt-path-matcher.cjs +2 -1
  191. package/eslint/rules/explicit-zod-strictness.cjs +120 -0
  192. package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
  193. package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
  194. package/eslint/rules/no-bare-symlink-in-tests.cjs +213 -0
  195. package/eslint/rules/no-blind-catch.cjs +171 -0
  196. package/eslint/rules/no-child-process-execSync.cjs +8 -0
  197. package/eslint/rules/no-decaying-referent.cjs +217 -0
  198. package/eslint/rules/no-dotdot-containment.cjs +160 -0
  199. package/eslint/rules/no-file-url-string-concat.cjs +4 -1
  200. package/eslint/rules/no-fragile-entrypoint-guard.cjs +228 -0
  201. package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
  202. package/eslint/rules/no-fs-promises-cp.cjs +7 -0
  203. package/eslint/rules/no-fs-realpathSync.cjs +8 -0
  204. package/eslint/rules/no-hardcoded-path-split.cjs +29 -18
  205. package/eslint/rules/no-io-in-unit-tier.cjs +129 -0
  206. package/eslint/rules/no-literal-process-exit.cjs +129 -0
  207. package/eslint/rules/no-manual-path-normalize.cjs +136 -107
  208. package/eslint/rules/no-os-tmpdir.cjs +8 -0
  209. package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
  210. package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
  211. package/eslint/rules/no-path-startswith.cjs +7 -2
  212. package/eslint/rules/no-process-exit-in-phase.cjs +127 -0
  213. package/eslint/rules/no-raw-node-path.cjs +563 -0
  214. package/eslint/rules/no-raw-text-decode.cjs +305 -0
  215. package/eslint/rules/no-registry-count-pin.cjs +255 -0
  216. package/eslint/rules/no-self-package-import.cjs +168 -0
  217. package/eslint/rules/no-test-scoped-functions.cjs +10 -3
  218. package/eslint/rules/no-unix-shell-commands.cjs +6 -2
  219. package/eslint/rules/no-unsafe-root-join.cjs +23 -2
  220. package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
  221. package/eslint/rules/no-version-literal.cjs +151 -0
  222. package/eslint/rules/prefer-startswith-over-regex.cjs +9 -0
  223. package/eslint/rules/require-justified-skip.cjs +8 -2
  224. package/eslint/rules/safe-import.cjs +1 -1
  225. package/package.json +16 -16
  226. package/dist/link-auth/build-headers.d.ts +0 -34
  227. package/dist/link-auth/build-headers.d.ts.map +0 -1
  228. package/dist/link-auth/build-headers.js +0 -58
  229. package/dist/link-auth/build-headers.js.map +0 -1
  230. package/dist/link-auth/expand-macro.d.ts +0 -38
  231. package/dist/link-auth/expand-macro.d.ts.map +0 -1
  232. package/dist/link-auth/expand-macro.js +0 -133
  233. package/dist/link-auth/expand-macro.js.map +0 -1
  234. package/dist/link-auth/macros.yaml +0 -50
  235. package/dist/link-auth/resolve-token.d.ts +0 -83
  236. package/dist/link-auth/resolve-token.d.ts.map +0 -1
  237. package/dist/link-auth/resolve-token.js +0 -101
  238. package/dist/link-auth/resolve-token.js.map +0 -1
  239. package/dist/link-auth/resolve.d.ts +0 -102
  240. package/dist/link-auth/resolve.d.ts.map +0 -1
  241. package/dist/link-auth/resolve.js +0 -66
  242. package/dist/link-auth/resolve.js.map +0 -1
  243. package/dist/link-auth/rewrite.d.ts +0 -52
  244. package/dist/link-auth/rewrite.d.ts.map +0 -1
  245. package/dist/link-auth/rewrite.js +0 -102
  246. package/dist/link-auth/rewrite.js.map +0 -1
  247. package/dist/link-auth/select-provider.d.ts +0 -30
  248. package/dist/link-auth/select-provider.d.ts.map +0 -1
  249. package/dist/link-auth/select-provider.js +0 -55
  250. package/dist/link-auth/select-provider.js.map +0 -1
  251. package/dist/link-auth/template.d.ts +0 -40
  252. package/dist/link-auth/template.d.ts.map +0 -1
  253. package/dist/link-auth/template.js +0 -89
  254. package/dist/link-auth/template.js.map +0 -1
  255. package/dist/link-auth/transforms.d.ts +0 -46
  256. package/dist/link-auth/transforms.d.ts.map +0 -1
  257. package/dist/link-auth/transforms.js +0 -52
  258. package/dist/link-auth/transforms.js.map +0 -1
  259. package/dist/template-entry.d.ts +0 -10
  260. package/dist/template-entry.d.ts.map +0 -1
  261. package/dist/template-entry.js +0 -10
  262. package/dist/template-entry.js.map +0 -1
  263. package/dist/template.d.ts +0 -7
  264. package/dist/template.d.ts.map +0 -1
  265. package/dist/template.js +0 -18
  266. package/dist/template.js.map +0 -1
  267. package/eslint/rules/no-path-join.cjs +0 -14
  268. package/eslint/rules/no-path-relative.cjs +0 -14
  269. package/eslint/rules/no-path-resolve.cjs +0 -14
  270. package/eslint/rules/path-function-rule-factory.cjs +0 -496
package/dist/index.js CHANGED
@@ -3,88 +3,133 @@
3
3
  * Core shared utilities with no dependencies on other packages
4
4
  *
5
5
  * Utilities are added as needed by other packages, not speculatively.
6
+ *
7
+ * **A module that brings a third-party dependency belongs on a subpath, not
8
+ * here.** This entry has ~245 in-repo importers plus published adopters, and
9
+ * every one of them pays for the whole graph reachable from it — a bundler that
10
+ * cannot tree-shake a dependency (a CJS package, or one reached through
11
+ * `new URL(..., import.meta.url)`) inlines it into every consumer that touched
12
+ * a path helper. The domains that carry weight each have their own entry:
13
+ * `./crawl`, `./git`, `./process`, `./skill-test`, `./template`, `./yaml`.
14
+ * `./skill-test` is there for a *transitive* reason rather than a direct one —
15
+ * it declares no dependency itself, but spawning a headless agent goes through
16
+ * `./process`, which reaches `which` and (because `safeExecSync` refuses `git`
17
+ * and delegates) `@vibe-validate/git`. Reachability is the criterion, not the
18
+ * import a module happens to write.
19
+ *
20
+ * The rule is enforced, not merely stated: `test/subpath-purity.test.ts` asserts
21
+ * this entry's reachable third-party set by equality, so a dependency arriving
22
+ * here — directly or transitively, through any module below — turns that row red
23
+ * and has to be an argued edit rather than an unnoticed one.
6
24
  */
7
- // Safe command execution (cross-platform, no shell injection)
8
- export * from './safe-exec.js';
9
- // Windows shell-invocation helpers (.cmd/.bat/.ps1 handling), shared by every spawn wrapper
10
- export * from './windows-shell.js';
11
- // Hardened async spawn (streaming stdio + correct Windows .cmd/.bat launching)
12
- export * from './spawn-hardened.js';
25
+ // The base of every error VAT throws on purpose; `code` is what a catch
26
+ // block dispatches on, never `message`.
27
+ export * from './errors/vat-error.js';
13
28
  // Cross-platform path utilities
14
29
  export * from './path-utils.js';
15
- // Blocking stdio for published bins (process.exit must not truncate output)
16
- export * from './stdio-blocking.js';
30
+ // The filesystem-backed containment verdict every delete/copy/uninstall sink asks.
31
+ export * from './path-containment.js';
32
+ // The Dirent classifiers a walk uses instead of a bare isFile()/isDirectory().
33
+ export * from './dirent-kind.js';
34
+ // THE content-decoding seam: bytes to text, in one place. `decodeTextContent` is
35
+ // pure and also reachable from the dependency-free `./text` entry; the two
36
+ // file-reading wrappers need `node:fs` and are also on `./fs`. Other routes from
37
+ // bytes to text — `buf.toString('utf-8')`, `new TextDecoder()`,
38
+ // `readFile(p, 'utf-8')` — are a lint error under `local/no-raw-text-decode`
39
+ // WHERE IT IS REGISTERED (this package's `src` and `resources`' `src`); the rest
40
+ // of the repo is a migration ledger in `eslint.config.js`, not a covered claim.
41
+ export * from './text-content.js';
42
+ export * from './text-file.js';
17
43
  // Asset reference resolution (paths + npm bare specifiers)
18
44
  export * from './asset-reference.js';
19
45
  // Filesystem utilities.
20
46
  //
21
- // Named rather than `export *` on purpose: `classifyFilenameCase` (the pure
22
- // judge over a hand-held row) and `siblingNamesFrom` (the table lookup that
23
- // throws on a miss) are internal members of the fill+judge pairs below, and a
24
- // star re-export would publish them on this barrel the moment they were written.
25
- // (The `./fs` subpath was already an explicit list and was never at risk.) The
26
- // public surface is a decision, not a side effect of module layout.
47
+ // Named rather than `export *` on purpose: `fs-utils.ts` holds internal members
48
+ // of the fill+judge pairs below — the per-directory index build, the table-key
49
+ // derivation — and a star re-export would publish each of them on this barrel
50
+ // the moment it was written. (The `./fs` subpath was already an explicit list
51
+ // and was never at risk.) The public surface is a decision, not a side effect of
52
+ // module layout.
27
53
  //
28
54
  // TWO materialized columns are published, both shaped fill-first-then-judge:
29
55
  //
30
- // - sibling names — `fillSiblingNames` (every listing, once, up front) plus
31
- // `classifyFilenameCaseFrom` (pure judgement over the filled table). Its row
32
- // lookup `siblingNamesFrom` stays internal because a row there is not yet an
33
- // answer: it still needs a judge, and that judge is what we export.
56
+ // - path spellings — `fillPathSpellings` plus `pathSpellingFrom`. It judges a
57
+ // WHOLE path, which is what a caller validating references needs: judging
58
+ // only the basename hands every directory component back to the host
59
+ // filesystem's own folding, so a link that resolves on macOS 404s on a
60
+ // case-sensitive filesystem. The `DirectorySpellingIndex` underneath is
61
+ // published too, for the lazy caller that cannot enumerate its targets up
62
+ // front.
34
63
  // - realpaths — `fillRealpaths` (every canonicalization, once, up front) plus
35
- // `realpathFrom`. Here the row lookup IS the judge: the canonical path is the
36
- // answer, so there is nothing left to keep internal.
64
+ // `realpathFrom`.
65
+ //
66
+ // In both, the row lookup IS the judge: the row is the answer, so there is
67
+ // nothing left to keep internal.
37
68
  //
38
69
  // There is deliberately no one-call wrapper composing either pair: handed one, a
39
70
  // caller with many paths loops over it, which reinstates the per-path `await` the
40
71
  // pairs exist to remove.
41
72
  //
42
- // The `SiblingNames` row type is withheld for the same reason its judge is: it is
43
- // only ever that judge's parameter, so publishing it would advertise a shape no
44
- // consumer can hand anywhere. `SiblingNamesTable` and `RealpathTable` — what the
45
- // fills return and the judges consume — are the ones a caller can name, as are
46
- // `FilenameCaseVerdict`/`FilenameMatch`, which a consumer does not hand in but
47
- // does receive and branch on.
73
+ // `PathSpellingTable` and `RealpathTable` — what the fills return and the judges
74
+ // consume — are the types a caller can name, as are `PathSpelling`,
75
+ // `PathSpellingRequest`, `ComponentMatch` and `FilenameMatch`, which a consumer
76
+ // receives and branches on.
48
77
  //
49
78
  // The cost of that decision, stated so it is not a surprise: a new *type* added
50
79
  // to `fs-utils.ts` no longer reaches consumers automatically, and
51
80
  // `barrel-exports.test.ts` pins runtime names only, so nothing will fail. The
52
81
  // symptom is a type that cannot be imported, which surfaces the first time
53
82
  // someone tries — not a silent break in existing code.
54
- export { classifyFilenameCaseFrom, copyDirectory, fillRealpaths, fillSiblingNames, FsLookupCache, realpathFrom, } from './fs-utils.js';
55
- // Directory crawling with glob patterns
56
- export * from './file-crawler.js';
57
- // Git ignore checking
58
- export * from './gitignore-checker.js';
59
- // Git URL parsing (parse/detect git URLs, GitHub shorthand, SSH forms)
60
- export * from './git-url.js';
61
- // Git utilities (using git commands directly)
62
- export * from './git-utils.js';
83
+ // The lookups themselves, plus the memo every fill shares.
84
+ export { isFilesystemAccessError, isPathAbsentError } from './errors/errno.js';
85
+ export { copyDirectory, CopyLinkEscapesSourceError, FsLookupCache, transientRefusalClause, } from './fs-utils.js';
86
+ // The two fill+judge pairs, in the order the note above lists them, plus the
87
+ // lazy index a caller that cannot enumerate its targets up front reaches for.
88
+ export { DirectorySpellingIndex, fillPathSpellings, fillRealpaths, pathSpellingFrom, realpathFrom, spellingWalkRoot, } from './fs-utils.js';
63
89
  // Project root discovery (canonical: config → git → null).
64
90
  // CLI-boundary use only — see docs/concepts/roots-and-config.md.
65
91
  export * from './project-utils.js';
66
- // Git tracking cache (for efficient git-ignore checking)
67
- export * from './git-tracker.js';
68
- // Test helpers for isolated test output directories
69
- export * from './test-helpers.js';
92
+ // The runtime half of `test-helpers.ts`: symlink creation with its host
93
+ // capability probe, and the git-environment detachment every child process
94
+ // needs. Named rather than `export *` because the SAME module also holds the
95
+ // fs-refusal fakes (`refuseSyncFs` and siblings), which are test scaffolding
96
+ // and reach adopters on the `./testing` subpath only. A published runtime
97
+ // barrel that carried them — eight such names, once — was the audit's finding,
98
+ // and an `export *` here is how they got in.
99
+ export { createSymlink, createSymlinkAsync, detachGitEnv, errnoError, INHERITED_GIT_ENV, symlinkCapability, } from './test-helpers.js';
70
100
  // Zod type introspection (version-agnostic)
71
101
  export * from './zod-introspection.js';
72
- // Handlebars template rendering (cached, no HTML escaping)
73
- export * from './template.js';
74
102
  // Skill target resolution (cross-platform flat skill install paths)
75
103
  export * from './skill-targets.js';
76
- // linkAuth pure engine — public API only (issue #113).
77
- // Internal helpers (rewrite, build-headers, etc.) stay module-private.
78
- export { resolveAuthenticatedUrl, } from './link-auth/resolve.js';
79
- export { defaultRunCommand } from './link-auth/resolve-token.js';
80
- export { expandMacro, UnknownMacroError } from './link-auth/expand-macro.js';
81
- // Skill testing utilities (environment management for headless agent runs)
82
- export * from './skill-test/index.js';
83
104
  // Glob pattern helpers (isGlob, staticGlobBase, globMagicRemainder)
84
105
  export * from './glob/glob-pattern.js';
85
106
  // Filesystem hashing (sha256 of raw file bytes)
86
107
  export * from './fs/file-hash.js';
87
- // Byte-surgical YAML value updater (replace/insert without reflowing the doc)
88
- export * from './yaml/surgical-yaml.js';
89
108
  export { parseWholeNumberAtLeast } from './numeric-args.js';
109
+ // THE reading of an environment variable as a boolean. One implementation
110
+ // because three switches each had their own `!== '0'` comparison and none of
111
+ // them turned off for `=false`.
112
+ export { parseEnvBoolean } from './env-flag.js';
113
+ // Machine-independent string ordering for hashed/serialized output — never `localeCompare`.
114
+ export { compareCodeUnits } from './compare-code-units.js';
115
+ // The crawl-timing seam: which contributor, stratum and fixpoint pass owns the
116
+ // time it takes to FIND documents.
117
+ //
118
+ // It lives HERE, at the bottom of the graph, because the work it measures is
119
+ // spread across four packages above this one — `resources` builds the registry,
120
+ // `agent-skills` walks the link graph, `claude-marketplace` enumerates an
121
+ // inventory, and `utils` itself initializes the `GitTracker` both crawlers
122
+ // consume. Both arms must record through ONE recorder or the two are not
123
+ // comparable, and only `utils` is beneath all of them. See the module header.
124
+ //
125
+ // Named rather than `export *`: the accumulator internals (`addEntry`,
126
+ // `keyOf`, `recordInheritedPass`) are not surface, and `timing-dump.ts` is
127
+ // plumbing shared with `resources`' package-internal `parse-timing.ts` — a star
128
+ // re-export would publish both the moment either grew a symbol.
129
+ export { CRAWL_BLOB_POPULATE_ID, CRAWL_CLOSURE_CONTRIBUTE_ID, CRAWL_CLOSURE_RESOLVE_ID, CRAWL_PASS_INSIDE, CRAWL_REGISTRY_ADMIT_ID, CRAWL_REGISTRY_ENUMERATE_ID, CRAWL_REGISTRY_ID_PREFIX, CRAWL_REGISTRY_RESOLVE_LINKS_ID, CRAWL_SHARED_GIT_TRACKER_ID, CRAWL_STORE_READ_ID, CRAWL_STORE_WRITE_ID, CRAWL_STRATA, CRAWL_WALKER_GITIGNORE_ID, CRAWL_WALKER_ID, crawlTimingStart, recordContributorInvocation, recordCrawlPass, recordRegistryPass, recordSharedPass, withContributorStratum, withOuterBracket, } from './crawl-timing.js';
130
+ // The three `__*ForTest` seams of the same module are on `./testing`, not here.
131
+ // The on-disk plumbing both timing seams share. Exported because
132
+ // `resources`' `parse-timing.ts` is the other consumer and now sits a package
133
+ // away; nothing else should reach for it.
134
+ export { ensureTimingDirectory, normalizeTimingDirectory, readTimingProcess, writeTimingDump, } from './timing-dump.js';
90
135
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,8DAA8D;AAC9D,cAAc,gBAAgB,CAAC;AAE/B,4FAA4F;AAC5F,cAAc,oBAAoB,CAAC;AAEnC,+EAA+E;AAC/E,cAAc,qBAAqB,CAAC;AAEpC,gCAAgC;AAChC,cAAc,iBAAiB,CAAC;AAEhC,4EAA4E;AAC5E,cAAc,qBAAqB,CAAC;AAEpC,2DAA2D;AAC3D,cAAc,sBAAsB,CAAC;AAErC,wBAAwB;AACxB,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,8EAA8E;AAC9E,iFAAiF;AACjF,+EAA+E;AAC/E,oEAAoE;AACpE,EAAE;AACF,6EAA6E;AAC7E,EAAE;AACF,8EAA8E;AAC9E,iFAAiF;AACjF,iFAAiF;AACjF,wEAAwE;AACxE,gFAAgF;AAChF,kFAAkF;AAClF,yDAAyD;AACzD,EAAE;AACF,iFAAiF;AACjF,kFAAkF;AAClF,yBAAyB;AACzB,EAAE;AACF,kFAAkF;AAClF,gFAAgF;AAChF,iFAAiF;AACjF,+EAA+E;AAC/E,+EAA+E;AAC/E,8BAA8B;AAC9B,EAAE;AACF,gFAAgF;AAChF,kEAAkE;AAClE,8EAA8E;AAC9E,2EAA2E;AAC3E,uDAAuD;AACvD,OAAO,EACL,wBAAwB,EACxB,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,YAAY,GACb,MAAM,eAAe,CAAC;AAUvB,wCAAwC;AACxC,cAAc,mBAAmB,CAAC;AAElC,sBAAsB;AACtB,cAAc,wBAAwB,CAAC;AAEvC,uEAAuE;AACvE,cAAc,cAAc,CAAC;AAE7B,8CAA8C;AAC9C,cAAc,gBAAgB,CAAC;AAE/B,2DAA2D;AAC3D,iEAAiE;AACjE,cAAc,oBAAoB,CAAC;AAEnC,yDAAyD;AACzD,cAAc,kBAAkB,CAAC;AAEjC,oDAAoD;AACpD,cAAc,mBAAmB,CAAC;AAElC,4CAA4C;AAC5C,cAAc,wBAAwB,CAAC;AAEvC,2DAA2D;AAC3D,cAAc,eAAe,CAAC;AAE9B,oEAAoE;AACpE,cAAc,oBAAoB,CAAC;AAEnC,uDAAuD;AACvD,uEAAuE;AACvE,OAAO,EAKL,uBAAuB,GAExB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EAAE,iBAAiB,EAAoB,MAAM,8BAA8B,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAE7E,2EAA2E;AAC3E,cAAc,uBAAuB,CAAC;AAEtC,oEAAoE;AACpE,cAAc,wBAAwB,CAAC;AAEvC,gDAAgD;AAChD,cAAc,mBAAmB,CAAC;AAElC,8EAA8E;AAC9E,cAAc,yBAAyB,CAAC;AAExC,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * What a listing does with a directory it was refused — the vocabulary shared
3
+ * by the filesystem walk (`file-crawler.ts`) and the git listings
4
+ * (`git-utils.ts`), so a caller decides ONCE for both routes.
5
+ *
6
+ * 🚨 A refused listing is a gap in the POPULATION: every file beneath that
7
+ * directory the listing would have found was never seen and is absent from
8
+ * every count downstream. There are exactly two honest answers — stop, with a
9
+ * sentence the adopter can act on, or keep going and REPORT the gap — and
10
+ * which one is right is the caller's knowledge, not the crawler's. So the
11
+ * policy is {@link UnreadablePolicy}, it is REQUIRED wherever a listing is
12
+ * asked for, and it has no default.
13
+ *
14
+ * 🪤 It used to be `onUnreadable?`, an optional callback. Every caller that
15
+ * ignored it compiled; the two routes then defaulted differently (the walk
16
+ * threw a programmer-facing sentence, the git route dropped the refusal off
17
+ * its stderr without a word); and the miss surfaced one review round later as
18
+ * a HIGH. A required field is what makes `tsc` enumerate the callers.
19
+ */
20
+ import { VatError } from './errors/vat-error.js';
21
+ import { type DirectoryRefusal } from './fs-utils.js';
22
+ /**
23
+ * What a caller that STOPS on a refusal tells the person reading the error.
24
+ *
25
+ * A caller that has decided to stop — a build that must not ship an incomplete
26
+ * bundle, a discovery that must not silently find fewer skills — owes the
27
+ * adopter the directory against the root they know, and the knob THEY have.
28
+ */
29
+ export interface RefuseListingContext {
30
+ /**
31
+ * The root the refused directory is expressed against. An absolute path in
32
+ * an error is the developer's `$HOME` in every CI log.
33
+ */
34
+ root: string;
35
+ /**
36
+ * What the adopter can do about it — naming this lane's deliberate-drop
37
+ * mechanism (`resources.exclude`, a plugin `exclude:`, `--exclude`), which
38
+ * differs per caller and is why the crawler cannot supply it.
39
+ */
40
+ remedy: string;
41
+ }
42
+ /**
43
+ * The caller's decision about a directory the listing cannot open.
44
+ *
45
+ * - `{ refuse }` — STOP: the listing throws {@link DirectoryListingRefusedError}
46
+ * with the adopter-facing sentence built from the context. Right for every
47
+ * lane whose output is acted on as a complete population (a build, a
48
+ * discovery, a fingerprint, a copy).
49
+ * - `{ degrade }` — KEEP GOING and hand the refusal over. The caller is
50
+ * promising to REPORT the gap — as its own finding, with the directory and
51
+ * errno the refusal carries — not to drop it. Right for a backstop appended
52
+ * to a larger report, where one unlistable directory must not destroy every
53
+ * finding beside it.
54
+ *
55
+ * A directory that VANISHED between being enumerated and being listed
56
+ * (`ENOENT` / `ENOTDIR`) is not a refusal under either arm: it is no longer in
57
+ * the population and is skipped without a call.
58
+ */
59
+ export type UnreadablePolicy = {
60
+ readonly refuse: RefuseListingContext;
61
+ } | {
62
+ readonly degrade: (refusal: DirectoryRefusal) => void;
63
+ };
64
+ /**
65
+ * Thrown by a listing under `{ refuse }` when a directory refused to be
66
+ * listed. The message is the adopter's sentence — see {@link refusedListingMessage}.
67
+ */
68
+ export declare class DirectoryListingRefusedError extends VatError {
69
+ readonly refusal: DirectoryRefusal;
70
+ constructor(refusal: DirectoryRefusal, context: RefuseListingContext);
71
+ }
72
+ /**
73
+ * The adopter-facing sentence: root-relative directory, errno, the gap, and
74
+ * the caller's remedy — **the one owner of that sentence.**
75
+ *
76
+ * Exported so every lane that refuses says the same thing. Three lanes used to
77
+ * compose this by hand and had drifted by a clause each ("enumerated" /
78
+ * "scanned" / "the population could not be enumerated"); the transient clause
79
+ * in particular was written once in `fs-utils.ts` precisely so nobody would.
80
+ * See {@link REFUSAL_GAP_CLAUSE} for why it asserts nothing about what WAS
81
+ * enumerated beneath the directory.
82
+ *
83
+ * @param refusal - What was refused
84
+ * @param context - The root to express it against, and what the adopter can do
85
+ * @returns One sentence, root-relative, never containing the absolute path
86
+ */
87
+ export declare function refusedListingMessage(refusal: DirectoryRefusal, context: RefuseListingContext): string;
88
+ /**
89
+ * Apply the caller's policy to one refusal: throw under `{ refuse }`, hand it
90
+ * over under `{ degrade }`. The single place either route settles a refusal,
91
+ * so the two cannot drift.
92
+ *
93
+ * @param policy - The caller's decision
94
+ * @param refusal - The directory that could not be listed
95
+ */
96
+ export declare function settleRefusal(policy: UnreadablePolicy, refusal: DirectoryRefusal): void;
97
+ /**
98
+ * Refuse an omitted policy up front, by name, before any directory is listed.
99
+ *
100
+ * The type already makes `unreadable` required, and that is what enumerates
101
+ * the TypeScript callers. This is the runtime half, for the callers the type
102
+ * cannot reach — test files are not typechecked by the build, and a JavaScript
103
+ * adopter has no compiler at all. Without it an omitted policy would surface
104
+ * only on the first refusal, as a `TypeError` from the `in` operator, and a
105
+ * tree with nothing unreadable would keep returning complete lists — the
106
+ * optional seam back again, one layer down.
107
+ *
108
+ * @param policy - What the caller passed as `unreadable`
109
+ * @param api - The entry point, for the sentence
110
+ */
111
+ export declare function requireUnreadablePolicy(policy: UnreadablePolicy | undefined, api: string): asserts policy is UnreadablePolicy;
112
+ //# sourceMappingURL=listing-refusal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listing-refusal.d.ts","sourceRoot":"","sources":["../src/listing-refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,KAAK,gBAAgB,EAA0B,MAAM,eAAe,CAAC;AAG9E;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAA;CAAE,GACzC;IAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAAE,CAAC;AAE9D;;;GAGG;AACH,qBAAa,4BAA6B,SAAQ,QAAQ;IACxD,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;gBAEvB,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,oBAAoB;CAIrE;AAmBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,oBAAoB,GAAG,MAAM,CAOtG;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,gBAAgB,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAGvF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,gBAAgB,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,gBAAgB,CAO7H"}
@@ -0,0 +1,105 @@
1
+ /**
2
+ * What a listing does with a directory it was refused — the vocabulary shared
3
+ * by the filesystem walk (`file-crawler.ts`) and the git listings
4
+ * (`git-utils.ts`), so a caller decides ONCE for both routes.
5
+ *
6
+ * 🚨 A refused listing is a gap in the POPULATION: every file beneath that
7
+ * directory the listing would have found was never seen and is absent from
8
+ * every count downstream. There are exactly two honest answers — stop, with a
9
+ * sentence the adopter can act on, or keep going and REPORT the gap — and
10
+ * which one is right is the caller's knowledge, not the crawler's. So the
11
+ * policy is {@link UnreadablePolicy}, it is REQUIRED wherever a listing is
12
+ * asked for, and it has no default.
13
+ *
14
+ * 🪤 It used to be `onUnreadable?`, an optional callback. Every caller that
15
+ * ignored it compiled; the two routes then defaulted differently (the walk
16
+ * threw a programmer-facing sentence, the git route dropped the refusal off
17
+ * its stderr without a word); and the miss surfaced one review round later as
18
+ * a HIGH. A required field is what makes `tsc` enumerate the callers.
19
+ */
20
+ import { VatError } from './errors/vat-error.js';
21
+ import { transientRefusalClause } from './fs-utils.js';
22
+ import { safePath, toForwardSlash } from './path-utils.js';
23
+ /**
24
+ * Thrown by a listing under `{ refuse }` when a directory refused to be
25
+ * listed. The message is the adopter's sentence — see {@link refusedListingMessage}.
26
+ */
27
+ export class DirectoryListingRefusedError extends VatError {
28
+ refusal;
29
+ constructor(refusal, context) {
30
+ super('DIRECTORY_LISTING_REFUSED', refusedListingMessage(refusal, context));
31
+ this.refusal = refusal;
32
+ }
33
+ }
34
+ /**
35
+ * What a refused listing costs, said so it is true on BOTH routes.
36
+ *
37
+ * 🪤 It used to read "so nothing beneath it was enumerated: every file there
38
+ * is in the declared scan". Both halves were the readdir walk's truth and the
39
+ * git route's lie: `git ls-files --others` reports the refusal while the index
40
+ * has already named every TRACKED file beneath the directory — the same run
41
+ * that printed the sentence had scanned one of them — and the projection's
42
+ * population is the whole non-ignored tree, which no `include`/`exclude`
43
+ * "declared". What holds on every route is the hazard itself: whatever the
44
+ * listing would have found and nothing else named is missing, and the shorter
45
+ * result is indistinguishable from a complete one.
46
+ */
47
+ const REFUSAL_GAP_CLAUSE = 'any file beneath it that no other listing named is absent from every count, ' +
48
+ 'and the result cannot be told from a complete one';
49
+ /**
50
+ * The adopter-facing sentence: root-relative directory, errno, the gap, and
51
+ * the caller's remedy — **the one owner of that sentence.**
52
+ *
53
+ * Exported so every lane that refuses says the same thing. Three lanes used to
54
+ * compose this by hand and had drifted by a clause each ("enumerated" /
55
+ * "scanned" / "the population could not be enumerated"); the transient clause
56
+ * in particular was written once in `fs-utils.ts` precisely so nobody would.
57
+ * See {@link REFUSAL_GAP_CLAUSE} for why it asserts nothing about what WAS
58
+ * enumerated beneath the directory.
59
+ *
60
+ * @param refusal - What was refused
61
+ * @param context - The root to express it against, and what the adopter can do
62
+ * @returns One sentence, root-relative, never containing the absolute path
63
+ */
64
+ export function refusedListingMessage(refusal, context) {
65
+ const relative = toForwardSlash(safePath.relative(context.root, refusal.directory));
66
+ const where = relative === '' ? 'the scan root itself' : `the directory '${relative}'`;
67
+ const remedy = refusal.transient
68
+ ? `${transientRefusalClause(refusal.code)}, so nothing is wrong with the tree — re-run before investigating anything.`
69
+ : context.remedy;
70
+ return `Listing ${where} was refused (${refusal.code}): ${REFUSAL_GAP_CLAUSE}. ${remedy}`;
71
+ }
72
+ /**
73
+ * Apply the caller's policy to one refusal: throw under `{ refuse }`, hand it
74
+ * over under `{ degrade }`. The single place either route settles a refusal,
75
+ * so the two cannot drift.
76
+ *
77
+ * @param policy - The caller's decision
78
+ * @param refusal - The directory that could not be listed
79
+ */
80
+ export function settleRefusal(policy, refusal) {
81
+ if ('refuse' in policy)
82
+ throw new DirectoryListingRefusedError(refusal, policy.refuse);
83
+ policy.degrade(refusal);
84
+ }
85
+ /**
86
+ * Refuse an omitted policy up front, by name, before any directory is listed.
87
+ *
88
+ * The type already makes `unreadable` required, and that is what enumerates
89
+ * the TypeScript callers. This is the runtime half, for the callers the type
90
+ * cannot reach — test files are not typechecked by the build, and a JavaScript
91
+ * adopter has no compiler at all. Without it an omitted policy would surface
92
+ * only on the first refusal, as a `TypeError` from the `in` operator, and a
93
+ * tree with nothing unreadable would keep returning complete lists — the
94
+ * optional seam back again, one layer down.
95
+ *
96
+ * @param policy - What the caller passed as `unreadable`
97
+ * @param api - The entry point, for the sentence
98
+ */
99
+ export function requireUnreadablePolicy(policy, api) {
100
+ if (policy === undefined) {
101
+ throw new TypeError(`${api}: \`unreadable\` is required — pass { refuse: { root, remedy } } to stop on a directory the listing cannot open, ` +
102
+ 'or { degrade: (refusal) => … } to keep going and report the gap yourself.');
103
+ }
104
+ }
105
+ //# sourceMappingURL=listing-refusal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listing-refusal.js","sourceRoot":"","sources":["../src/listing-refusal.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAyB,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AA4C3D;;;GAGG;AACH,MAAM,OAAO,4BAA6B,SAAQ,QAAQ;IAC/C,OAAO,CAAmB;IAEnC,YAAY,OAAyB,EAAE,OAA6B;QAClE,KAAK,CAAC,2BAA2B,EAAE,qBAAqB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAC5E,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,kBAAkB,GACtB,8EAA8E;IAC9E,mDAAmD,CAAC;AAEtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAAyB,EAAE,OAA6B;IAC5F,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IACpF,MAAM,KAAK,GAAG,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,kBAAkB,QAAQ,GAAG,CAAC;IACvF,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS;QAC9B,CAAC,CAAC,GAAG,sBAAsB,CAAC,OAAO,CAAC,IAAI,CAAC,6EAA6E;QACtH,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACnB,OAAO,WAAW,KAAK,iBAAiB,OAAO,CAAC,IAAI,MAAM,kBAAkB,KAAK,MAAM,EAAE,CAAC;AAC5F,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,MAAwB,EAAE,OAAyB;IAC/E,IAAI,QAAQ,IAAI,MAAM;QAAE,MAAM,IAAI,4BAA4B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACvF,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,uBAAuB,CAAC,MAAoC,EAAE,GAAW;IACvF,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,SAAS,CACjB,GAAG,GAAG,mHAAmH;YACvH,2EAA2E,CAC9E,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `isUnderRoot` — the containment question a SINK asks.
3
+ *
4
+ * A delete, a copy, an uninstall: each takes a path that somebody else spelled
5
+ * (a manifest's `name:`, a `flatSkills` entry, a positional the user typed) and
6
+ * is about to act on `join(root, that)`. The question is whether the action
7
+ * lands inside the root, and the only honest answer comes from the
8
+ * filesystem: `startsWith('..')` on a lexical `relative()` misses a symlink
9
+ * inside the root that points out, refuses a member named `..cache`, and is
10
+ * blind to a drive letter — the sweep behind this module watched all three
11
+ * happen at exit 0.
12
+ *
13
+ * Both sides are canonicalized the same way, through the deepest existing
14
+ * ancestor, so a root reached through a symlink (macOS `/tmp`, a linked
15
+ * `~/.claude`) still contains its members, and a candidate that does not exist
16
+ * yet is judged by where creating it would land. Absence is reported as its
17
+ * own answer rather than folded into either side, because a delete sink and a
18
+ * create sink want opposite things from it.
19
+ *
20
+ * Lexical classification of an already-relative path — a projection identity,
21
+ * a report relativizer — is a different question with a different helper:
22
+ * `relativeEscapesRoot` in `path-core.ts`, which touches no filesystem.
23
+ */
24
+ /**
25
+ * Where a candidate stands relative to a root, as the filesystem sees it.
26
+ *
27
+ * - `inside` — the candidate exists and its realpath is a STRICT descendant of
28
+ * the root's realpath. A delete or copy may proceed.
29
+ * - `outside` — its realpath (or, when it does not exist, the realpath of its
30
+ * deepest existing ancestor with the missing remainder re-appended) is not
31
+ * under the root. The root itself is `outside` too: nothing is under itself,
32
+ * and a sink that could delete its own root has no business here.
33
+ * - `absent` — nothing exists at the candidate, and creating it would land
34
+ * inside the root. A create sink proceeds; a delete sink has nothing to do.
35
+ */
36
+ export type Containment = 'inside' | 'outside' | 'absent';
37
+ /**
38
+ * Is `candidate` strictly under `root`, as the filesystem judges it?
39
+ *
40
+ * Both paths may be relative (resolved from cwd) or absolute, in either
41
+ * separator; neither has to exist. See {@link Containment} for the three
42
+ * answers and which sinks accept which.
43
+ *
44
+ * ⚠️ Costs one `lstat` per missing component plus two `realpath`s. That is
45
+ * nothing for a sink — a delete or a copy dwarfs it — and too much for a loop
46
+ * over every link in a corpus, which is why the projection and link lanes
47
+ * classify lexically with `relativeEscapesRoot` and never call this.
48
+ *
49
+ * @param root - The directory the action must stay inside
50
+ * @param candidate - The path the action is about to touch
51
+ * @returns `'inside'`, `'outside'`, or `'absent'`
52
+ * @throws When the OS refuses to examine either path for any reason but
53
+ * absence — a refusal is reported, never read as "not there"
54
+ *
55
+ * @example
56
+ * // A delete sink: only an existing, contained target may go.
57
+ * if (isUnderRoot(skillsDir, target) !== 'inside') throw new Error(`refusing to remove ${target}: not inside ${skillsDir}`);
58
+ * await rm(target, { recursive: true, force: true });
59
+ *
60
+ * // A create sink: absent-but-contained is exactly the happy path.
61
+ * if (isUnderRoot(skillsDir, dest) === 'outside') throw new Error(`refusing to install to ${dest}: not inside ${skillsDir}`);
62
+ */
63
+ export declare function isUnderRoot(root: string, candidate: string): Containment;
64
+ //# sourceMappingURL=path-containment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-containment.d.ts","sourceRoot":"","sources":["../src/path-containment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAQH;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AAkD1D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,CAQxE"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `isUnderRoot` — the containment question a SINK asks.
3
+ *
4
+ * A delete, a copy, an uninstall: each takes a path that somebody else spelled
5
+ * (a manifest's `name:`, a `flatSkills` entry, a positional the user typed) and
6
+ * is about to act on `join(root, that)`. The question is whether the action
7
+ * lands inside the root, and the only honest answer comes from the
8
+ * filesystem: `startsWith('..')` on a lexical `relative()` misses a symlink
9
+ * inside the root that points out, refuses a member named `..cache`, and is
10
+ * blind to a drive letter — the sweep behind this module watched all three
11
+ * happen at exit 0.
12
+ *
13
+ * Both sides are canonicalized the same way, through the deepest existing
14
+ * ancestor, so a root reached through a symlink (macOS `/tmp`, a linked
15
+ * `~/.claude`) still contains its members, and a candidate that does not exist
16
+ * yet is judged by where creating it would land. Absence is reported as its
17
+ * own answer rather than folded into either side, because a delete sink and a
18
+ * create sink want opposite things from it.
19
+ *
20
+ * Lexical classification of an already-relative path — a projection identity,
21
+ * a report relativizer — is a different question with a different helper:
22
+ * `relativeEscapesRoot` in `path-core.ts`, which touches no filesystem.
23
+ */
24
+ import { lstatSync } from 'node:fs';
25
+ import path from 'node:path';
26
+ import { isPathAbsentError } from './errors/errno.js';
27
+ import { normalizePath, safePath, toForwardSlash } from './path-utils.js';
28
+ /**
29
+ * The canonical spelling of `target`, from its deepest existing ancestor.
30
+ *
31
+ * Walks up until `lstat` answers, canonicalizes THAT with the same realpath
32
+ * every path helper uses, and re-appends the missing tail. A refusal is not
33
+ * an absence: `EACCES` on an ancestor, `ELOOP` on a cycle, an invalid name —
34
+ * the OS is saying it cannot examine the path, and a containment verdict
35
+ * built on the spelling it refused is the bug this module exists to remove.
36
+ * Those stay loud.
37
+ *
38
+ * `lstat` rather than `stat`, so a dangling symlink counts as existing: it is
39
+ * an entry the sink can act on (a delete removes the link), and its realpath
40
+ * failing is answered by {@link normalizePath} with the lexical spelling, which
41
+ * is where the entry is.
42
+ */
43
+ function canonicalFromAncestor(target) {
44
+ const absolute = safePath.resolve(target);
45
+ const missing = [];
46
+ let candidate = absolute;
47
+ for (;;) {
48
+ if (entryExists(candidate)) {
49
+ return {
50
+ canonical: safePath.join(toForwardSlash(normalizePath(candidate)), ...missing),
51
+ exists: missing.length === 0,
52
+ };
53
+ }
54
+ const parent = toForwardSlash(path.dirname(candidate));
55
+ // Fixpoint at a filesystem root: `dirname` returns its own input, nothing
56
+ // on the path resolved, and the lexical form is the only answer there is.
57
+ if (parent === candidate) {
58
+ return { canonical: absolute, exists: false };
59
+ }
60
+ missing.unshift(path.basename(candidate));
61
+ candidate = parent;
62
+ }
63
+ }
64
+ /** Whether an entry (a file, a directory, or a link — dangling or not) is at `p`. */
65
+ function entryExists(p) {
66
+ try {
67
+ lstatSync(p);
68
+ return true;
69
+ }
70
+ catch (error) {
71
+ if (isPathAbsentError(error))
72
+ return false;
73
+ throw error;
74
+ }
75
+ }
76
+ /**
77
+ * Is `candidate` strictly under `root`, as the filesystem judges it?
78
+ *
79
+ * Both paths may be relative (resolved from cwd) or absolute, in either
80
+ * separator; neither has to exist. See {@link Containment} for the three
81
+ * answers and which sinks accept which.
82
+ *
83
+ * ⚠️ Costs one `lstat` per missing component plus two `realpath`s. That is
84
+ * nothing for a sink — a delete or a copy dwarfs it — and too much for a loop
85
+ * over every link in a corpus, which is why the projection and link lanes
86
+ * classify lexically with `relativeEscapesRoot` and never call this.
87
+ *
88
+ * @param root - The directory the action must stay inside
89
+ * @param candidate - The path the action is about to touch
90
+ * @returns `'inside'`, `'outside'`, or `'absent'`
91
+ * @throws When the OS refuses to examine either path for any reason but
92
+ * absence — a refusal is reported, never read as "not there"
93
+ *
94
+ * @example
95
+ * // A delete sink: only an existing, contained target may go.
96
+ * if (isUnderRoot(skillsDir, target) !== 'inside') throw new Error(`refusing to remove ${target}: not inside ${skillsDir}`);
97
+ * await rm(target, { recursive: true, force: true });
98
+ *
99
+ * // A create sink: absent-but-contained is exactly the happy path.
100
+ * if (isUnderRoot(skillsDir, dest) === 'outside') throw new Error(`refusing to install to ${dest}: not inside ${skillsDir}`);
101
+ */
102
+ export function isUnderRoot(root, candidate) {
103
+ const rootReal = canonicalFromAncestor(root).canonical;
104
+ const target = canonicalFromAncestor(candidate);
105
+ const prefix = rootReal.endsWith('/') ? rootReal : `${rootReal}/`;
106
+ if (!target.canonical.startsWith(prefix)) {
107
+ return 'outside';
108
+ }
109
+ return target.exists ? 'inside' : 'absent';
110
+ }
111
+ //# sourceMappingURL=path-containment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-containment.js","sourceRoot":"","sources":["../src/path-containment.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAgB1E;;;;;;;;;;;;;;GAcG;AACH,SAAS,qBAAqB,CAAC,MAAc;IAC3C,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,SAAS,GAAG,QAAQ,CAAC;IACzB,SAAS,CAAC;QACR,IAAI,WAAW,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3B,OAAO;gBACL,SAAS,EAAE,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC;gBAC9E,MAAM,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC;aAC7B,CAAC;QACJ,CAAC;QACD,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;QACvD,0EAA0E;QAC1E,0EAA0E;QAC1E,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QAChD,CAAC;QACD,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;QAC1C,SAAS,GAAG,MAAM,CAAC;IACrB,CAAC;AACH,CAAC;AAED,qFAAqF;AACrF,SAAS,WAAW,CAAC,CAAS;IAC5B,IAAI,CAAC;QACH,SAAS,CAAC,CAAC,CAAC,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,iBAAiB,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAC3C,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,SAAiB;IACzD,MAAM,QAAQ,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC;IACvD,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,GAAG,CAAC;IAClE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACzC,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC7C,CAAC"}