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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/README.md +69 -23
  2. package/dist/asset-reference.js +3 -11
  3. package/dist/asset-reference.js.map +1 -1
  4. package/dist/compare-code-units.d.ts +28 -0
  5. package/dist/compare-code-units.d.ts.map +1 -0
  6. package/dist/compare-code-units.js +33 -0
  7. package/dist/compare-code-units.js.map +1 -0
  8. package/dist/crawl-timing.d.ts +657 -0
  9. package/dist/crawl-timing.d.ts.map +1 -0
  10. package/dist/crawl-timing.js +765 -0
  11. package/dist/crawl-timing.js.map +1 -0
  12. package/dist/crawl.d.ts +2 -4
  13. package/dist/crawl.d.ts.map +1 -1
  14. package/dist/crawl.js +2 -4
  15. package/dist/crawl.js.map +1 -1
  16. package/dist/dirent-kind.d.ts +83 -0
  17. package/dist/dirent-kind.d.ts.map +1 -0
  18. package/dist/dirent-kind.js +129 -0
  19. package/dist/dirent-kind.js.map +1 -0
  20. package/dist/entrypoint.d.ts +54 -0
  21. package/dist/entrypoint.d.ts.map +1 -0
  22. package/dist/entrypoint.js +71 -0
  23. package/dist/entrypoint.js.map +1 -0
  24. package/dist/env-flag.d.ts +51 -0
  25. package/dist/env-flag.d.ts.map +1 -0
  26. package/dist/env-flag.js +62 -0
  27. package/dist/env-flag.js.map +1 -0
  28. package/dist/errors/errno.d.ts +51 -0
  29. package/dist/errors/errno.d.ts.map +1 -0
  30. package/dist/errors/errno.js +112 -0
  31. package/dist/errors/errno.js.map +1 -0
  32. package/dist/errors/vat-error.d.ts +60 -0
  33. package/dist/errors/vat-error.d.ts.map +1 -0
  34. package/dist/errors/vat-error.js +86 -0
  35. package/dist/errors/vat-error.js.map +1 -0
  36. package/dist/file-crawler.d.ts +60 -0
  37. package/dist/file-crawler.d.ts.map +1 -1
  38. package/dist/file-crawler.js +133 -29
  39. package/dist/file-crawler.js.map +1 -1
  40. package/dist/fs/file-hash.d.ts.map +1 -1
  41. package/dist/fs/file-hash.js +0 -1
  42. package/dist/fs/file-hash.js.map +1 -1
  43. package/dist/fs-utils.d.ts +422 -191
  44. package/dist/fs-utils.d.ts.map +1 -1
  45. package/dist/fs-utils.js +534 -204
  46. package/dist/fs-utils.js.map +1 -1
  47. package/dist/fs.d.ts +5 -2
  48. package/dist/fs.d.ts.map +1 -1
  49. package/dist/fs.js +9 -7
  50. package/dist/fs.js.map +1 -1
  51. package/dist/git-run.d.ts +167 -0
  52. package/dist/git-run.d.ts.map +1 -0
  53. package/dist/git-run.js +154 -0
  54. package/dist/git-run.js.map +1 -0
  55. package/dist/git-snapshot.d.ts +199 -0
  56. package/dist/git-snapshot.d.ts.map +1 -0
  57. package/dist/git-snapshot.js +250 -0
  58. package/dist/git-snapshot.js.map +1 -0
  59. package/dist/git-tracker.d.ts +166 -3
  60. package/dist/git-tracker.d.ts.map +1 -1
  61. package/dist/git-tracker.js +250 -22
  62. package/dist/git-tracker.js.map +1 -1
  63. package/dist/git-utils.d.ts +84 -1
  64. package/dist/git-utils.d.ts.map +1 -1
  65. package/dist/git-utils.js +208 -82
  66. package/dist/git-utils.js.map +1 -1
  67. package/dist/git.d.ts +13 -4
  68. package/dist/git.d.ts.map +1 -1
  69. package/dist/git.js +12 -4
  70. package/dist/git.js.map +1 -1
  71. package/dist/gitignore-checker.d.ts.map +1 -1
  72. package/dist/gitignore-checker.js +15 -6
  73. package/dist/gitignore-checker.js.map +1 -1
  74. package/dist/glob/glob-pattern.js +5 -5
  75. package/dist/glob/glob-pattern.js.map +1 -1
  76. package/dist/index.d.ts +32 -20
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +95 -50
  79. package/dist/index.js.map +1 -1
  80. package/dist/listing-refusal.d.ts +112 -0
  81. package/dist/listing-refusal.d.ts.map +1 -0
  82. package/dist/listing-refusal.js +105 -0
  83. package/dist/listing-refusal.js.map +1 -0
  84. package/dist/path-containment.d.ts +64 -0
  85. package/dist/path-containment.d.ts.map +1 -0
  86. package/dist/path-containment.js +111 -0
  87. package/dist/path-containment.js.map +1 -0
  88. package/dist/path-core.d.ts +113 -18
  89. package/dist/path-core.d.ts.map +1 -1
  90. package/dist/path-core.js +139 -23
  91. package/dist/path-core.js.map +1 -1
  92. package/dist/path-utils.d.ts.map +1 -1
  93. package/dist/path-utils.js +42 -47
  94. package/dist/path-utils.js.map +1 -1
  95. package/dist/path.d.ts +1 -1
  96. package/dist/path.d.ts.map +1 -1
  97. package/dist/path.js +1 -1
  98. package/dist/path.js.map +1 -1
  99. package/dist/process.d.ts +7 -0
  100. package/dist/process.d.ts.map +1 -1
  101. package/dist/process.js +7 -0
  102. package/dist/process.js.map +1 -1
  103. package/dist/project-utils.d.ts.map +1 -1
  104. package/dist/project-utils.js +15 -9
  105. package/dist/project-utils.js.map +1 -1
  106. package/dist/project.d.ts +7 -5
  107. package/dist/project.d.ts.map +1 -1
  108. package/dist/project.js +7 -5
  109. package/dist/project.js.map +1 -1
  110. package/dist/safe-exec.d.ts +40 -11
  111. package/dist/safe-exec.d.ts.map +1 -1
  112. package/dist/safe-exec.js +111 -25
  113. package/dist/safe-exec.js.map +1 -1
  114. package/dist/skill-targets.d.ts +2 -2
  115. package/dist/skill-targets.js +2 -2
  116. package/dist/skill-test/auth-resolver.d.ts +10 -2
  117. package/dist/skill-test/auth-resolver.d.ts.map +1 -1
  118. package/dist/skill-test/auth-resolver.js +26 -11
  119. package/dist/skill-test/auth-resolver.js.map +1 -1
  120. package/dist/skill-test/index.d.ts +1 -1
  121. package/dist/skill-test/index.d.ts.map +1 -1
  122. package/dist/skill-test/index.js +1 -1
  123. package/dist/skill-test/index.js.map +1 -1
  124. package/dist/skill-test/spawn-claude.d.ts +30 -1
  125. package/dist/skill-test/spawn-claude.d.ts.map +1 -1
  126. package/dist/skill-test/spawn-claude.js +71 -5
  127. package/dist/skill-test/spawn-claude.js.map +1 -1
  128. package/dist/skill-test/transcript.d.ts +41 -2
  129. package/dist/skill-test/transcript.d.ts.map +1 -1
  130. package/dist/skill-test/transcript.js +55 -10
  131. package/dist/skill-test/transcript.js.map +1 -1
  132. package/dist/spawn-hardened.js +0 -1
  133. package/dist/spawn-hardened.js.map +1 -1
  134. package/dist/stdio-blocking.js +13 -14
  135. package/dist/stdio-blocking.js.map +1 -1
  136. package/dist/test-helpers.d.ts +135 -100
  137. package/dist/test-helpers.d.ts.map +1 -1
  138. package/dist/test-helpers.js +224 -146
  139. package/dist/test-helpers.js.map +1 -1
  140. package/dist/testing/executables.d.ts +32 -0
  141. package/dist/testing/executables.d.ts.map +1 -0
  142. package/dist/testing/executables.js +70 -0
  143. package/dist/testing/executables.js.map +1 -0
  144. package/dist/testing/hostile-tree.d.ts +79 -0
  145. package/dist/testing/hostile-tree.d.ts.map +1 -0
  146. package/dist/testing/hostile-tree.js +164 -0
  147. package/dist/testing/hostile-tree.js.map +1 -0
  148. package/dist/testing/platform-gates.d.ts +31 -0
  149. package/dist/testing/platform-gates.d.ts.map +1 -0
  150. package/dist/testing/platform-gates.js +31 -0
  151. package/dist/testing/platform-gates.js.map +1 -0
  152. package/dist/testing/temp-dir.d.ts +255 -0
  153. package/dist/testing/temp-dir.d.ts.map +1 -0
  154. package/dist/testing/temp-dir.js +363 -0
  155. package/dist/testing/temp-dir.js.map +1 -0
  156. package/dist/testing.d.ts +94 -3
  157. package/dist/testing.d.ts.map +1 -1
  158. package/dist/testing.js +119 -3
  159. package/dist/testing.js.map +1 -1
  160. package/dist/text-content.d.ts +197 -0
  161. package/dist/text-content.d.ts.map +1 -0
  162. package/dist/text-content.js +354 -0
  163. package/dist/text-content.js.map +1 -0
  164. package/dist/text-file.d.ts +45 -0
  165. package/dist/text-file.d.ts.map +1 -0
  166. package/dist/text-file.js +51 -0
  167. package/dist/text-file.js.map +1 -0
  168. package/dist/text.d.ts +16 -0
  169. package/dist/text.d.ts.map +1 -0
  170. package/dist/text.js +16 -0
  171. package/dist/text.js.map +1 -0
  172. package/dist/timing-dump.d.ts +136 -0
  173. package/dist/timing-dump.d.ts.map +1 -0
  174. package/dist/timing-dump.js +229 -0
  175. package/dist/timing-dump.js.map +1 -0
  176. package/dist/windows-shell.d.ts +0 -1
  177. package/dist/windows-shell.d.ts.map +1 -1
  178. package/dist/windows-shell.js +0 -1
  179. package/dist/windows-shell.js.map +1 -1
  180. package/dist/zod-introspection.js +2 -2
  181. package/dist/zod-introspection.js.map +1 -1
  182. package/eslint/README.md +223 -49
  183. package/eslint/index.cjs +111 -97
  184. package/eslint/index.d.cts +16 -6
  185. package/eslint/rules/callee-name.cjs +23 -0
  186. package/eslint/rules/commands-import-boundary.cjs +148 -0
  187. package/eslint/rules/dead-import.cjs +1 -1
  188. package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
  189. package/eslint/rules/eslint-rule-factory.cjs +5 -2
  190. package/eslint/rules/exempt-path-matcher.cjs +2 -1
  191. package/eslint/rules/explicit-zod-strictness.cjs +120 -0
  192. package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
  193. package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
  194. package/eslint/rules/no-bare-symlink-in-tests.cjs +213 -0
  195. package/eslint/rules/no-blind-catch.cjs +171 -0
  196. package/eslint/rules/no-child-process-execSync.cjs +8 -0
  197. package/eslint/rules/no-decaying-referent.cjs +217 -0
  198. package/eslint/rules/no-dotdot-containment.cjs +160 -0
  199. package/eslint/rules/no-file-url-string-concat.cjs +4 -1
  200. package/eslint/rules/no-fragile-entrypoint-guard.cjs +228 -0
  201. package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
  202. package/eslint/rules/no-fs-promises-cp.cjs +7 -0
  203. package/eslint/rules/no-fs-realpathSync.cjs +8 -0
  204. package/eslint/rules/no-hardcoded-path-split.cjs +29 -18
  205. package/eslint/rules/no-io-in-unit-tier.cjs +129 -0
  206. package/eslint/rules/no-literal-process-exit.cjs +129 -0
  207. package/eslint/rules/no-manual-path-normalize.cjs +136 -107
  208. package/eslint/rules/no-os-tmpdir.cjs +8 -0
  209. package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
  210. package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
  211. package/eslint/rules/no-path-startswith.cjs +7 -2
  212. package/eslint/rules/no-process-exit-in-phase.cjs +127 -0
  213. package/eslint/rules/no-raw-node-path.cjs +563 -0
  214. package/eslint/rules/no-raw-text-decode.cjs +305 -0
  215. package/eslint/rules/no-registry-count-pin.cjs +255 -0
  216. package/eslint/rules/no-self-package-import.cjs +168 -0
  217. package/eslint/rules/no-test-scoped-functions.cjs +10 -3
  218. package/eslint/rules/no-unix-shell-commands.cjs +6 -2
  219. package/eslint/rules/no-unsafe-root-join.cjs +23 -2
  220. package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
  221. package/eslint/rules/no-version-literal.cjs +151 -0
  222. package/eslint/rules/prefer-startswith-over-regex.cjs +9 -0
  223. package/eslint/rules/require-justified-skip.cjs +8 -2
  224. package/eslint/rules/safe-import.cjs +1 -1
  225. package/package.json +16 -16
  226. package/dist/link-auth/build-headers.d.ts +0 -34
  227. package/dist/link-auth/build-headers.d.ts.map +0 -1
  228. package/dist/link-auth/build-headers.js +0 -58
  229. package/dist/link-auth/build-headers.js.map +0 -1
  230. package/dist/link-auth/expand-macro.d.ts +0 -38
  231. package/dist/link-auth/expand-macro.d.ts.map +0 -1
  232. package/dist/link-auth/expand-macro.js +0 -133
  233. package/dist/link-auth/expand-macro.js.map +0 -1
  234. package/dist/link-auth/macros.yaml +0 -50
  235. package/dist/link-auth/resolve-token.d.ts +0 -83
  236. package/dist/link-auth/resolve-token.d.ts.map +0 -1
  237. package/dist/link-auth/resolve-token.js +0 -101
  238. package/dist/link-auth/resolve-token.js.map +0 -1
  239. package/dist/link-auth/resolve.d.ts +0 -102
  240. package/dist/link-auth/resolve.d.ts.map +0 -1
  241. package/dist/link-auth/resolve.js +0 -66
  242. package/dist/link-auth/resolve.js.map +0 -1
  243. package/dist/link-auth/rewrite.d.ts +0 -52
  244. package/dist/link-auth/rewrite.d.ts.map +0 -1
  245. package/dist/link-auth/rewrite.js +0 -102
  246. package/dist/link-auth/rewrite.js.map +0 -1
  247. package/dist/link-auth/select-provider.d.ts +0 -30
  248. package/dist/link-auth/select-provider.d.ts.map +0 -1
  249. package/dist/link-auth/select-provider.js +0 -55
  250. package/dist/link-auth/select-provider.js.map +0 -1
  251. package/dist/link-auth/template.d.ts +0 -40
  252. package/dist/link-auth/template.d.ts.map +0 -1
  253. package/dist/link-auth/template.js +0 -89
  254. package/dist/link-auth/template.js.map +0 -1
  255. package/dist/link-auth/transforms.d.ts +0 -46
  256. package/dist/link-auth/transforms.d.ts.map +0 -1
  257. package/dist/link-auth/transforms.js +0 -52
  258. package/dist/link-auth/transforms.js.map +0 -1
  259. package/dist/template-entry.d.ts +0 -10
  260. package/dist/template-entry.d.ts.map +0 -1
  261. package/dist/template-entry.js +0 -10
  262. package/dist/template-entry.js.map +0 -1
  263. package/dist/template.d.ts +0 -7
  264. package/dist/template.d.ts.map +0 -1
  265. package/dist/template.js +0 -18
  266. package/dist/template.js.map +0 -1
  267. package/eslint/rules/no-path-join.cjs +0 -14
  268. package/eslint/rules/no-path-relative.cjs +0 -14
  269. package/eslint/rules/no-path-resolve.cjs +0 -14
  270. package/eslint/rules/path-function-rule-factory.cjs +0 -496
