@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/dist/path-core.js CHANGED
@@ -11,6 +11,28 @@
11
11
  * `./path-utils.ts` and are exposed via the `./fs` entry.
12
12
  */
13
13
  import path from 'node:path';
14
+ import { VatError } from './errors/vat-error.js';
15
+ /**
16
+ * Thrown by {@link safePath.joinUnderRoot} when the joined path would land
17
+ * outside its root.
18
+ *
19
+ * A class with a code rather than a prefixed sentence: three packages used to
20
+ * recognise this refusal with `error.message.startsWith('safePath.joinUnderRoot:')`,
21
+ * which is a contract on prose. Dispatch with
22
+ * `isVatError(error, PathEscapesRootError.code)` — it survives the `src`/`dist`
23
+ * boundary that `instanceof` does not.
24
+ */
25
+ export class PathEscapesRootError extends VatError {
26
+ /** The code every instance carries, for `isVatError(error, PathEscapesRootError.code)`. */
27
+ static code = 'PATH_ESCAPES_ROOT';
28
+ /**
29
+ * @param root - The root the path had to stay under
30
+ * @param detail - Which segment, or which result, escaped it
31
+ */
32
+ constructor(root, detail) {
33
+ super(PathEscapesRootError.code, `safePath.joinUnderRoot: ${detail} escapes root "${root}".`);
34
+ }
35
+ }
14
36
  /**
15
37
  * Check if a path is absolute
16
38
  *
@@ -49,8 +71,9 @@ export function isAbsoluteAnyPlatform(p) {
49
71
  /**
50
72
  * True if `p` contains a `..` parent-directory traversal segment.
51
73
  *
52
- * Forward-slash-normalized, then inspects each `/`-delimited segment — so a
53
- * `..` is caught regardless of the original OS separator. A containment guard
74
+ * Backslashes are read as separators on every host (the input is author-written
75
+ * config), then each `/`-delimited segment is inspected — so a `..` is caught
76
+ * regardless of the separator the author typed. A containment guard
54
77
  * for config-supplied relative paths (skill `files:` dest values, glob magic
55
78
  * remainders) that must never climb above their anchor directory.
56
79
  *
@@ -61,7 +84,74 @@ export function isAbsoluteAnyPlatform(p) {
61
84
  * hasParentTraversalSegment('a..b/c') // false (".." must be a whole segment)
62
85
  */
