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

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 (330) hide show
  1. package/README.md +112 -10
  2. package/dist/archive/archive.d.ts +129 -0
  3. package/dist/archive/archive.d.ts.map +1 -0
  4. package/dist/archive/archive.js +223 -0
  5. package/dist/archive/archive.js.map +1 -0
  6. package/dist/archive/entries.d.ts +132 -0
  7. package/dist/archive/entries.d.ts.map +1 -0
  8. package/dist/archive/entries.js +223 -0
  9. package/dist/archive/entries.js.map +1 -0
  10. package/dist/archive/limits.d.ts +39 -0
  11. package/dist/archive/limits.d.ts.map +1 -0
  12. package/dist/archive/limits.js +59 -0
  13. package/dist/archive/limits.js.map +1 -0
  14. package/dist/archive/records.d.ts +14 -0
  15. package/dist/archive/records.d.ts.map +1 -0
  16. package/dist/archive/records.js +3 -0
  17. package/dist/archive/records.js.map +1 -0
  18. package/dist/archive/tar-reader.d.ts +18 -0
  19. package/dist/archive/tar-reader.d.ts.map +1 -0
  20. package/dist/archive/tar-reader.js +173 -0
  21. package/dist/archive/tar-reader.js.map +1 -0
  22. package/dist/archive/zip-reader.d.ts +76 -0
  23. package/dist/archive/zip-reader.d.ts.map +1 -0
  24. package/dist/archive/zip-reader.js +376 -0
  25. package/dist/archive/zip-reader.js.map +1 -0
  26. package/dist/archive.d.ts +14 -0
  27. package/dist/archive.d.ts.map +1 -0
  28. package/dist/archive.js +12 -0
  29. package/dist/archive.js.map +1 -0
  30. package/dist/asset-reference.d.ts +17 -1
  31. package/dist/asset-reference.d.ts.map +1 -1
  32. package/dist/asset-reference.js +31 -7
  33. package/dist/asset-reference.js.map +1 -1
  34. package/dist/dirent-kind.d.ts +13 -40
  35. package/dist/dirent-kind.d.ts.map +1 -1
  36. package/dist/dirent-kind.js +21 -69
  37. package/dist/dirent-kind.js.map +1 -1
  38. package/dist/entrypoint.d.ts +2 -2
  39. package/dist/entrypoint.d.ts.map +1 -1
  40. package/dist/entrypoint.js +4 -3
  41. package/dist/entrypoint.js.map +1 -1
  42. package/dist/env-flag.d.ts +13 -5
  43. package/dist/env-flag.d.ts.map +1 -1
  44. package/dist/env-flag.js +13 -5
  45. package/dist/env-flag.js.map +1 -1
  46. package/dist/errors/confirmed-absent.d.ts +41 -0
  47. package/dist/errors/confirmed-absent.d.ts.map +1 -0
  48. package/dist/errors/confirmed-absent.js +43 -0
  49. package/dist/errors/confirmed-absent.js.map +1 -0
  50. package/dist/errors/errno-table.d.ts +177 -0
  51. package/dist/errors/errno-table.d.ts.map +1 -0
  52. package/dist/errors/errno-table.js +343 -0
  53. package/dist/errors/errno-table.js.map +1 -0
  54. package/dist/errors/fs-boundary.d.ts +36 -0
  55. package/dist/errors/fs-boundary.d.ts.map +1 -0
  56. package/dist/errors/fs-boundary.js +121 -0
  57. package/dist/errors/fs-boundary.js.map +1 -0
  58. package/dist/errors/fs-fault.d.ts +176 -0
  59. package/dist/errors/fs-fault.d.ts.map +1 -0
  60. package/dist/errors/fs-fault.js +215 -0
  61. package/dist/errors/fs-fault.js.map +1 -0
  62. package/dist/errors/git-errno.d.ts +13 -0
  63. package/dist/errors/git-errno.d.ts.map +1 -0
  64. package/dist/errors/git-errno.js +45 -0
  65. package/dist/errors/git-errno.js.map +1 -0
  66. package/dist/errors/path-present.d.ts +72 -0
  67. package/dist/errors/path-present.d.ts.map +1 -0
  68. package/dist/errors/path-present.js +110 -0
  69. package/dist/errors/path-present.js.map +1 -0
  70. package/dist/errors/rag-index-empty.d.ts +16 -0
  71. package/dist/errors/rag-index-empty.d.ts.map +1 -0
  72. package/dist/errors/rag-index-empty.js +16 -0
  73. package/dist/errors/rag-index-empty.js.map +1 -0
  74. package/dist/errors/suppressed-faults.d.ts +34 -0
  75. package/dist/errors/suppressed-faults.d.ts.map +1 -0
  76. package/dist/errors/suppressed-faults.js +66 -0
  77. package/dist/errors/suppressed-faults.js.map +1 -0
  78. package/dist/file-crawler.d.ts +13 -18
  79. package/dist/file-crawler.d.ts.map +1 -1
  80. package/dist/file-crawler.js +140 -24
  81. package/dist/file-crawler.js.map +1 -1
  82. package/dist/fs-utils.d.ts +75 -50
  83. package/dist/fs-utils.d.ts.map +1 -1
  84. package/dist/fs-utils.js +148 -158
  85. package/dist/fs-utils.js.map +1 -1
  86. package/dist/fs.d.ts +5 -4
  87. package/dist/fs.d.ts.map +1 -1
  88. package/dist/fs.js +5 -4
  89. package/dist/fs.js.map +1 -1
  90. package/dist/git-snapshot.d.ts +25 -17
  91. package/dist/git-snapshot.d.ts.map +1 -1
  92. package/dist/git-snapshot.js +65 -11
  93. package/dist/git-snapshot.js.map +1 -1
  94. package/dist/git-tracker.d.ts +11 -3
  95. package/dist/git-tracker.d.ts.map +1 -1
  96. package/dist/git-tracker.js +29 -6
  97. package/dist/git-tracker.js.map +1 -1
  98. package/dist/git-utils.d.ts.map +1 -1
  99. package/dist/git-utils.js +7 -35
  100. package/dist/git-utils.js.map +1 -1
  101. package/dist/git.d.ts +1 -1
  102. package/dist/git.d.ts.map +1 -1
  103. package/dist/git.js +1 -1
  104. package/dist/git.js.map +1 -1
  105. package/dist/gitignore-checker.d.ts.map +1 -1
  106. package/dist/gitignore-checker.js +5 -4
  107. package/dist/gitignore-checker.js.map +1 -1
  108. package/dist/in-order.d.ts +93 -0
  109. package/dist/in-order.d.ts.map +1 -0
  110. package/dist/in-order.js +165 -0
  111. package/dist/in-order.js.map +1 -0
  112. package/dist/index.d.ts +20 -4
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +30 -4
  115. package/dist/index.js.map +1 -1
  116. package/dist/kill-process-tree.d.ts +43 -0
  117. package/dist/kill-process-tree.d.ts.map +1 -0
  118. package/dist/kill-process-tree.js +48 -0
  119. package/dist/kill-process-tree.js.map +1 -0
  120. package/dist/link-following.d.ts +115 -0
  121. package/dist/link-following.d.ts.map +1 -0
  122. package/dist/link-following.js +283 -0
  123. package/dist/link-following.js.map +1 -0
  124. package/dist/listing-refusal.d.ts +44 -0
  125. package/dist/listing-refusal.d.ts.map +1 -1
  126. package/dist/listing-refusal.js +79 -2
  127. package/dist/listing-refusal.js.map +1 -1
  128. package/dist/name-fold.d.ts +25 -0
  129. package/dist/name-fold.d.ts.map +1 -0
  130. package/dist/name-fold.js +40 -0
  131. package/dist/name-fold.js.map +1 -0
  132. package/dist/path-containment.d.ts +17 -0
  133. package/dist/path-containment.d.ts.map +1 -1
  134. package/dist/path-containment.js +28 -5
  135. package/dist/path-containment.js.map +1 -1
  136. package/dist/path-utils.d.ts +14 -17
  137. package/dist/path-utils.d.ts.map +1 -1
  138. package/dist/path-utils.js +37 -55
  139. package/dist/path-utils.js.map +1 -1
  140. package/dist/process.d.ts +2 -0
  141. package/dist/process.d.ts.map +1 -1
  142. package/dist/process.js +2 -0
  143. package/dist/process.js.map +1 -1
  144. package/dist/project-utils.d.ts.map +1 -1
  145. package/dist/project-utils.js +8 -9
  146. package/dist/project-utils.js.map +1 -1
  147. package/dist/safe-exec.d.ts +6 -2
  148. package/dist/safe-exec.d.ts.map +1 -1
  149. package/dist/safe-exec.js +1 -1
  150. package/dist/safe-exec.js.map +1 -1
  151. package/dist/skill-targets.d.ts.map +1 -1
  152. package/dist/skill-targets.js +3 -1
  153. package/dist/skill-targets.js.map +1 -1
  154. package/dist/skill-test/spawn-claude.d.ts +0 -15
  155. package/dist/skill-test/spawn-claude.d.ts.map +1 -1
  156. package/dist/skill-test/spawn-claude.js +11 -50
  157. package/dist/skill-test/spawn-claude.js.map +1 -1
  158. package/dist/special-file.d.ts +18 -0
  159. package/dist/special-file.d.ts.map +1 -0
  160. package/dist/special-file.js +21 -0
  161. package/dist/special-file.js.map +1 -0
  162. package/dist/terminating-signals.d.ts +129 -0
  163. package/dist/terminating-signals.d.ts.map +1 -0
  164. package/dist/terminating-signals.js +210 -0
  165. package/dist/terminating-signals.js.map +1 -0
  166. package/dist/test-helpers.d.ts +9 -5
  167. package/dist/test-helpers.d.ts.map +1 -1
  168. package/dist/test-helpers.js +18 -21
  169. package/dist/test-helpers.js.map +1 -1
  170. package/dist/test-scratch-guard.d.ts +34 -0
  171. package/dist/test-scratch-guard.d.ts.map +1 -0
  172. package/dist/test-scratch-guard.js +45 -0
  173. package/dist/test-scratch-guard.js.map +1 -0
  174. package/dist/testing/archive-fixtures.d.ts +147 -0
  175. package/dist/testing/archive-fixtures.d.ts.map +1 -0
  176. package/dist/testing/archive-fixtures.js +449 -0
  177. package/dist/testing/archive-fixtures.js.map +1 -0
  178. package/dist/testing/executables.d.ts +6 -0
  179. package/dist/testing/executables.d.ts.map +1 -1
  180. package/dist/testing/executables.js +20 -9
  181. package/dist/testing/executables.js.map +1 -1
  182. package/dist/testing/fault-fs-preload.d.ts +16 -0
  183. package/dist/testing/fault-fs-preload.d.ts.map +1 -0
  184. package/dist/testing/fault-fs-preload.js +22 -0
  185. package/dist/testing/fault-fs-preload.js.map +1 -0
  186. package/dist/testing/fault-fs.d.ts +115 -0
  187. package/dist/testing/fault-fs.d.ts.map +1 -0
  188. package/dist/testing/fault-fs.js +411 -0
  189. package/dist/testing/fault-fs.js.map +1 -0
  190. package/dist/testing/fault-spec.d.ts +83 -0
  191. package/dist/testing/fault-spec.d.ts.map +1 -0
  192. package/dist/testing/fault-spec.js +132 -0
  193. package/dist/testing/fault-spec.js.map +1 -0
  194. package/dist/testing/hostile-tree.d.ts.map +1 -1
  195. package/dist/testing/hostile-tree.js +3 -4
  196. package/dist/testing/hostile-tree.js.map +1 -1
  197. package/dist/testing/platform-gates.d.ts +8 -0
  198. package/dist/testing/platform-gates.d.ts.map +1 -1
  199. package/dist/testing/platform-gates.js +24 -0
  200. package/dist/testing/platform-gates.js.map +1 -1
  201. package/dist/testing/temp-dir.d.ts +31 -0
  202. package/dist/testing/temp-dir.d.ts.map +1 -1
  203. package/dist/testing/temp-dir.js +63 -0
  204. package/dist/testing/temp-dir.js.map +1 -1
  205. package/dist/testing/tier-timeout.d.ts +34 -0
  206. package/dist/testing/tier-timeout.d.ts.map +1 -0
  207. package/dist/testing/tier-timeout.js +45 -0
  208. package/dist/testing/tier-timeout.js.map +1 -0
  209. package/dist/testing/tree-snapshot.d.ts +71 -0
  210. package/dist/testing/tree-snapshot.d.ts.map +1 -0
  211. package/dist/testing/tree-snapshot.js +163 -0
  212. package/dist/testing/tree-snapshot.js.map +1 -0
  213. package/dist/testing.d.ts +6 -0
  214. package/dist/testing.d.ts.map +1 -1
  215. package/dist/testing.js +8 -1
  216. package/dist/testing.js.map +1 -1
  217. package/dist/text-content.d.ts +29 -0
  218. package/dist/text-content.d.ts.map +1 -1
  219. package/dist/text-content.js +64 -0
  220. package/dist/text-content.js.map +1 -1
  221. package/dist/text-file.d.ts +58 -6
  222. package/dist/text-file.d.ts.map +1 -1
  223. package/dist/text-file.js +111 -11
  224. package/dist/text-file.js.map +1 -1
  225. package/dist/text.d.ts +1 -1
  226. package/dist/text.d.ts.map +1 -1
  227. package/dist/text.js +1 -1
  228. package/dist/text.js.map +1 -1
  229. package/dist/timing-dump.d.ts.map +1 -1
  230. package/dist/timing-dump.js +2 -10
  231. package/dist/timing-dump.js.map +1 -1
  232. package/dist/tree-change/apply-decisions.d.ts +101 -0
  233. package/dist/tree-change/apply-decisions.d.ts.map +1 -0
  234. package/dist/tree-change/apply-decisions.js +139 -0
  235. package/dist/tree-change/apply-decisions.js.map +1 -0
  236. package/dist/tree-change/apply.d.ts +98 -0
  237. package/dist/tree-change/apply.d.ts.map +1 -0
  238. package/dist/tree-change/apply.js +364 -0
  239. package/dist/tree-change/apply.js.map +1 -0
  240. package/dist/tree-change/copy-decisions.d.ts +58 -0
  241. package/dist/tree-change/copy-decisions.d.ts.map +1 -0
  242. package/dist/tree-change/copy-decisions.js +66 -0
  243. package/dist/tree-change/copy-decisions.js.map +1 -0
  244. package/dist/tree-change/copy-tree.d.ts +125 -0
  245. package/dist/tree-change/copy-tree.d.ts.map +1 -0
  246. package/dist/tree-change/copy-tree.js +279 -0
  247. package/dist/tree-change/copy-tree.js.map +1 -0
  248. package/dist/tree-change/files.d.ts +167 -0
  249. package/dist/tree-change/files.d.ts.map +1 -0
  250. package/dist/tree-change/files.js +423 -0
  251. package/dist/tree-change/files.js.map +1 -0
  252. package/dist/tree-change/followed-walk.d.ts +54 -0
  253. package/dist/tree-change/followed-walk.d.ts.map +1 -0
  254. package/dist/tree-change/followed-walk.js +81 -0
  255. package/dist/tree-change/followed-walk.js.map +1 -0
  256. package/dist/tree-change/host-chain.d.ts +48 -0
  257. package/dist/tree-change/host-chain.d.ts.map +1 -0
  258. package/dist/tree-change/host-chain.js +85 -0
  259. package/dist/tree-change/host-chain.js.map +1 -0
  260. package/dist/tree-change/identity-compare.d.ts +61 -0
  261. package/dist/tree-change/identity-compare.d.ts.map +1 -0
  262. package/dist/tree-change/identity-compare.js +115 -0
  263. package/dist/tree-change/identity-compare.js.map +1 -0
  264. package/dist/tree-change/identity.d.ts +62 -0
  265. package/dist/tree-change/identity.d.ts.map +1 -0
  266. package/dist/tree-change/identity.js +152 -0
  267. package/dist/tree-change/identity.js.map +1 -0
  268. package/dist/tree-change/index.d.ts +24 -0
  269. package/dist/tree-change/index.d.ts.map +1 -0
  270. package/dist/tree-change/index.js +14 -0
  271. package/dist/tree-change/index.js.map +1 -0
  272. package/dist/tree-change/link-chain.d.ts +180 -0
  273. package/dist/tree-change/link-chain.d.ts.map +1 -0
  274. package/dist/tree-change/link-chain.js +377 -0
  275. package/dist/tree-change/link-chain.js.map +1 -0
  276. package/dist/tree-change/link-containment.d.ts +105 -0
  277. package/dist/tree-change/link-containment.d.ts.map +1 -0
  278. package/dist/tree-change/link-containment.js +196 -0
  279. package/dist/tree-change/link-containment.js.map +1 -0
  280. package/dist/tree-change/link-walk.d.ts +49 -0
  281. package/dist/tree-change/link-walk.d.ts.map +1 -0
  282. package/dist/tree-change/link-walk.js +69 -0
  283. package/dist/tree-change/link-walk.js.map +1 -0
  284. package/dist/tree-change/plan.d.ts +206 -0
  285. package/dist/tree-change/plan.d.ts.map +1 -0
  286. package/dist/tree-change/plan.js +320 -0
  287. package/dist/tree-change/plan.js.map +1 -0
  288. package/dist/tree-change/readable-tree.d.ts +44 -0
  289. package/dist/tree-change/readable-tree.d.ts.map +1 -0
  290. package/dist/tree-change/readable-tree.js +52 -0
  291. package/dist/tree-change/readable-tree.js.map +1 -0
  292. package/dist/tree-change/rollback-error.d.ts +29 -0
  293. package/dist/tree-change/rollback-error.d.ts.map +1 -0
  294. package/dist/tree-change/rollback-error.js +31 -0
  295. package/dist/tree-change/rollback-error.js.map +1 -0
  296. package/dist/tree-change/staging-names.d.ts +27 -0
  297. package/dist/tree-change/staging-names.d.ts.map +1 -0
  298. package/dist/tree-change/staging-names.js +40 -0
  299. package/dist/tree-change/staging-names.js.map +1 -0
  300. package/dist/tree-change/tree-walk.d.ts +81 -0
  301. package/dist/tree-change/tree-walk.d.ts.map +1 -0
  302. package/dist/tree-change/tree-walk.js +212 -0
  303. package/dist/tree-change/tree-walk.js.map +1 -0
  304. package/dist/win32-namespace.d.ts +24 -0
  305. package/dist/win32-namespace.d.ts.map +1 -0
  306. package/dist/win32-namespace.js +31 -0
  307. package/dist/win32-namespace.js.map +1 -0
  308. package/dist/yaml/surgical-yaml.d.ts +12 -3
  309. package/dist/yaml/surgical-yaml.d.ts.map +1 -1
  310. package/dist/yaml/surgical-yaml.js +16 -6
  311. package/dist/yaml/surgical-yaml.js.map +1 -1
  312. package/eslint/README.md +12 -4
  313. package/eslint/rules/fs-binding-scan.cjs +278 -0
  314. package/eslint/rules/no-adhoc-errno.cjs +364 -0
  315. package/eslint/rules/no-bare-symlink-in-tests.cjs +2 -3
  316. package/eslint/rules/no-blind-catch.cjs +1 -1
  317. package/eslint/rules/no-destructive-fs.cjs +359 -0
  318. package/eslint/rules/no-existssync.cjs +70 -0
  319. package/eslint/rules/no-fragile-entrypoint-guard.cjs +3 -2
  320. package/eslint/rules/no-link-following-fs.cjs +68 -0
  321. package/eslint/rules/no-literal-process-exit.cjs +96 -8
  322. package/eslint/rules/no-literal-test-timeout.cjs +214 -0
  323. package/eslint/rules/no-raw-signal-listener.cjs +224 -0
  324. package/eslint/rules/no-stdout-outside-writer.cjs +135 -0
  325. package/package.json +10 -2
  326. package/dist/errors/errno.d.ts +0 -51
  327. package/dist/errors/errno.d.ts.map +0 -1
  328. package/dist/errors/errno.js +0 -112
  329. package/dist/errors/errno.js.map +0 -1
  330. package/eslint/rules/no-fs-promises-cp.cjs +0 -43