package/eslint/index.cjs CHANGED
@@ -9,7 +9,7 @@
9
9
  * than in CI on a different OS.
10
10
  *
11
11
  * They ship as a SUBPATH rather than a separate package because an ESLint plugin
12
- * is data, not code that runs: every module below exports a plain rule object and
12
+ * is data, not code that runs: every rule module exports a plain rule object and
13
13
  * none of them `require('eslint')`. So this entry resolves — and the other twelve
14
14
  * subpaths keep resolving — whether or not ESLint is installed, which is why
15
15
  * `eslint` is declared as an OPTIONAL peer dependency. One install, one version,
@@ -20,6 +20,28 @@
20
20
  * entry point can be both `require()`d from an `eslint.config.cjs` and `import`ed
21
21
  * from an `eslint.config.js`/`.mjs`.
22
22
  *
23
+ * ## The manifest is the directory
24
+ *
25
+ * There is no hand-maintained list of rules here. Every `rules/*.cjs` whose
26
+ * export carries a `meta` object IS a rule, keyed by its basename; the modules
27
+ * that export a factory or a helper object (`eslint-rule-factory`,
28
+ * `no-command-direct-factory`, `exempt-path-matcher`, `safe-import`,
29
+ * `dead-import`) have no `meta` and are skipped. Each rule then declares its own
30
+ * place in `configs.recommended` through `meta.docs.recommended` and
31
+ * `meta.docs.recommendedSeverity`, beside the rule it describes — so adding a
32
+ * rule is one file, and the README/docs table is generated from the same
33
+ * metadata (`bun run generate:eslint-rules-doc` in `packages/utils`).
34
+ *
35
+ * The last hand list here held 27 entries and was mirrored by three literal
36
+ * counts in two test files and two prose counts in two docs, every one of which
37
+ * had drifted at least once. A directory listing cannot drift.
38
+ *
39
+ * `node:fs` and `node:path` are the ONLY external modules this subpath reaches,
40
+ * and only from this file: the rule modules themselves still require nothing —
41
+ * `test/eslint/subpath-purity.test.ts` pins both halves. Two builtins that ship
42
+ * with every Node install do not change the optional-peer property, which was
43
+ * only ever about `eslint` and third-party packages.
44
+ *
23
45
  * Rules whose exemptions name a file (the ONE implementation file allowed to call
24
46
  * the primitive) take an `exemptFiles` option — see README.md. The shipped