63
86
  export function hasParentTraversalSegment(p) {
64
- return toForwardSlash(p).split('/').includes('..');
87
+ // eslint-disable-next-line local/no-dotdot-containment -- this IS the one lexical `..`-segment test the rule points to; it classifies a config-supplied RELATIVE spelling before any root exists to ask the filesystem about. Sinks use isUnderRoot().
88
+ return toForwardSlashAnyPlatform(p).split('/').includes('..');
89
+ }
90
+ /**
91
+ * True when a root-relative path — as `safePath.relative(root, p)` spells it —
92
+ * names something the root does not contain.
93
+ *
94
+ * The three shapes `path.relative` can return for an outsider: the parent
95
+ * itself (`..`), a climb through it (`../x`), and, on Windows only, an
96
+ * absolute path (a target on another drive has no relative spelling). A name
97
+ * that merely BEGINS with two dots (`..notes.md`) is a member and reads as one;
98
+ * the bare `startsWith('..')` this replaces refused it, dropped the file from
99
+ * the package and unlinked the reference, at exit 0.
100
+ *
101
+ * ⚠️ **Lexical, on purpose.** This classifies a relative path that was already
102
+ * computed; it does not ask the filesystem, so a symlink inside the root that
103
+ * points outside reads as inside here. That is the right answer for the
104
+ * callers that own no root to ask about — a projection identity, a permission
105
+ * pattern, a report relativizer — and the WRONG one for a delete or copy sink,
106
+ * which must ask {@link isUnderRoot} from `@vibe-agent-toolkit/utils` instead.
107
+ * The empty relative (the root itself) is not an escape; whether equality is
108
+ * acceptable is the caller's question and is asked beside this one.
109
+ *
110
+ * @param normalizedRelative - A forward-slashed root-relative path
111
+ * @returns True when the root does not contain it
112
+ *
113
+ * @example
114
+ * relativeEscapesRoot(safePath.relative(root, p)) // the whole idiom
115
+ * relativeEscapesRoot('../x') // true
116
+ * relativeEscapesRoot('..notes.md') // false — a member whose name starts with dots
117
+ */
118
+ export function relativeEscapesRoot(normalizedRelative) {
119
+ // eslint-disable-next-line local/no-dotdot-containment -- this IS the one lexical relative-path classifier the rule points to; every former copy of this pair now calls here. Sinks use isUnderRoot().
120
+ return normalizedRelative === '..' || normalizedRelative.startsWith('../') || isAbsoluteAnyPlatform(normalizedRelative);
121
+ }
122
+ /** A Windows drive-relative spelling (`C:`) — `path.resolve` sends it to that drive's cwd. */
123
+ const DRIVE_RELATIVE = /^[A-Za-z]:/u;
124
+ /**
125
+ * True when `name` can only ever be ONE directory entry under whatever it is
126
+ * joined to: non-empty, not `.` or `..`, no separator of either platform, no
127
+ * NUL, no drive-letter prefix.
128
+ *
129
+ * The check for a caller-controlled NAME — a skill name from a manifest, a
130
+ * session id, a positional the user typed — that is about to become
131
+ * `join(root, name)`. A name is not a path: `..cache` and `a..b` are legitimate
132
+ * entries, and `includes('..')` refused them while `startsWith` let `x/../..`
133
+ * through. The question is whether the join can land anywhere but directly
134
+ * under `root`, and that is answered by the segment's shape alone, with no
135
+ * filesystem — which is also why this belongs beside the path helpers rather
136
+ * than beside {@link isUnderRoot}, which is the check for a PATH.
137
+ *
138
+ * @param name - The proposed entry name
139
+ * @returns True when `join(root, name)` is a direct child of `root`
140
+ *
141
+ * @example
142
+ * isSingleFsSegment('my-skill') // true
143
+ * isSingleFsSegment('..cache') // true — dots inside a name are just dots
144
+ * isSingleFsSegment('../victim') // false
145
+ * isSingleFsSegment('..') // false
146
+ */
147
+ export function isSingleFsSegment(name) {
148
+ return (name !== '' &&
149
+ name !== '.' &&
150
+ name !== '..' &&
151
+ !name.includes('/') &&
152
+ !name.includes('\\') &&
153
+ !name.includes('\0') &&
154
+ !DRIVE_RELATIVE.test(name));
65
155
  }
66
156
  /**
67
157
  * Compute a `ValidationIssue.location`: an absolute source file path made
@@ -69,7 +159,7 @@ export function hasParentTraversalSegment(p) {
69
159
  *
70
160
  * This is the ONE relativizer every VAT validation lane uses. `location` is
71
161
  * contractually project-relative (see `ValidationIssue` in
72
- * `@vibe-agent-toolkit/agent-schema`), so producers must route through here
162
+ * `@vibe-agent-toolkit/schema`), so producers must route through here
73
163
  * rather than emitting `skillPath` directly — absolute locations leak the
74
164
  * developer's home directory into CI logs and make `validation.allow` globs,
75
165
  * which match against `location`, unwritable.
@@ -133,36 +223,94 @@ export function getRelativePath(from, to) {
133
223
  // Calculate relative path from source directory to target file
134
224
  return toForwardSlash(path.relative(fromDir, to));
135
225
  }
226
+ /** True on hosts (win32) where a backslash is a path separator; everywhere else it is a filename character. */
227
+ const BACKSLASH_IS_NATIVE_SEPARATOR = path.sep === '\\';
136
228
  /**
137
- * Convert a path to forward slashes
229
+ * Convert a NATIVE path — one the filesystem, `path.*`, `readdir` or git
230
+ * handed you — to forward slashes.
138
231
  *
139
- * Windows accepts both forward slashes and backslashes as path separators.
140
- * This function normalizes all paths to use forward slashes for consistency.
141
- * Useful for glob pattern matching, cross-platform comparisons, and string operations.
232
+ * Converts only where a backslash is a separator (win32). On POSIX a backslash is a
233
+ * legal filename character, so `docs/x\y.md` is one file and is returned
234
+ * unchanged: converting it would invent a phantom `docs/x/` directory and, in
235
+ * {@link safePath.joinUnderRoot}, turn an `x\..\..` NAME into a climb.
142
236
  *
143
- * @param p - Path to convert
144
- * @returns Path with forward slashes
237
+ * For AUTHOR-WRITTEN text — an href, a glob, a config value, a CLI argument,
238
+ * an archive entry name — whose backslashes must read as separators on every
239
+ * host, use {@link toForwardSlashAnyPlatform}.
145
240
  *
146
- * @example
147
- * toForwardSlash('C:\\Users\\docs\\README.md')
148
- * // Returns: 'C:/Users/docs/README.md'
241
+ * @param p - A native path
242
+ * @returns The path with forward slashes (identity on POSIX)
149
243
  *
150
- * toForwardSlash('/project/docs/README.md')
151
- * // Returns: '/project/docs/README.md' (unchanged)
244
+ * @example
245
+ * toForwardSlash('C:\\Users\\docs\\README.md') // win32: 'C:/Users/docs/README.md'
246
+ * toForwardSlash('docs/x\\y.md') // POSIX: 'docs/x\\y.md' (unchanged)
152
247
  */