@@ -0,0 +1,214 @@
1
+ /**
2
+ * ESLint rule: no-literal-test-timeout
3
+ *
4
+ * Flags a per-test, per-suite or per-hook timeout written as a number:
5
+ * `it(name, fn, 60_000)`, `beforeAll(fn, 3_600_000)`, `it(name, { timeout: 30_000 }, fn)`.
6
+ * A test runner's tier config scales its default to the platform — this repo's
7
+ * integration tier allows 15 minutes per test on Windows and 1 on Unix — and a
8
+ * literal at the call site replaces that with one number for every machine. So
9
+ * the literal that is generous on a fast runner is the timeout on a slow one: a
10
+ * `60_000` that was 1× the Unix default was 1/15 of the Windows one, and failed
11
+ * there on runner speed alone, with no code change.
12
+ *
13
+ * A multiple of the default (`inject('tierTestTimeout') * 60`) fails the other
14
+ * way: it scales an hour-sized Unix budget by the Windows factor too — 15 hours —
15
+ * so a hang burns the CI job's whole allowance and names no test.
16
+ *
17
+ * The allowed shapes: omit it (the tier default applies), pass the tier's own
18
+ * value (`inject('tierTestTimeout')`, `vitest.shared.ts`), or ask for a budget
19
+ * over it with `atLeastTierTimeout(inject('tierTestTimeout'), 3_600_000)` from
20
+ * `@vibe-agent-toolkit/utils/testing` — the default is the floor and the budget
21
+ * a ceiling over it. A number is accepted ONLY as that helper's budget; its first
22
+ * argument (the floor) is walked like any other timeout.
23
+ *
24
+ * A literal is found anywhere in the timeout expression — arithmetic, either arm
25
+ * of a conditional, another call's arguments, a cast or parentheses — and behind
26
+ * a same-file `const`. A parameter, an import, a member read or a call result
27
+ * with no literal argument is accepted.
28
+ *
29
+ * ⚠️ STILL BLIND (syntax-only rule): a literal behind an IMPORTED constant, a
30
+ * `let`, an object property, or an options object held in a `const`. There is
31
+ * deliberately no escape-hatch comment: a hang guard that must stay short is a
32
+ * property of the code under test, and belongs in that code's own timeout (a
33
+ * spawn's `timeout`, an `AbortSignal`), not in the runner's wall clock.
34
+ *
35
+ * Not in `recommended`: the remedy names a value the adopter's runner config
36
+ * must provide, which this pack cannot supply.
37
+ *
38
+ * @example
39
+ * // BAD — one number for every machine
40
+ * it('populates the corpus', async () => { ... }, 60_000);
41
+ *
42
+ * // GOOD — the tier default
43
+ * it('populates the corpus', async () => { ... });
44
+ *
45
+ * // GOOD — a measured budget, never below this machine's allowance
46
+ * beforeAll(async () => { ... }, atLeastTierTimeout(inject('tierHookTimeout'), 3_600_000));
47
+ */
48
+
49
+ 'use strict';
50
+
51
+ /** Runner globals whose call takes a timeout. */
52
+ const TIMED_ROOTS = new Set(['it', 'test', 'describe', 'suite']);
53
+ const HOOK_ROOTS = new Set(['beforeAll', 'afterAll', 'beforeEach', 'afterEach']);
54
+
55
+ /** Members that return a test function after taking a table or a condition. */
56
+ const FACTORY_MEMBERS = new Set(['each', 'for', 'skipIf', 'runIf']);
57
+
58
+ /** Members that ARE a test function. */
59
+ const MODIFIER_MEMBERS = new Set(['concurrent', 'sequential', 'only', 'skip', 'fails', 'todo', 'shuffle']);
60
+
61
+ /** Wrappers that change a value's type or grouping, not its value. */
62
+ const TRANSPARENT = new Set(['TSAsExpression', 'TSSatisfiesExpression', 'TSNonNullExpression', 'ChainExpression']);
63
+
64
+ function unwrap(node) {
65
+ let current = node;
66
+ while (current && TRANSPARENT.has(current.type)) current = current.expression;
67
+ return current;
68
+ }
69
+
70
+ /**
71
+ * The root runner name a callee calls as a test function, or null.
72
+ *
73
+ * `it`, `it.concurrent`, `it.skipIf(c)`, `it.each(t)`, `it.each\`…\`` all qualify; `it.each`
74
+ * itself (the table call) does not, because its arguments are not a timeout.
75
+ */
76
+ function testFunctionRoot(callee) {
77
+ const node = unwrap(callee);
78
+ if (!node) return null;
79
+ if (node.type === 'Identifier') return TIMED_ROOTS.has(node.name) || HOOK_ROOTS.has(node.name) ? node.name : null;
80
+ if (node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier'
81
+ && MODIFIER_MEMBERS.has(node.property.name)) {
82
+ return testFunctionRoot(node.object);
83
+ }
84
+ if (node.type === 'CallExpression' || node.type === 'TaggedTemplateExpression') {
85
+ const factory = unwrap(node.type === 'CallExpression' ? node.callee : node.tag);
86
+ if (factory?.type === 'MemberExpression' && !factory.computed
87
+ && factory.property.type === 'Identifier' && FACTORY_MEMBERS.has(factory.property.name)) {
88
+ return testFunctionRoot(factory.object);
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+
94
+ /** The `const` initializer an identifier names in this file, or null. */
95
+ function constInitializer(sourceCode, identifier) {
96
+ let scope = sourceCode.getScope(identifier);
97
+ while (scope) {
98
+ const variable = scope.set.get(identifier.name);
99
+ if (variable) {
100
+ const definition = variable.defs[0];
101
+ if (definition?.type === 'Variable' && definition.parent.kind === 'const'
102
+ && definition.node.id.type === 'Identifier') {
103
+ return definition.node.init;
104
+ }
105
+ return null;
106
+ }
107
+ scope = scope.upper;
108
+ }
109
+ return null;
110
+ }
111
+
112
+ /** The one call whose numeric argument is accepted: its budget, over the tier's floor. */
113
+ const BUDGET_HELPER = 'atLeastTierTimeout';
114
+
115
+ function isBudgetHelper(callee) {
116
+ const node = unwrap(callee);
117
+ if (!node) return false;
118
+ if (node.type === 'Identifier') return node.name === BUDGET_HELPER;
119
+ return node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier'
120
+ && node.property.name === BUDGET_HELPER;
121
+ }
122
+
123
+ /**
124
+ * The first numeric literal a timeout expression decides its value with, or null.
125
+ *
126
+ * Walks arithmetic, both arms of a conditional (not its test), call arguments and
127
+ * same-file `const` initializers. The budget helper contributes only its first
128
+ * argument: its second is the one place a number is the contract.
129
+ */
130
+ function findLiteral(sourceCode, node, seen) {
131
+ const value = unwrap(node);
132
+ if (!value) return null;
133
+ switch (value.type) {
134
+ case 'Literal':
135
+ return typeof value.value === 'number' ? value : null;
136
+ case 'UnaryExpression':
137
+ return findLiteral(sourceCode, value.argument, seen);
138
+ case 'BinaryExpression':
139
+ case 'LogicalExpression':
140
+ return findLiteral(sourceCode, value.left, seen) ?? findLiteral(sourceCode, value.right, seen);
141
+ case 'ConditionalExpression':
142
+ return findLiteral(sourceCode, value.consequent, seen) ?? findLiteral(sourceCode, value.alternate, seen);
143
+ case 'CallExpression': {
144
+ const walked = isBudgetHelper(value.callee) ? value.arguments.slice(0, 1) : value.arguments;
145
+ for (const argument of walked) {
146
+ const found = findLiteral(sourceCode, argument, seen);
147
+ if (found !== null) return found;
148
+ }
149
+ return null;
150
+ }
151
+ case 'Identifier': {
152
+ if (seen.has(value.name)) return null;
153
+ seen.add(value.name);
154
+ const init = constInitializer(sourceCode, value);
155
+ return init === null ? null : findLiteral(sourceCode, init, seen);
156
+ }
157
+ default:
158
+ // A function (the test body), a member read, a template: not a number decided here.
159
+ return null;
160
+ }
161
+ }
162
+
163
+ /** The literal a timeout argument carries — in itself, or in its `timeout` property. */
164
+ function literalTimeoutIn(sourceCode, argument) {
165
+ const node = unwrap(argument);
166
+ if (!node) return null;
167
+ if (node.type !== 'ObjectExpression') return findLiteral(sourceCode, node, new Set());
168
+ for (const property of node.properties) {
169
+ if (property.type === 'Property' && !property.computed && property.key.type === 'Identifier'
170
+ && property.key.name === 'timeout') {
171
+ return findLiteral(sourceCode, property.value, new Set());
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+
177
+ module.exports = {
178
+ meta: {
179
+ type: 'problem',
180
+ docs: {
181
+ description:
182
+ 'Disallow a numeric per-test or per-hook timeout — omit it, or pass a budget through ' +
183
+ 'atLeastTierTimeout(tier, budget), so the platform-scaled default is the floor and the budget a ceiling',
184
+ category: 'Code and test hygiene',
185
+ bans: '`it(name, fn, 60_000)`, `beforeAll(fn, tier * 12)`, `{ timeout: isWin ? 900_000 : 60_000 }`',
186
+ useInstead: 'the tier default, or `atLeastTierTimeout(inject(\'tierTestTimeout\'), budget)`',
187
+ // Not recommended: the remedy names a value the adopter's runner config must provide.
188
+ recommended: false,
189
+ },
190
+ schema: [],
191
+ messages: {
192
+ literalTimeout:
193
+ 'A number in a timeout replaces or scales the tier\'s platform-scaled default. Omit it, or write ' +
194
+ 'atLeastTierTimeout(inject(\'tierTestTimeout\' | \'tierHookTimeout\'), budget).',
195
+ },
196
+ },
197
+
198
+ create(context) {
199
+ const sourceCode = context.sourceCode;
200
+ return {
201
+ CallExpression(node) {
202
+ const root = testFunctionRoot(node.callee);
203
+ if (root === null) return;
204
+ // A hook's timeout is its second argument; a test's or suite's is any
205
+ // argument after the name (vitest takes options before or after the body).
206
+ const candidates = HOOK_ROOTS.has(root) ? node.arguments.slice(1, 2) : node.arguments.slice(1);
207
+ for (const argument of candidates) {
208
+ const literal = literalTimeoutIn(sourceCode, argument);
209
+ if (literal !== null) context.report({ node: literal, messageId: 'literalTimeout' });
210
+ }
211
+ },
212
+ };
213
+ },
214
+ };
@@ -0,0 +1,224 @@
1
+ /**
2
+ * ESLint rule: no-raw-signal-listener
3
+ *
4
+ * Flags every way of adding — or removing — a listener for a SIGNAL on the process by hand. The
5
+ * event is a signal when it is a string literal (or template) naming one, or any event name that
6
+ * is not a literal at all (a loop variable over a signal list, a parameter). Caught shapes:
7
+ *
8
+ * - `process.on` / `once` / `addListener` / `prependListener` / `prependOnceListener` — and
9
+ * `off` / `removeListener` / `removeAllListeners`, which can strip a guard (`removeAllListeners()`
10
+ * with no event strips every one);
11
+ * - the same method reached as `process['on']`, through `globalThis.process` / `global.process`,
12
+ * through a local alias (`const p = process`), through a `node:process` (or `process`) default
13
+ * or namespace import, or as a named import of the method from it (`import { on } from
14
+ * 'node:process'`);
15
+ * - `once(process, <signal>)` / `on(process, <signal>)` — `node:events`' promise and iterator
16
+ * forms, called bare or as a member (`events.once`, `EventEmitter.on`) — with any of the process
17
+ * references above as the emitter.
18
+ *
19
+ * NOT caught: a process reference that only reaches the call through a function's parameter, an
20
+ * object property, or any value the rule cannot follow to its declaration in the same file.
21
+ *
22
+ * Why: a terminating-signal handler written by hand is the shape that leaked a clone and a lock.
23
+ * It removed itself on the first signal and started an asynchronous cleanup, so the second — a
24
+ * terminal Ctrl-C under a wrapper that forwards signals delivers two — met the default disposition
25
+ * and killed the process half-way through the cleanup. `handleTerminatingSignals` (on
26
+ * `@vibe-agent-toolkit/utils/process`) is the one installation: it swallows the duplicate that
27
+ * arrives with the first signal, composes with every other live guard, and dies of the signal. A
28
+ * removal by hand can strip that guard's listener out from under it.
29
+ *
30
+ * An event name that is not a literal is flagged because a signal list spread into a loop is how
31
+ * the second-commonest hand-written handler looks, and nothing at the call site says whether the
32
+ * variable holds a signal. Name the event literally when it is not one.
33
+ *
34
+ * Option `allow: string[]` — repo-relative paths of the files permitted to listen directly (the one
35
+ * file that implements the helper). Matched at a path-segment boundary via `exempt-path-matcher.cjs`.
36
+ *
37
+ * @example
38
+ * // BAD
39
+ * process.on('SIGINT', () => cleanup().then(() => process.exit(130)));
40
+ * for (const signal of TERMINATING_SIGNALS) process.on(signal, onSignal);
41
+ * await once(process, 'SIGTERM');
42
+ *
43
+ * // GOOD
44
+ * const guard = handleTerminatingSignals({ cleanup, fallbackExitCode: ExitCode.ERROR });
45
+ * // ...
46
+ * guard.release();
47
+ */
48
+
49
+ 'use strict';
50
+
51
+ const { createExemptPathMatcher } = require('./exempt-path-matcher.cjs');
52
+
53
+ const ADD_METHODS = new Set(['on', 'once', 'addListener', 'prependListener', 'prependOnceListener']);
54
+ const REMOVE_METHODS = new Set(['off', 'removeListener', 'removeAllListeners']);
55
+ /** `node:events`' static forms that take the emitter as their first argument. */
56
+ const EVENTS_STATIC_METHODS = new Set(['on', 'once']);
57
+ const PROCESS_MODULES = new Set(['process', 'node:process']);
58
+ const GLOBAL_OBJECTS = new Set(['globalThis', 'global']);
59
+ /** How many aliases deep a process reference is followed — `const a = process; const b = a;`. */
60
+ const MAX_ALIAS_DEPTH = 8;
61
+
62
+ /** The member name of `node` — `x.name` or `x['name']` — or null when it is not static. */
63
+ function memberName(node) {
64
+ if (node.type !== 'MemberExpression') return null;
65
+ if (!node.computed && node.property.type === 'Identifier') return node.property.name;
66
+ if (node.computed && node.property.type === 'Literal' && typeof node.property.value === 'string') {
67
+ return node.property.value;
68
+ }
69
+ return null;
70
+ }
71
+
72
+ /** The variable `identifier` refers to, through the scope chain, or null for an unresolved global. */
73
+ function resolveVariable(sourceCode, identifier) {
74
+ let scope = sourceCode.getScope(identifier);
75
+ while (scope) {
76
+ const variable = scope.set.get(identifier.name);
77
+ if (variable) return variable;
78
+ scope = scope.upper;
79
+ }
80
+ return null;
81
+ }
82
+
83
+ /** The import a variable is bound to: `{ module, imported }`, imported being 'default', '*' or a name. */
84
+ function importBinding(variable) {
85
+ const def = variable?.defs?.[0];
86
+ if (def?.type !== 'ImportBinding') return null;
87
+ switch (def.node.type) {
88
+ case 'ImportNamespaceSpecifier':
89
+ return { module: def.parent?.source?.value, imported: '*' };
90
+ case 'ImportDefaultSpecifier':
91
+ return { module: def.parent?.source?.value, imported: 'default' };
92
+ case 'ImportSpecifier': {
93
+ const { imported } = def.node;
94
+ return { module: def.parent?.source?.value, imported: imported.type === 'Identifier' ? imported.name : imported.value };
95
+ }
96
+ // `import p = require('node:process')` — the module object itself.
97
+ case 'TSImportEqualsDeclaration':
98
+ return { module: def.node.moduleReference?.expression?.value, imported: 'default' };
99
+ default:
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /** Whether `node` evaluates to the `process` object, as far as this file can tell. */
105
+ function isProcessRef(sourceCode, node, depth = 0) {
106
+ if (depth > MAX_ALIAS_DEPTH) return false;
107
+ if (node.type === 'MemberExpression') {
108
+ return node.object.type === 'Identifier' && GLOBAL_OBJECTS.has(node.object.name) && memberName(node) === 'process';
109
+ }
110
+ if (node.type !== 'Identifier') return false;
111
+ const variable = resolveVariable(sourceCode, node);
112
+ if (variable === null || variable.defs.length === 0) return node.name === 'process';
113
+ const binding = importBinding(variable);
114
+ if (binding !== null) return PROCESS_MODULES.has(binding.module) && (binding.imported === 'default' || binding.imported === '*');
115
+ const def = variable.defs[0];
116
+ if (def.type !== 'Variable' || def.node.id.type !== 'Identifier' || def.node.init == null) return false;
117
+ return isProcessRef(sourceCode, def.node.init, depth + 1);
118
+ }
119
+
120
+ /** The process method `callee` names — `process.on`, `p['once']`, an imported `on` — or null. */
121
+ function processMethod(sourceCode, callee) {
122
+ const name = memberName(callee);
123
+ if (name !== null) return isProcessRef(sourceCode, callee.object) ? name : null;
124
+ if (callee.type !== 'Identifier') return null;
125
+ const binding = importBinding(resolveVariable(sourceCode, callee));
126
+ return binding !== null && PROCESS_MODULES.has(binding.module) ? binding.imported : null;
127
+ }
128
+
129
+ /** The literal text of an event-name argument, or null when it is not literal. */
130
+ function literalEventName(arg) {
131
+ if (arg.type === 'Literal' && typeof arg.value === 'string') return arg.value;
132
+ if (arg.type === 'TemplateLiteral' && arg.expressions.length === 0) return arg.quasis[0].value.cooked;
133
+ if (arg.type === 'TemplateLiteral') return arg.quasis[0].value.cooked.startsWith('SIG') ? 'SIG' : null;
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * What `node` does to a signal listener — `{ method, eventArg, removal }` — or null when it is
139
+ * not a listener call on the process at all.
140
+ */
141
+ function listenerCall(sourceCode, node) {
142
+ const { callee } = node;
143
+ const method = processMethod(sourceCode, callee);
144
+ if (method !== null && (ADD_METHODS.has(method) || REMOVE_METHODS.has(method))) {
145
+ return { method, eventArg: node.arguments[0], removal: REMOVE_METHODS.has(method) };
146
+ }
147
+ const staticName = callee.type === 'Identifier' ? callee.name : memberName(callee);
148
+ const [emitter, eventArg] = node.arguments;
149
+ if (staticName !== null && EVENTS_STATIC_METHODS.has(staticName) && emitter !== undefined && isProcessRef(sourceCode, emitter)) {
150
+ return { method: staticName, eventArg, removal: false };
151
+ }
152
+ return null;
153
+ }
154
+
155
+ module.exports = {
156
+ meta: {
157
+ type: 'problem',
158
+ docs: {
159
+ description:
160
+ 'Disallow adding or removing a process listener for a signal (or for any event not named ' +
161
+ 'literally) by hand — install a terminating-signal handler through handleTerminatingSignals',
162
+ category: 'Filesystem and process',
163
+ bans: '`process.on(<signal>, …)`, `process.off(<signal>, …)`, `once(process, <signal>)` and `process.on(<non-literal>, …)`',
164
+ useInstead: '`handleTerminatingSignals()`',
165
+ subpath: '/process',
166
+ // Not recommended: it names a helper an adopter may not use, and the one file allowed to
167
+ // listen directly is a fact about each repo's layout (the `allow` option).
168
+ recommended: false,
169
+ recommendedSeverity: 'error',
170
+ },
171
+ schema: [
172
+ {
173
+ type: 'object',
174
+ properties: {
175
+ allow: { type: 'array', items: { type: 'string' }, uniqueItems: true },
176
+ },
177
+ additionalProperties: false,
178
+ },
179
+ ],
180
+ messages: {
181
+ signalListener:
182
+ 'process.{{method}}(\'{{event}}\') installs a signal handler by hand. Use ' +
183
+ 'handleTerminatingSignals from @vibe-agent-toolkit/utils/process: it swallows the ' +
184
+ 'duplicate that arrives with the first signal, composes with every other guard, and dies ' +
185
+ 'of the signal.',
186
+ computedEvent:
187
+ 'process.{{method}}(<non-literal>) may be a signal handler installed by hand. Name the ' +
188
+ 'event literally, or — for a signal — use handleTerminatingSignals from ' +
189
+ '@vibe-agent-toolkit/utils/process.',
190
+ signalRemoval:
191
+ 'process.{{method}}({{event}}) removes a signal listener by hand, which can strip a ' +
192
+ 'handleTerminatingSignals guard out from under it. Release the guard instead.',
193
+ },
194
+ },
195
+
196
+ create(context) {
197
+ const allow = context.options?.[0]?.allow ?? [];
198
+ const filename = context.filename ?? context.getFilename();
199
+ if (createExemptPathMatcher(allow)(filename)) return {};
200
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
201
+
202
+ return {
203
+ CallExpression(node) {
204
+ const call = listenerCall(sourceCode, node);
205
+ if (call === null) return;
206
+ const { method, eventArg, removal } = call;
207
+ if (eventArg === undefined) {
208
+ // `removeAllListeners()` strips every listener, every guard's included.
209
+ if (method === 'removeAllListeners') context.report({ node, messageId: 'signalRemoval', data: { method, event: '' } });
210
+ return;
211
+ }
212
+ const event = literalEventName(eventArg);
213
+ if (event !== null && !event.startsWith('SIG')) return;
214
+ if (removal) {
215
+ context.report({ node, messageId: 'signalRemoval', data: { method, event: event === null ? '<non-literal>' : `'${event}'` } });
216
+ } else if (event === null) {
217
+ context.report({ node, messageId: 'computedEvent', data: { method } });
218
+ } else {
219
+ context.report({ node, messageId: 'signalListener', data: { method, event } });
220
+ }
221
+ },
222
+ };
223
+ },
224
+ };
@@ -0,0 +1,135 @@
1
+ /**
2
+ * ESLint rule: no-stdout-outside-writer
3
+ *
4
+ * Under the configured `paths` (a CLI's command directory), stdout belongs to
5
+ * ONE writer. A command that writes its own bytes publishes a shape nothing
6
+ * validates, in a format it chose, beside an exit code it decided — which is
7
+ * how a CLI grows one document contract per verb. The writer validates the
8
+ * document against its registered schema before a byte leaves and derives the
9
+ * exit code from what it wrote; this rule makes it the only way out.
10
+ *
11
+ * Reported under `paths` (test files excepted):
12
+ *
13
+ * - `process.stdout.write(…)`;
14
+ * - `console.log|info|debug|dir|table(…)` — the console methods that write
15
+ * stdout (`console.error` / `console.warn` are stderr, the human channel,
16
+ * and are not this rule's business);
17
+ * - a call to a stdout-writing helper BY NAME — `writeStdoutSync`,
18
+ * `writeAllSync`, bare or as a member — and one HANDED ON as an argument
19
+ * (`render(result, writeStdoutSync)`), which writes stdout just the
20
+ * same. A rule that saw only the primitives was blind to every command that
21
+ * writes through a helper.
22
+ *
23
+ * Option `allowFiles: string[]` — repo-relative files whose stdout is not a
24
+ * document (a protocol leaf). Asserted both ways: a listed file with no
25
+ * violation left is itself an error (`staleAllow`), so the list never holds a
26
+ * dead entry.
27
+ *
28
+ * @example
29
+ * // BAD — a shape nothing validates, beside a code decided here
30
+ * writeStdoutSync(yaml.stringify(result));
31
+ * process.stdout.write(`${JSON.stringify(result)}\n`);
32
+ *
33
+ * // GOOD — validated against the registry, exit derived from what was written
34
+ * endWithReport('okf validate', report, format);
35
+ */
36
+
37
+ 'use strict';
38
+
39
+ const { createExemptDirectoryMatcher, createExemptPathMatcher, isTestFile } = require('./exempt-path-matcher.cjs');
40
+
41
+ /** The `console` methods that write stdout. */
42
+ const STDOUT_CONSOLE_METHODS = new Set(['log', 'info', 'debug', 'dir', 'table']);
43
+
44
+ /** Helpers that write stdout, recognised by name wherever they are imported from. */
45
+ const STDOUT_HELPERS = new Set([
46
+ 'writeStdoutSync',
47
+ 'writeAllSync',
48
+ ]);
49
+
50
+ /** Whether `node` is `<object>.<property>` (non-computed) with an identifier object. */
51
+ function isMember(node, object, property) {
52
+ return (
53
+ node.type === 'MemberExpression' &&
54
+ !node.computed &&
55
+ node.property.type === 'Identifier' &&
56
+ (property === undefined || node.property.name === property) &&
57
+ (object === undefined || (node.object.type === 'Identifier' && node.object.name === object))
58
+ );
59
+ }
60
+
61
+ /** Whether `callee` is `process.stdout.write`. */
62
+ function isProcessStdoutWrite(callee) {
63
+ return isMember(callee, undefined, 'write') && isMember(callee.object, 'process', 'stdout');
64
+ }
65
+
66
+ /** The called function's own name — `f(…)` or `x.f(…)` — or null. */
67
+ function calleeName(callee) {
68
+ if (callee.type === 'Identifier') return callee.name;
69
+ if (isMember(callee)) return callee.property.name;
70
+ return null;
71
+ }
72
+
73
+ /** Whether a call writes stdout by one of the routes this rule reports. */
74
+ function writesStdout(callee) {
75
+ if (isProcessStdoutWrite(callee)) return true;
76
+ if (isMember(callee, 'console') && STDOUT_CONSOLE_METHODS.has(callee.property.name)) return true;
77
+ return STDOUT_HELPERS.has(calleeName(callee));
78
+ }
79
+
80
+ module.exports = {
81
+ meta: {
82
+ type: 'problem',
83
+ docs: {
84
+ description:
85
+ 'Disallow writing stdout under a command directory except through the one document writer — ' +
86
+ 'process.stdout.write, console.log and the stdout helpers publish a shape nothing validates',
87
+ // A position on one CLI's architecture: it names that CLI's command
88
+ // directory and its writer, which no adopter's layout shares.
89
+ recommended: false,
90
+ },
91
+ schema: [
92
+ {
93
+ type: 'object',
94
+ properties: {
95
+ paths: { type: 'array', items: { type: 'string' }, uniqueItems: true },
96
+ allowFiles: { type: 'array', items: { type: 'string' }, uniqueItems: true },
97
+ },
98
+ required: ['paths', 'allowFiles'],
99
+ additionalProperties: false,
100
+ },
101
+ ],
102
+ messages: {
103
+ stdoutOutsideWriter:
104
+ 'stdout is written by the document writer only (endWithReport / endWithRefusal / ' +
105
+ 'writeExternalDocument / writeArtifact). A command that writes its own bytes publishes a ' +
106
+ 'shape nothing validates, beside an exit code it decided.',
107
+ staleAllow:
108
+ 'This file is listed in no-stdout-outside-writer allowFiles but writes stdout by no ' +
109
+ 'reported route any more. Remove it from the list — the ratchet only shrinks.',
110
+ },
111
+ },
112
+
113
+ create(context) {
114
+ const options = context.options?.[0] ?? { paths: [], allowFiles: [] };
115
+ const filename = context.filename ?? context.getFilename();
116
+ if (isTestFile(filename) || !createExemptDirectoryMatcher(options.paths)(filename)) {
117
+ return {};
118
+ }
119
+ const allowed = createExemptPathMatcher(options.allowFiles)(filename);
120
+ let violations = 0;
121
+
122
+ return {
123
+ CallExpression(node) {
124
+ const handedOn = node.arguments.filter((arg) => arg.type === 'Identifier' && STDOUT_HELPERS.has(arg.name));
125
+ const sites = writesStdout(node.callee) ? [node, ...handedOn] : handedOn;
126
+ violations += sites.length;
127
+ if (allowed) return;
128
+ for (const site of sites) context.report({ node: site, messageId: 'stdoutOutsideWriter' });
129
+ },
130
+ 'Program:exit'(node) {
131
+ if (allowed && violations === 0) context.report({ node, messageId: 'staleAllow' });
132
+ },
133
+ };
134
+ },
135
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-agent-toolkit/utils",
3
- "version": "0.2.0-rc.10",
3
+ "version": "0.2.0-rc.12",
4
4
  "type": "module",
5
5
  "description": "Core utility functions shared across the vibe-agent-toolkit packages",
6
6
  "sideEffects": [
@@ -17,7 +17,7 @@
17
17
  "author": "Jeff Dutton",
18
18
  "license": "MIT",
19
19
  "engines": {
20
- "node": ">=22.13.0"
20
+ "node": ">=22.16.0"
21
21
  },
22
22
  "main": "./dist/index.js",
23
23
  "types": "./dist/index.d.ts",
@@ -66,6 +66,10 @@
66
66
  "types": "./dist/testing.d.ts",
67
67
  "import": "./dist/testing.js"
68
68
  },
69
+ "./archive": {
70
+ "types": "./dist/archive.d.ts",
71
+ "import": "./dist/archive.js"
72
+ },
69
73
  "./asset": {
70
74
  "types": "./dist/asset.d.ts",
71
75
  "import": "./dist/asset.js"
@@ -93,14 +97,17 @@
93
97
  "build": "tsx ../dev-tools/src/tsc-clean-build.ts",
94
98
  "test:unit": "vitest run",
95
99
  "test:integration": "vitest run --config vitest.integration.config.ts",
100
+ "test:system": "vitest run --config vitest.system.config.ts",
96
101
  "test:watch": "vitest",
97
102
  "typecheck": "tsc --noEmit",
98
103
  "clean": "rimraf --glob dist \"*.tsbuildinfo\" .tsc-staging"
99
104
  },
100
105
  "dependencies": {
101
106
  "@vibe-validate/git": "0.20.1",
107
+ "adm-zip": "^0.6.1",
102
108
  "ignore": "^7.0.5",
103
109
  "picomatch": "^4.0.3",
110
+ "tar": "^7.5.7",
104
111
  "which": "^5.0.0",
105
112
  "yaml": "^2.9.0"
106
113
  },
@@ -113,6 +120,7 @@
113
120
  }
114
121
  },
115
122
  "devDependencies": {
123
+ "@types/adm-zip": "^0.5.8",
116
124
  "@types/which": "^3.0.4",
117
125
  "zod": "^3.25.0"
118
126
  },
@@ -1,51 +0,0 @@
1
- /**
2
- * Errno predicates — the two questions every catch site asks of an OS error,
3
- * answered by the `code` on the error or down its `cause` chain.
4
- *
5
- * ⚠️ This module imports NOTHING. It is the leaf that `fs-utils.ts`,
6
- * `path-utils.ts`, `dirent-kind.ts`, `path-containment.ts` and the rest of the
7
- * package share; it lived in `fs-utils.ts` until that put three of them in an
8
- * import cycle (`fs-utils → path-utils → fs-utils` and two more), and a leaf
9
- * is the one place a shared predicate cannot re-open one. (The cycle was once
10
- * blamed for a platform-dependent knip verdict on these two names; breaking it
11
- * changed nothing there — the cause was a missing knip entry, see
12
- * `docs/contributing/traps.md`, "A subpath module's re-exports flap by platform".)
13
- */
14
- /**
15
- * Whether `error` is the filesystem refusing a path rather than a bug.
16
- *
17
- * Deliberately NOT `error instanceof Error`: the point of every caller is to
18
- * degrade on a hostile tree, and a `TypeError` from a validator is not that.
19
- * Treating one as environmental turns a real defect into a warning about
20
- * whichever file it happened on — which makes a tool quietest exactly when it is
21
- * most wrong.
22
- *
23
- * Walks `cause`, because the errno is routinely re-wrapped on its way up. The CLI
24
- * config loader turns a read failure into `new Error('Failed to load config: …')`;
25
- * without following the chain the predicate answered "not a filesystem error" for
26
- * a plain `EACCES`, and an unreadable config aborted a whole `vat audit` run. Any
27
- * layer that adds context to an OS error defeats a `code`-only check, so the check
28
- * cannot be `code`-only.
29
- */
30
- export declare function isFilesystemAccessError(error: unknown): boolean;
31
- /**
32
- * Whether `error` means **there is nothing at this path** — `ENOENT`, or
33
- * `ENOTDIR` for a path whose component turned out to be a file — and nothing
34
- * else.
35
- *
36
- * This is the narrowing a `try { stat(p) } catch { return null }` is rewritten
37
- * to under the `no-blind-catch` lint rule: the sentinel stands for *absent*,
38
- * so only an absence may produce it; a refusal (`EACCES`, `EPERM`, `ELOOP`) or
39
- * a bug (`TypeError`) is rethrown and stays loud.
40
- *
41
- * ⚠️ Deliberately NOT {@link isFilesystemAccessError}. That predicate answers
42
- * "is this the environment's fault?" and to answer it groups `ENOENT` with
43
- * `EACCES` — the exact conflation that once turned an unreadable directory
44
- * into an empty one. The two questions have two predicates on purpose; see
45
- * also {@link listingFailure}, which makes the same split for `readdir`.
46
- *
47
- * Walks `cause` for the same reason its sibling does: the errno is routinely
48
- * re-wrapped on its way up.
49
- */
50
- export declare function isPathAbsentError(error: unknown): boolean;
51
- //# sourceMappingURL=errno.d.ts.map