25
47
  * defaults are EMPTY: an exemption is a claim about a specific repo's layout, and
@@ -29,109 +51,75 @@
29
51
 
30
52
  'use strict';
31
53
 
32
- const rules = {
33
- 'no-child-process-execSync': require('./rules/no-child-process-execSync.cjs'),
34
- 'no-hardcoded-path-split': require('./rules/no-hardcoded-path-split.cjs'),
35
- 'no-path-startswith': require('./rules/no-path-startswith.cjs'),
36
- 'no-unix-shell-commands': require('./rules/no-unix-shell-commands.cjs'),
37
- 'no-os-tmpdir': require('./rules/no-os-tmpdir.cjs'),
38
- 'no-fs-mkdirSync': require('./rules/no-fs-mkdirSync.cjs'),
39
- 'no-fs-realpathSync': require('./rules/no-fs-realpathSync.cjs'),
40
- 'no-manual-path-normalize': require('./rules/no-manual-path-normalize.cjs'),
41
- 'no-path-sep-in-strings': require('./rules/no-path-sep-in-strings.cjs'),
42
- 'no-path-operations-in-comparisons': require('./rules/no-path-operations-in-comparisons.cjs'),
43
- 'no-path-join': require('./rules/no-path-join.cjs'),
44
- 'no-path-resolve': require('./rules/no-path-resolve.cjs'),
45
- 'no-path-relative': require('./rules/no-path-relative.cjs'),
46
- 'no-test-scoped-functions': require('./rules/no-test-scoped-functions.cjs'),
47
- 'no-fs-promises-cp': require('./rules/no-fs-promises-cp.cjs'),
48
- 'no-url-pathname-for-fs': require('./rules/no-url-pathname-for-fs.cjs'),
49
- 'no-bare-dynamic-import-path': require('./rules/no-bare-dynamic-import-path.cjs'),
50
- 'no-file-url-string-concat': require('./rules/no-file-url-string-concat.cjs'),
51
- 'prefer-startswith-over-regex': require('./rules/prefer-startswith-over-regex.cjs'),
52
- 'no-unsafe-root-join': require('./rules/no-unsafe-root-join.cjs'),
53
- 'require-justified-skip': require('./rules/require-justified-skip.cjs'),
54
- };
54
+ const { readdirSync } = require('node:fs');
55
+ const path = require('node:path');
56
+
57
+ const RULES_DIR = path.join(__dirname, 'rules');
55
58
 