153
248
  export function toForwardSlash(p) {
154
- return p.replaceAll('\\', '/');
249
+ return BACKSLASH_IS_NATIVE_SEPARATOR ? toForwardSlashAnyPlatform(p) : p;
250
+ }
251
+ /**
252
+ * Convert every backslash to a forward slash, on every host.
253
+ *
254
+ * For AUTHOR-WRITTEN text that may carry Windows spellings regardless of where
255
+ * VAT runs — markdown hrefs, globs, config values, CLI arguments, zip entry
256
+ * names — and for containment guards that must refuse `..\x` everywhere.
257
+ * Never use it on a path read from the filesystem or git: on POSIX that
258
+ * backslash is part of a filename. Use {@link toForwardSlash} for those.
259
+ *
260
+ * @param text - Author-written path text
261
+ * @returns The text with every backslash replaced by `/`
262
+ *
263
+ * @example
264
+ * toForwardSlashAnyPlatform('..\\evil') // '../evil' on every host
265
+ */
266
+ export function toForwardSlashAnyPlatform(text) {
267
+ // eslint-disable-next-line local/no-manual-path-normalize -- this IS the converter the rule's autofix writes; it cannot call itself.
268
+ return text.replaceAll('\\', '/');
269
+ }
270
+ /**
271
+ * Normalize text to Unicode NFC — the form in which two *visually identical*
272
+ * filenames compare equal.
273
+ *
274
+ * `é` has two encodings: precomposed NFC (`U+00E9`) and decomposed NFD
275
+ * (`e` + `U+0301`). They render identically and name the same file, yet they are
276
+ * different strings, so `===`, `toLowerCase()`, `Map.get()` and `Set.has()` all
277
+ * report them as different. `readdir` hands back whichever form is on disk —
278
+ * APFS preserves what was written, and decomposed names are common on macOS —
279
+ * while a markdown link typed in an editor almost always carries the composed
280
+ * form. The two sides of a filename comparison therefore disagree about a file
281
+ * that plainly exists.
282
+ *
283
+ * ⚠️ **This produces a COMPARISON KEY, never a path to open.** Do not normalize
284
+ * a path on its way to `fs.*`. macOS would not notice — it is
285
+ * normalization-*insensitive* at the syscall level, so `existsSync` answers the
286
+ * same for either form — but Linux is not: on ext4 the two forms are simply
287
+ * different byte sequences naming different files, so opening the normalized
288
+ * form of a decomposed filename fails outright. That asymmetry is exactly why
289
+ * this is not folded into {@link safePath.resolve}: its output is handed
290
+ * straight to the filesystem. Normalize where two strings are *compared*, and
291
+ * leave the string the filesystem receives alone.
292
+ *
293
+ * @param value - A filename, path segment, or whole path
294
+ * @returns The same text in NFC. Pure ASCII is returned unchanged.
295
+ *
296
+ * @example
297
+ * toNfc('cafe\u0301.md') === toNfc('caf\u00e9.md') // true — same file, two encodings
298
+ */
299
+ export function toNfc(value) {
300
+ return value.normalize('NFC');
155
301
  }