56
59
  /**
57
- * Rules deliberately LEFT OUT of `configs.recommended`.
58
- *
59
- * `recommended` is the cross-platform-safety core: every rule in it flags a call
60
- * that is wrong (or unportable) regardless of how the adopting project likes to
61
- * write tests. The two below are neither — they encode a position on TEST STYLE:
62
- *
63
- * - `require-justified-skip` — a specific annotation grammar (`SKIP(#123): reason`)
64
- * for a disabled test, plus a view on what counts as a tautological assertion.
65
- * - `no-test-scoped-functions` — a view on WHERE a test helper may be declared
66
- * (module scope, never inside `describe`/`it`).
67
- *
68
- * Someone installing this package for `safePath.join()` should not silently
69
- * inherit either. Both ship in `rules` and stay enabled explicitly:
70
- *
71
- * '@vibe-agent-toolkit/require-justified-skip': 'error',
72
- *
73
- * That is exactly what VAT's own `eslint.config.js` does — it does not consume
74
- * `configs.recommended` at all, so this exclusion changes nothing about how this
75
- * repo lints itself.
60
+ * The two values `recommendedSeverity` may take. A rule that is not
61
+ * recommended may still declare one — it is the severity the rule WOULD ride
62
+ * at, and the generated docs table prints it — but `off` is not a spelling: a
63
+ * rule that wants to be off is a rule with `recommended: false`.
76
64
  */