156
302
  /**
157
303
  * Cross-platform safe path operations.
158
304
  *
159
305
  * Wraps Node's `path.join()`, `path.resolve()`, and `path.relative()` to always
160
- * return forward-slash paths. On Windows, the native `path.*` functions return
306
+ * return forward-slash paths (converted through {@link toForwardSlash}, so a
307
+ * backslash inside a POSIX filename survives). On Windows, the native `path.*` functions return
161
308
  * backslashes, which causes bugs when paths are used as Map keys, compared as
162
309
  * strings, or matched with glob patterns.
163
310
  *
164
311
  * **Use these instead of importing from `node:path` directly.**
165
- * ESLint rules enforce this — see `no-path-join`, `no-path-resolve`, `no-path-relative`.
312
+ * An ESLint rule enforces this — see `no-raw-node-path` (its `functions` option
313
+ * table maps each of `join`/`resolve`/`relative` to its `safePath.*` replacement).
166
314
  *
167
315
  * @example
168
316
  * ```typescript
@@ -173,7 +321,7 @@ export function toForwardSlash(p) {
173
321
  * safePath.resolve('/project', './docs') // → '/project/docs'
174
322
  * safePath.relative('/project/docs', '/project') // → '..'
175
323
  * safePath.joinUnderRoot('/harness', 'skill-abc') // → '/harness/skill-abc'
176
- * safePath.joinUnderRoot('/harness', '../escape') // throws Error
324
+ * safePath.joinUnderRoot('/harness', '../escape') // throws PathEscapesRootError
177
325
  * ```
178
326
  */