77
- const RECOMMENDED_EXCLUDE = new Set([
78
- 'require-justified-skip',
79
- 'no-test-scoped-functions',
80
- // Excluded for a DIFFERENT reason than the two above: not a style opinion, but
81
- // an unsound heuristic. It keys on whether an identifier's name ends in `root`
82
- // rather than on whether any segment is caller-controlled, which makes it
83
- // simultaneously noisy and blind. Measured on a 4,670-file adopter tree: 108
84
- // findings, 0 autofixable, and every one of these verified by execution here:
85
- //
86
- // FIRES safePath.join(repoRoot, 'docs', 'product') <- all literals, cannot escape
87
- // FIRES safePath.resolve(packageRoot, '..', '..') <- escaping IS the intent; the fix breaks it
88
- // FIRES safePath.join(repoRoot) <- one argument, no segment at all
89
- // silent safePath.join(base, userInput) <- THE dangerous shape, missed
90
- //
91
- // A rule that misses the case it exists to catch must not ride in a config
92
- // named `recommended` at any severity — a safety core that cries wolf teaches
93
- // people to ignore it, which costs the true positives too. It still ships, and
94
- // it still earns `error` where scoped to directories in which a path escape is
95
- // a security boundary (this repo scopes it to the skill-test staging code).
96
- // Re-include it when it keys on taint rather than on naming.
97
- 'no-unsafe-root-join',
98
- ]);
65
+ const RECOMMENDED_SEVERITIES = new Set(['error', 'warn']);
99
66
 
100
67
  /**
101
- * Default severities for `configs.recommended`.
102
- *
103
- * `error` is the default: every rule below flags a call whose replacement is a
104
- * one-line swap, and a wrong answer is a real bug on some platform.
105
- *
106
- * `warn` is reserved for the case where a fresh adopter's first run would
107
- * otherwise be a wall of blocking errors they cannot triage in one sitting:
108
- * `no-path-join` / `no-path-resolve` / `no-path-relative`, the highest-churn
109
- * rules by far (they fire on every raw `node:path` call in the codebase).
110
- * Measured on a 4,670-file adopter tree: 3,963 + 372 + 1 findings, **all
111
- * autofixable**. All three auto-fix, so `warn` lets a project run `--fix` and
112
- * burn the list down incrementally instead of blocking CI on day one.
113
- *
114
- * Raise them to `error` once the backlog is clear — that is what VAT itself does.
68
+ * Read a rule's `meta.docs` and refuse anything a rule in this pack must not ship
69
+ * without. Thrown at load time, so a malformed rule fails every consumer's
70
+ * `eslint` run at startup rather than silently landing outside `recommended`.
71
+ */
72
+ function validateRuleDocs(name, rule) {
73
+ if (typeof rule.create !== 'function') {
74
+ throw new TypeError(`eslint rule '${name}' exports a meta but no create function`);
75
+ }
76
+ const docs = rule.meta.docs;
77
+ if (typeof docs?.description !== 'string' || docs.description.length === 0) {
78
+ throw new TypeError(`eslint rule '${name}' has no meta.docs.description`);
79
+ }
80
+ if (typeof docs.recommended !== 'boolean') {
81
+ throw new TypeError(`eslint rule '${name}' must declare meta.docs.recommended as a boolean`);
82
+ }
83
+ if (docs.recommendedSeverity !== undefined && !RECOMMENDED_SEVERITIES.has(docs.recommendedSeverity)) {
84
+ throw new TypeError(`eslint rule '${name}' declares meta.docs.recommendedSeverity outside 'error' | 'warn'`);
85
+ }
86
+ if (docs.recommended && docs.recommendedSeverity === undefined) {
87
+ throw new TypeError(`eslint rule '${name}' is recommended but declares no meta.docs.recommendedSeverity`);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Every rule module under `rules/`, keyed by basename, in directory order
93
+ * (which `readdirSync` returns sorted on every platform this package supports).
115
94
  *
116
- * The criterion for `warn` is MIGRATION VOLUME, not how real the finding is. Every
117
- * rule in this pack either prevents a bug or shifts a static-analysis finding left of
118
- * a merge, and both are worth blocking on; what `warn` buys is a first run that reads
119
- * as a backlog to `--fix` rather than a wall. `prefer-startswith-over-regex` was
120
- * briefly graded on a different axis ("style, not a defect") and demoted — that was
121
- * wrong twice over: avoiding a SonarQube S6557 at lint time instead of at merge time
122
- * is a real saving, and the rule's matcher rejects any regex containing a
123
- * metacharacter, so it only fires on true literal prefixes and has near-zero churn.
124
- * It is `error`.
95
+ * Distinguished from the factories and helpers by a `meta` export, not by
96
+ * filename: a naming convention is a claim nobody checks, and a factory that
97
+ * happened to be named `no-…` would otherwise be registered as a rule whose
98
+ * `create` is a function that builds rules.
125
99
  */
126
- const RECOMMENDED_WARN = new Set([
127
- 'no-path-join',
128
- 'no-path-resolve',
129
- 'no-path-relative',
130
- ]);
100
+ function discoverRules(rulesDir = RULES_DIR) {
101
+ const rules = {};
102
+ for (const file of readdirSync(rulesDir)) {
103
+ if (!file.endsWith('.cjs')) {
104
+ continue;
105
+ }
106
+ // eslint-disable-next-line security/detect-non-literal-require -- the directory listing IS the manifest; every entry is a file this package ships
107
+ const candidate = require(path.join(rulesDir, file));
108
+ if (typeof candidate !== 'object' || candidate === null || typeof candidate.meta !== 'object') {
109
+ continue;
110
+ }
111
+ const name = file.slice(0, -'.cjs'.length);
112
+ validateRuleDocs(name, candidate);
113
+ rules[name] = candidate;
114
+ }
115
+ return rules;
116
+ }
117
+
118
+ const rules = discoverRules();
131
119
 
132
120
  /**
133
121
  * Plugin namespace an adopter gets from `configs.recommended`, and therefore the
134
- * prefix on every rule id (`@vibe-agent-toolkit/no-path-join`).
122
+ * prefix on every rule id (`@vibe-agent-toolkit/no-raw-node-path`).
135
123
  *
136
124
  * Deliberately the SCOPE, not the full subpath specifier: rule ids are the surface
137
125
  * adopters type into `rules`, `eslint-disable` comments and CI baselines, and they
@@ -146,15 +134,41 @@ const plugin = {
146
134
  },
147
135
  rules,
148
136
  configs: {},
137
+ /** Test seam: the discovery walk over an arbitrary directory, so the load-time refusals can be exercised on a fixture. */
138
+ __internal: { discoverRules },
149
139
  };
150
140
 
141
+ /**
142
+ * `configs.recommended` is the cross-platform-safety core: every rule whose
143
+ * `meta.docs.recommended` is true, at the severity it declares.
144
+ *
145
+ * `error` is the norm: every such rule flags a call whose replacement is a
146
+ * one-line swap, and a wrong answer is a real bug on some platform. `warn` is
147
+ * reserved for the case where a fresh adopter's first run would otherwise be a
148
+ * wall of blocking errors they cannot triage in one sitting — the criterion is
149
+ * MIGRATION VOLUME, not how real the finding is. A rule whose findings were
150
+ * doubted would be out of `recommended` entirely, not demoted.
151
+ *
152
+ * Each rule that opts OUT states why beside its own `recommended: false`. The
153
+ * reasons fall into a few families — a position on TEST STYLE
154
+ * (`require-justified-skip`, `no-test-scoped-functions`), a heuristic that keys
155
+ * on a NAMING CONVENTION rather than the property it cares about
156
+ * (`no-unsafe-root-join`, `no-process-exit-in-phase`), a SEAM that only exists
157
+ * once the consumer writes it (`no-raw-text-decode`), a REQUIRED OPTION this
158
+ * config cannot supply (`no-self-package-import`), and a claim about the
159
+ * CONSUMER's environment that is right for some and wrong for others
160
+ * (`no-fragile-entrypoint-guard`, `no-bare-symlink-in-tests`). Someone installing
161
+ * this package for `safePath.join()` should not silently inherit any of them.
162
+ * All still ship in `rules` and are enabled by naming them — which is what
163
+ * VAT's own `eslint.config.js` does; it does not consume `configs.recommended`.
164
+ */
151
165
  plugin.configs.recommended = {
152
166
  name: '@vibe-agent-toolkit/utils/eslint/recommended',
153
167
  plugins: { [NAMESPACE]: plugin },
154
168
  rules: Object.fromEntries(
155
- Object.keys(rules)
156
- .filter((name) => !RECOMMENDED_EXCLUDE.has(name))
157
- .map((name) => [`${NAMESPACE}/${name}`, RECOMMENDED_WARN.has(name) ? 'warn' : 'error']),
169
+ Object.entries(rules)
170
+ .filter(([, rule]) => rule.meta.docs.recommended)
171
+ .map(([name, rule]) => [`${NAMESPACE}/${name}`, rule.meta.docs.recommendedSeverity]),
158
172
  ),
159
173
  };
160
174
 
@@ -37,20 +37,30 @@ declare namespace plugin {
37
37
  interface FlatConfig {
38
38
  name: string;
39
39
  plugins: Record<string, Plugin>;
40
- /** Rule id → severity. Keys are namespaced: `@vibe-agent-toolkit/no-path-join`. */
40
+ /** Rule id → severity. Keys are namespaced: `@vibe-agent-toolkit/no-raw-node-path`. */
41
41
  rules: Record<string, 'error' | 'warn' | 'off'>;
42
42
  }
43
43
 
44
44
  interface Plugin {
45
45
  meta: { name: string };
46
- /** Rule name WITHOUT the namespace prefix, e.g. `no-path-join`. */
46
+ /** Rule name WITHOUT the namespace prefix, e.g. `no-raw-node-path`. */
47
47
  rules: Record<string, RuleModule>;
48
48
  configs: {
49
49
  /**
50
- * The cross-platform safety core: 18 of the 21 rules, 15 `error` / 3 `warn`.
51
- * `no-test-scoped-functions` and `require-justified-skip` are excluded — they
52
- * are positions on test style, not portability facts. Both still ship in
53
- * `rules` and are enabled by naming them.
50
+ * The cross-platform safety core: every rule whose `meta.docs.recommended`
51
+ * is true, at the severity its `meta.docs.recommendedSeverity` declares.
52
+ *
53
+ * No count lives here on purpose. This comment once read "18 of the 22
54
+ * rules, four are excluded" while the registry held 24 and the exclude
55
+ * set six, and later "19 of the 27" — a number in prose is a claim the
56
+ * manifest cannot check. The generated table in `README.md` carries the
57
+ * live counts, and `test/eslint/rule-manifest.test.ts` asserts the
58
+ * config against the directory rather than against a literal.
59
+ *
60
+ * Every rule that opts out states why beside its own `recommended: false`
61
+ * in `rules/<name>.cjs`; all of them still ship in `rules` and are
62
+ * enabled by naming them — which is what this repo's own
63
+ * `eslint.config.js` does.
54
64
  */
55
65
  recommended: FlatConfig;
56
66
  };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared by the rules that match a call by name: the identifier a call is
3
+ * made through, bare (`spawn(…)`) or as the last member of a non-computed
4
+ * member expression (`cp.spawn(…)`), or `null` for any other callee.
5
+ *
6
+ * Not a rule — exports no `meta`, so `index.cjs` never registers it.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ /** @param {object} call - ESTree `CallExpression` node. */
12
+ function calleeName(call) {
13
+ const { callee } = call;
14
+ if (callee.type === 'Identifier') {
15
+ return callee.name;
16
+ }
17
+ if (callee.type === 'MemberExpression' && !callee.computed && callee.property.type === 'Identifier') {
18
+ return callee.property.name;
19
+ }
20
+ return null;
21
+ }
22
+
23
+ module.exports = { calleeName };
@@ -0,0 +1,148 @@
1
+ /**
2
+ * ESLint rule: commands-import-boundary
3
+ *
4
+ * In a command module — a file under one of the configured command
5
+ * directories — flags any import of the filesystem (`node:fs`,
6
+ * `node:fs/promises`, `fs`, `fs/promises`) and any import whose specifier
7
+ * matches a configured forbidden-module pattern (a package's internals, a
8
+ * local parser or walker module).
9
+ *
10
+ * A command's job is to parse arguments, call a seam, and render a report. The
11
+ * moment it opens a directory itself it has become a fifth enumeration lane
12
+ * nobody documented: `docs/contributing/command-lane-table.md` names the
13
+ * walkers every population must come through, and one command carrying its
14
+ * own ~700-line walker beside them was the audit finding behind this rule.
15
+ * The boundary is syntactic — an import is an import — so it holds at the
16
+ * desk rather than in a review.
17
+ *
18
+ * Shapes covered: static `import`, `import type` is IGNORED (a type does no
19
+ * I/O), `export … from`, dynamic `import('…')` and `require('…')` with a
20
+ * literal specifier.
21
+ *
22
+ * Options:
23
+ * - `commandGlobs: string[]` — repo-relative directories that hold commands
24
+ * (default `['packages/cli/src/commands/']`). A trailing `/**` is accepted
25
+ * and ignored; matching is by anchored directory prefix, so nested command
26
+ * directories are covered.
27
+ * - `forbiddenModules: string[]` — regex sources tested against the import
28
+ * specifier (default `['^@vibe-agent-toolkit/resources/']`: the barrel is a
29
+ * seam, a subpath is an internal).
30
+ * - `allowFiles: string[]` — repo-relative paths of today's offenders, the
31
+ * ratchet. Name files, never directories.
32
+ *
33
+ * @example
34
+ * // BAD — the command is now a walker
35
+ * import { readdirSync } from 'node:fs';
36
+ *
37
+ * // GOOD — the population comes through a declared lane
38
+ * import { crawlDirectory } from '@vibe-agent-toolkit/utils/crawl';
39
+ */
40
+
41
+ 'use strict';
42
+
43
+ const {
44
+ createExemptDirectoryMatcher,
45
+ createExemptPathMatcher,
46
+ } = require('./exempt-path-matcher.cjs');
47
+
48
+ const FS_MODULES = new Set(['node:fs', 'node:fs/promises', 'fs', 'fs/promises']);
49
+ const DEFAULT_COMMAND_DIRS = ['packages/cli/src/commands/'];
50
+ const DEFAULT_FORBIDDEN = ['^@vibe-agent-toolkit/resources/'];
51
+
52
+ /** Strip a trailing glob so `packages/cli/src/commands/**` reads as its directory. */
53
+ function toDirectory(entry) {
54
+ return entry.endsWith('/**') ? entry.slice(0, -2) : entry;
55
+ }
56
+
57
+ /** The string value of a literal module specifier, or null. */
58
+ function literalSpecifier(node) {
59
+ return node?.type === 'Literal' && typeof node.value === 'string' ? node.value : null;
60
+ }
61
+
62
+ module.exports = {
63
+ meta: {
64
+ type: 'problem',
65
+ docs: {
66
+ description:
67
+ 'Disallow filesystem and internal-module imports in command modules — a command calls a ' +
68
+ 'declared enumeration lane, it does not become one',
69
+ recommended: false,
70
+ recommendedSeverity: 'warn',
71
+ },
72
+ schema: [
73
+ {
74
+ type: 'object',
75
+ properties: {
76
+ commandGlobs: { type: 'array', items: { type: 'string' }, uniqueItems: true },
77
+ forbiddenModules: { type: 'array', items: { type: 'string' }, uniqueItems: true },
78
+ allowFiles: { type: 'array', items: { type: 'string' }, uniqueItems: true },
79
+ },
80
+ additionalProperties: false,
81
+ },
82
+ ],
83
+ messages: {
84
+ fsImport:
85
+ "Command modules do not import '{{source}}': reading the tree here makes this command an " +
86
+ 'undeclared enumeration lane. Go through a seam in @vibe-agent-toolkit/utils or ' +
87
+ '@vibe-agent-toolkit/resources (see docs/contributing/command-lane-table.md).',
88
+ forbiddenModule:
89
+ "Command modules do not import '{{source}}' (matches forbidden pattern /{{pattern}}/): " +
90
+ 'it is an internal, not a seam. Import the barrel or the declared lane instead.',
91
+ },
92
+ },
93
+
94
+ create(context) {
95
+ const options = context.options?.[0] ?? {};
96
+ const filename = context.filename ?? context.getFilename();
97
+ const commandDirs = (options.commandGlobs ?? DEFAULT_COMMAND_DIRS).map(toDirectory);
98
+ const isCommandFile = createExemptDirectoryMatcher(commandDirs);
99
+ const isAllowed = createExemptPathMatcher(options.allowFiles ?? []);
100
+ if (!isCommandFile(filename) || isAllowed(filename)) {
101
+ return {};
102
+ }
103
+
104
+ const forbidden = (options.forbiddenModules ?? DEFAULT_FORBIDDEN).map((pattern) => ({
105
+ pattern,
106
+ // eslint-disable-next-line security/detect-non-literal-regexp -- the option is a regex source by contract: a config-time string written by the repo, not user data
107
+ regex: new RegExp(pattern, 'u'),
108
+ }));
109
+
110
+ function check(node, source) {
111
+ if (source === null) {
112
+ return;
113
+ }
114
+ if (FS_MODULES.has(source)) {
115
+ context.report({ node, messageId: 'fsImport', data: { source } });
116
+ return;
117
+ }
118
+ const hit = forbidden.find(({ regex }) => regex.test(source));
119
+ if (hit) {
120
+ context.report({ node, messageId: 'forbiddenModule', data: { source, pattern: hit.pattern } });
121
+ }
122
+ }
123
+
124
+ return {
125
+ ImportDeclaration(node) {
126
+ if (node.importKind !== 'type') {
127
+ check(node, literalSpecifier(node.source));
128
+ }
129
+ },
130
+ ExportNamedDeclaration(node) {
131
+ if (node.source && node.exportKind !== 'type') {
132
+ check(node, literalSpecifier(node.source));
133
+ }
134
+ },
135
+ ExportAllDeclaration(node) {
136
+ check(node, literalSpecifier(node.source));
137
+ },
138
+ ImportExpression(node) {
139
+ check(node, literalSpecifier(node.source));
140
+ },
141
+ CallExpression(node) {
142
+ if (node.callee.type === 'Identifier' && node.callee.name === 'require') {
143
+ check(node, literalSpecifier(node.arguments[0]));
144
+ }
145
+ },
146
+ };
147
+ },
148
+ };
@@ -233,7 +233,7 @@ function reportDeadUnsafeImports(
233
233
  },
234
234
  // `fixer.remove(node)` takes the declaration and leaves its newline, so a
235
235
  // blank line remains where the import was. That is exactly what the
236
- // specifier-removal leg in `path-function-rule-factory.cjs` has always
236
+ // specifier-removal leg in `no-raw-node-path.cjs` has always
237
237
  // done — its fixtures pin the leading `\n` — and matching it keeps one
238
238
  // behaviour rather than two. Extending the range through a trailing
239
239
  // whitespace-only remainder would tidy both, and should be done to both at