179
327
  export const safePath = {
@@ -207,7 +355,7 @@ export const safePath = {
207
355
  * skill-test staging code was vulnerable to on Windows.
208
356
  *
209
357
  * @returns Forward-slash absolute path guaranteed to be inside `root`.
210
- * @throws {Error} If the resolved path would escape `root`.
358
+ * @throws {PathEscapesRootError} If the resolved path would escape `root`.
211
359
  *
212
360
  * @example
213
361
  * ```typescript
@@ -223,12 +371,12 @@ export const safePath = {
223
371
  // BEFORE resolving, so the error message can name the offending segment.
224
372
  for (const seg of segments) {
225
373
  if (path.isAbsolute(seg)) {
226
- throw new Error(`safePath.joinUnderRoot: segment "${seg}" is absolute and escapes root "${root}".`);
374
+ throw new PathEscapesRootError(root, `segment "${seg}" is absolute and`);
227
375
  }
228
376
  // Windows drive-letter check for POSIX hosts (path.isAbsolute won't catch
229
377
  // 'C:\...' on POSIX, but node's path.win32.isAbsolute does).
230
378
  if (path.win32.isAbsolute(seg)) {
231
- throw new Error(`safePath.joinUnderRoot: segment "${seg}" contains a Windows drive letter and escapes root "${root}".`);
379
+ throw new PathEscapesRootError(root, `segment "${seg}" contains a Windows drive letter and`);
232
380
  }
233
381
  }
234
382
  const resolvedRoot = path.resolve(root);
@@ -243,7 +391,7 @@ export const safePath = {
243
391
  // which would match '/rootEvil' when root is '/root').
244
392
  const rootPrefix = fwdRoot.endsWith('/') ? fwdRoot : `${fwdRoot}/`;
245
393
  if (fwdResult !== fwdRoot && !fwdResult.startsWith(rootPrefix)) {
246
- throw new Error(`safePath.joinUnderRoot: result "${fwdResult}" escapes root "${fwdRoot}".`);
394
+ throw new PathEscapesRootError(fwdRoot, `result "${fwdResult}"`);
247
395
  }
248
396
  return fwdResult;
249
397
  },
@@ -1 +1 @@
1
- {"version":3,"file":"path-core.js","sourceRoot":"","sources":["../src/path-core.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"path-core.js","sourceRoot":"","sources":["../src/path-core.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,MAAM,SAAS,CAAC;AAOlD;;;;;;;;;;GAUG;AACH,cAAc,gBAAgB,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,aAAa,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CA+BxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAezC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,GACxC,MAAM,CAiBR;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ1F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAGhF"}
1
+ {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,MAAM,SAAS,CAAC;AAQlD;;;;;;;;;;GAUG;AACH,cAAc,gBAAgB,CAAC;AAmC/B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,aAAa,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAqBxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,GACxC,MAAM,CAIR;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAQ1F;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAGhF"}
@@ -2,6 +2,7 @@ import { mkdirSync, realpathSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { isPathAbsentError } from './errors/errno.js';
5
6
  import { safePath } from './path-core.js';
6
7
  /**
7
8
  * Re-export every pure path helper so the package barrel keeps exposing the
@@ -15,6 +16,42 @@ import { safePath } from './path-core.js';
15
16
  * `test/path-fs-subpaths.test.ts`).
16
17
  */
17
18
  export * from './path-core.js';
19
+ /**
20
+ * `realpathSync.native(target)`, or `target` itself when there is nothing at
21
+ * that path to canonicalize.
22
+ *
23
+ * Absence is the ONLY failure answered with the lexical path: `normalizePath`
24
+ * is documented to accept a path that does not exist, and a path that does not
25
+ * exist has no realpath. Everything else — `EACCES` on an ancestor, `ELOOP` on
26
+ * a symlink cycle, `ENAMETOOLONG` — is the filesystem refusing a path that IS
27
+ * there. Those used to come back as the lexical spelling with nothing to say
28
+ * so, which put a lexical path where every caller compares canonical ones: a
29
+ * containment check judged such a path by the spelling the OS had just refused
30
+ * to resolve. They stay loud.
31
+ *
32
+ * The JS `realpathSync` is asked before absence is concluded, because the
33
+ * native call can fail where the JS walk succeeds — Node's docs note that on a
34
+ * musl libc without procfs the native `realpath(3)` cannot work at all, and it
35
+ * reports that as `ENOENT` for a path that exists. Only when both agree that
36
+ * nothing is there is the lexical path the answer.
37
+ */
38
+ function realpathOrSelf(target) {
39
+ try {
40
+ return realpathSync.native(target);
41
+ }
42
+ catch (error) {
43
+ if (!isPathAbsentError(error))
44
+ throw error;
45
+ }
46
+ try {
47
+ return realpathSync(target);
48
+ }
49
+ catch (error) {
50
+ if (isPathAbsentError(error))
51
+ return target;
52
+ throw error;
53
+ }
54
+ }
18
55
  /**
19
56
  * Normalize any path (resolve short names on Windows)
20
57
  *
@@ -65,21 +102,9 @@ export function normalizePath(...paths) {
65
102
  else {
66
103
  resolved = path.resolve(...paths);
67
104
  }
68
- try {
69
- // Use native OS realpath for better Windows compatibility
70
- return realpathSync.native(resolved);
71
- }
72
- catch {
73
- // Fallback to regular realpathSync
74
- try {
75
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Safe: resolved is from path.resolve
76
- return realpathSync(resolved);
77
- }
78
- catch {
79
- // Last resort: return resolved path (better than original input)
80
- return resolved;
81
- }
82
- }
105
+ // Native OS realpath first (resolves Windows short names); a path that is not
106
+ // there is answered with `resolved` — better than the original input.
107
+ return realpathOrSelf(resolved);
83
108
  }
84
109
  /**
85
110
  * Get normalized temp directory path
@@ -107,22 +132,7 @@ export function normalizePath(...paths) {
107
132
  * ```
108
133
  */
109
134
  export function normalizedTmpdir() {
110
- const temp = tmpdir();
111
- try {
112
- // Use native OS realpath for better Windows compatibility
113
- return realpathSync.native(temp);
114
- }
115
- catch {
116
- // Fallback to regular realpathSync
117
- try {
118
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Safe: temp is from tmpdir()
119
- return realpathSync(temp);
120
- }
121
- catch {
122
- // Last resort: return original
123
- return temp;
124
- }
125
- }
135
+ return realpathOrSelf(tmpdir());
126
136
  }
127
137
  /**
128
138
  * Create directory and return normalized path
@@ -156,23 +166,8 @@ export function normalizedTmpdir() {
156
166
  * ```
157
167
  */
158
168
  export function mkdirSyncReal(dirPath, options) {
159
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- This IS the mkdirSyncReal() implementation
160
169
  mkdirSync(dirPath, options);
161
- try {
162
- // Use native OS realpath for better Windows compatibility
163
- return realpathSync.native(dirPath);
164
- }
165
- catch {
166
- // Fallback to regular realpathSync
167
- try {
168
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- Safe: dirPath is function parameter
169
- return realpathSync(dirPath);
170
- }
171
- catch {
172
- // Last resort: return original
173
- return dirPath;
174
- }
175
- }
170
+ return realpathOrSelf(dirPath);
176
171
  }
177
172
  /**
178
173
  * Resolve an OS-native absolute path from an ESM module's `import.meta.url` and
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"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"}
package/dist/path.d.ts CHANGED
@@ -8,5 +8,5 @@
8
8
  * If you need `normalizedTmpdir`, `mkdirSyncReal`, or `normalizePath`, those
9
9
  * touch the filesystem — import them from `@vibe-agent-toolkit/utils/fs`.
10
10
  */
11
- export { safePath, toForwardSlash, isAbsolutePath, isAbsoluteAnyPlatform, hasParentTraversalSegment, toAbsolutePath, getRelativePath, issueLocation, } from './path-core.js';
11
+ export { PathEscapesRootError, safePath, toForwardSlash, toForwardSlashAnyPlatform, toNfc, isAbsolutePath, isAbsoluteAnyPlatform, hasParentTraversalSegment, isSingleFsSegment, relativeEscapesRoot, toAbsolutePath, getRelativePath, issueLocation, } from './path-core.js';
12
12
  //# sourceMappingURL=path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,QAAQ,EACR,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,yBAAyB,EACzB,cAAc,EACd,eAAe,EACf,aAAa,GACd,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,oBAAoB,EACpB,QAAQ,EACR,cAAc,EACd,yBAAyB,EACzB,KAAK,EACL,cAAc,EACd,qBAAqB,EACrB,yBAAyB,EACzB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,aAAa,GACd,MAAM,gBAAgB,CAAC"}
package/dist/path.js CHANGED
@@ -8,5 +8,5 @@
8
8
  * If you need `normalizedTmpdir`, `mkdirSyncReal`, or `normalizePath`, those
9
9
  * touch the filesystem — import them from `@vibe-agent-toolkit/utils/fs`.
10
10
  */
11
- export { safePath, toForwardSlash, isAbsolutePath, isAbsoluteAnyPlatform, hasParentTraversalSegment, toAbsolutePath, getRelativePath, issueLocation, } from './path-core.js';
11
+ export { PathEscapesRootError, safePath, toForwardSlash, toForwardSlashAnyPlatform, toNfc, isAbsolutePath, isAbsoluteAnyPlatform, hasParentTraversalSegment, isSingleFsSegment, relativeEscapesRoot, toAbsolutePath, getRelativePath, issueLocation, } from './path-core.js';
12
12
  //# sourceMappingURL=path.js.map
package/dist/path.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"path.js","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,QAAQ,EACR,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,yBAAyB,EACzB,cAAc,EACd,eAAe,EACf,aAAa,GACd,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"path.js","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,oBAAoB,EACpB,QAAQ,EACR,cAAc,EACd,yBAAyB,EACzB,KAAK,EACL,cAAc,EACd,qBAAqB,EACrB,yBAAyB,EACzB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,eAAe,EACf,aAAa,GACd,MAAM,gBAAgB,CAAC"}
package/dist/process.d.ts CHANGED
@@ -10,5 +10,12 @@
10
10
  export { type StdioBlockingResult, makeStdioBlocking, describeStdioBlocking, } from './stdio-blocking.js';
11
11
  export { type SafeExecOptions, type SafeExecResult, CommandExecutionError, safeExecSync, safeExecResult, isToolAvailable, getToolVersion, hasShellSyntax, safeExecFromString, } from './safe-exec.js';
12
12
  export { spawnHardened } from './spawn-hardened.js';
13
+ /**
14
+ * On `./process` because "is this process running me as its entry script?" is a
15
+ * question about the process, and because every package that has a bin or a
16
+ * build script needs it — including ones that must not depend on the private
17
+ * `dev-tools` package, which is where it used to live.
18
+ */
19
+ export { isEntrypoint } from './entrypoint.js';
13
20
  export { shouldUseShell, windowsShellQuote, buildWindowsShellLine, isPathLike, resolveShellCommandToken, } from './windows-shell.js';
14
21
  //# sourceMappingURL=process.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,KAAK,mBAAmB,EACxB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,qBAAqB,EACrB,YAAY,EACZ,cAAc,EACd,eAAe,EACf,cAAc,EACd,cAAc,EACd,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,KAAK,mBAAmB,EACxB,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,qBAAqB,EACrB,YAAY,EACZ,cAAc,EACd,eAAe,EACf,cAAc,EACd,cAAc,EACd,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
package/dist/process.js CHANGED
@@ -10,5 +10,12 @@
10
10
  export { makeStdioBlocking, describeStdioBlocking, } from './stdio-blocking.js';
11
11
  export { CommandExecutionError, safeExecSync, safeExecResult, isToolAvailable, getToolVersion, hasShellSyntax, safeExecFromString, } from './safe-exec.js';
12
12
  export { spawnHardened } from './spawn-hardened.js';
13
+ /**
14
+ * On `./process` because "is this process running me as its entry script?" is a
15
+ * question about the process, and because every package that has a bin or a
16
+ * build script needs it — including ones that must not depend on the private
17
+ * `dev-tools` package, which is where it used to live.
18
+ */
19
+ export { isEntrypoint } from './entrypoint.js';
13
20
  export { shouldUseShell, windowsShellQuote, buildWindowsShellLine, isPathLike, resolveShellCommandToken, } from './windows-shell.js';
14
21
  //# sourceMappingURL=process.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"process.js","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAEL,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAGL,qBAAqB,EACrB,YAAY,EACZ,cAAc,EACd,eAAe,EACf,cAAc,EACd,cAAc,EACd,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"process.js","sourceRoot":"","sources":["../src/process.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAEL,iBAAiB,EACjB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAGL,qBAAqB,EACrB,YAAY,EACZ,cAAc,EACd,eAAe,EACf,cAAc,EACd,cAAc,EACd,kBAAkB,GACnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,UAAU,EACV,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
@@ -28,10 +28,16 @@ export declare function findConfigFile(startDir: string): string | null;
28
28
  */
29
29
  export declare function findNodeWorkspaceRoot(startDir: string): string | null;
30
30
  /**
31
- * Reset {@link findProjectRoot}'s module-level cache.
31
+ * Reset the module-level walk-up caches: {@link findProjectRoot}'s, and the git
32
+ * root memo behind `gitFindRoot`.
32
33
  *
33
34
  * Call at the start of each independent CLI invocation so in-process callers
34
35
  * (and integration tests sharing a vitest worker) don't observe stale results.
36
+ *
37
+ * Both caches are cleared by this one function on purpose. They memoize the same
38
+ * kind of fact — which ancestor governs a directory — and are invalidated by the
39
+ * same events, so a caller that had to remember two reset names would sooner or
40
+ * later remember only one.
35
41
  */
36
42
  export declare function resetProjectRootCaches(): void;
37
43
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"project-utils.d.ts","sourceRoot":"","sources":["../src/project-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAUH;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY9D;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAmBrE;AAgBD;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C;AAoED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK/D"}
1
+ {"version":3,"file":"project-utils.d.ts","sourceRoot":"","sources":["../src/project-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAW9D;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyBrE;AAgBD;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAG7C;AAkED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK/D"}
@@ -6,9 +6,12 @@
6
6
  * fallbacks — a caller with no root must decide one rather than silently
7
7
  * falling back to an absolute path.
8
8
  */
9
- import { existsSync, readFileSync } from 'node:fs';
9
+ import { existsSync } from 'node:fs';
10
10
  import { dirname, parse } from 'node:path';
11
+ import { isPathAbsentError } from './errors/errno.js';
12
+ import { resetGitRootCache } from './git-root-cache.js';
11
13
  import { safePath } from './path-utils.js';
14
+ import { readTextContentSync } from './text-file.js';
12
15
  const CONFIG_FILENAME = 'vibe-agent-toolkit.config.yaml';
13
16
  const PACKAGE_JSON_FILENAME = 'package.json';
14
17
  /**
@@ -25,7 +28,6 @@ export function findConfigFile(startDir) {
25
28
  const root = parse(current).root;
26
29
  while (true) {
27
30
  const candidate = safePath.join(current, CONFIG_FILENAME);
28
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- walk-up is intentional
29
31
  if (existsSync(candidate))
30
32
  return candidate;
31
33
  if (current === root)
@@ -50,17 +52,24 @@ export function findNodeWorkspaceRoot(startDir) {
50
52
  let current = safePath.resolve(startDir);
51
53
  while (current !== dirname(current)) {
52
54
  const pkgPath = safePath.join(current, PACKAGE_JSON_FILENAME);
53
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- walk-up is intentional
54
55
  if (existsSync(pkgPath)) {
55
56
  try {
56
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- walk-up is intentional
57
- const parsed = JSON.parse(readFileSync(pkgPath, 'utf-8'));
57
+ // Through the decoding seam: this is the ADOPTER's manifest, not ours,
58
+ // and a UTF-8 BOM in front of `{` makes `JSON.parse` throw — which the
59
+ // `catch` below would silently report as "not the workspace root".
60
+ const parsed = JSON.parse(readTextContentSync(pkgPath).text);
58
61
  if (typeof parsed === 'object' && parsed !== null && 'workspaces' in parsed) {
59
62
  return current;
60
63
  }
61
64
  }
62
- catch {
63
- // Invalid JSON — skip and continue walking up.
65
+ catch (error) {
66
+ // Not JSON: not a manifest this walk can read, so skip it and keep
67
+ // walking up. Gone between `existsSync` and the read counts the same. A
68
+ // manifest the OS refused (`EACCES`), or a directory named
69
+ // `package.json` (`EISDIR`), is neither — reading past it would make
70
+ // the walk settle on the WRONG root, silently.
71
+ if (!(error instanceof SyntaxError) && !isPathAbsentError(error))
72
+ throw error;
64
73
  }
65
74
  }
66
75
  current = dirname(current);
@@ -81,13 +90,20 @@ export function findNodeWorkspaceRoot(startDir) {
81
90
  */
82
91
  const walkUpCache = new Map();
83
92
  /**
84
- * Reset {@link findProjectRoot}'s module-level cache.
93
+ * Reset the module-level walk-up caches: {@link findProjectRoot}'s, and the git
94
+ * root memo behind `gitFindRoot`.
85
95
  *
86
96
  * Call at the start of each independent CLI invocation so in-process callers
87
97
  * (and integration tests sharing a vitest worker) don't observe stale results.
98
+ *
99
+ * Both caches are cleared by this one function on purpose. They memoize the same
100
+ * kind of fact — which ancestor governs a directory — and are invalidated by the
101
+ * same events, so a caller that had to remember two reset names would sooner or
102
+ * later remember only one.
88
103
  */
89
104
  export function resetProjectRootCaches() {
90
105
  walkUpCache.clear();
106
+ resetGitRootCache();
91
107
  }
92
108
  /** Write `entry` into walkUpCache for every dir in `visited`. */
93
109
  function propagateCache(visited, entry) {
@@ -115,7 +131,6 @@ function configWalkPhase(startDir, visited) {
115
131
  return { kind: 'found', configRoot: cached.configRoot };
116
132
  }
117
133
  visited.push(current);
118
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- walk-up is intentional
119
134
  if (existsSync(safePath.join(current, CONFIG_FILENAME))) {
120
135
  const entry = { configRoot: current };
121
136
  propagateCache(visited, entry);
@@ -135,7 +150,6 @@ function configWalkPhase(startDir, visited) {
135
150
  function gitWalkPhase(startDir, visited) {
136
151
  let current = safePath.resolve(startDir);
137
152
  while (true) {
138
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- walk-up is intentional
139
153
  if (existsSync(safePath.join(current, '.git'))) {
140
154
  const entry = { configRoot: current };
141
155
  propagateCache(visited, entry);