@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.
- package/README.md +69 -23
- package/dist/asset-reference.js +3 -11
- package/dist/asset-reference.js.map +1 -1
- package/dist/compare-code-units.d.ts +28 -0
- package/dist/compare-code-units.d.ts.map +1 -0
- package/dist/compare-code-units.js +33 -0
- package/dist/compare-code-units.js.map +1 -0
- package/dist/crawl-timing.d.ts +657 -0
- package/dist/crawl-timing.d.ts.map +1 -0
- package/dist/crawl-timing.js +765 -0
- package/dist/crawl-timing.js.map +1 -0
- package/dist/crawl.d.ts +2 -4
- package/dist/crawl.d.ts.map +1 -1
- package/dist/crawl.js +2 -4
- package/dist/crawl.js.map +1 -1
- package/dist/dirent-kind.d.ts +83 -0
- package/dist/dirent-kind.d.ts.map +1 -0
- package/dist/dirent-kind.js +129 -0
- package/dist/dirent-kind.js.map +1 -0
- package/dist/entrypoint.d.ts +54 -0
- package/dist/entrypoint.d.ts.map +1 -0
- package/dist/entrypoint.js +71 -0
- package/dist/entrypoint.js.map +1 -0
- package/dist/env-flag.d.ts +51 -0
- package/dist/env-flag.d.ts.map +1 -0
- package/dist/env-flag.js +62 -0
- package/dist/env-flag.js.map +1 -0
- package/dist/errors/errno.d.ts +51 -0
- package/dist/errors/errno.d.ts.map +1 -0
- package/dist/errors/errno.js +112 -0
- package/dist/errors/errno.js.map +1 -0
- package/dist/errors/vat-error.d.ts +60 -0
- package/dist/errors/vat-error.d.ts.map +1 -0
- package/dist/errors/vat-error.js +86 -0
- package/dist/errors/vat-error.js.map +1 -0
- package/dist/file-crawler.d.ts +60 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +133 -29
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs/file-hash.d.ts.map +1 -1
- package/dist/fs/file-hash.js +0 -1
- package/dist/fs/file-hash.js.map +1 -1
- package/dist/fs-utils.d.ts +422 -191
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +534 -204
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +5 -2
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +9 -7
- package/dist/fs.js.map +1 -1
- package/dist/git-run.d.ts +167 -0
- package/dist/git-run.d.ts.map +1 -0
- package/dist/git-run.js +154 -0
- package/dist/git-run.js.map +1 -0
- package/dist/git-snapshot.d.ts +199 -0
- package/dist/git-snapshot.d.ts.map +1 -0
- package/dist/git-snapshot.js +250 -0
- package/dist/git-snapshot.js.map +1 -0
- package/dist/git-tracker.d.ts +166 -3
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +250 -22
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +84 -1
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +208 -82
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +13 -4
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +12 -4
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +15 -6
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/glob/glob-pattern.js +5 -5
- package/dist/glob/glob-pattern.js.map +1 -1
- package/dist/index.d.ts +32 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +95 -50
- package/dist/index.js.map +1 -1
- package/dist/listing-refusal.d.ts +112 -0
- package/dist/listing-refusal.d.ts.map +1 -0
- package/dist/listing-refusal.js +105 -0
- package/dist/listing-refusal.js.map +1 -0
- package/dist/path-containment.d.ts +64 -0
- package/dist/path-containment.d.ts.map +1 -0
- package/dist/path-containment.js +111 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/path-core.d.ts +113 -18
- package/dist/path-core.d.ts.map +1 -1
- package/dist/path-core.js +139 -23
- package/dist/path-core.js.map +1 -1
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +42 -47
- package/dist/path-utils.js.map +1 -1
- package/dist/path.d.ts +1 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +1 -1
- package/dist/path.js.map +1 -1
- package/dist/process.d.ts +7 -0
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +7 -0
- package/dist/process.js.map +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +15 -9
- package/dist/project-utils.js.map +1 -1
- package/dist/project.d.ts +7 -5
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +7 -5
- package/dist/project.js.map +1 -1
- package/dist/safe-exec.d.ts +40 -11
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +111 -25
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-targets.d.ts +2 -2
- package/dist/skill-targets.js +2 -2
- package/dist/skill-test/auth-resolver.d.ts +10 -2
- package/dist/skill-test/auth-resolver.d.ts.map +1 -1
- package/dist/skill-test/auth-resolver.js +26 -11
- package/dist/skill-test/auth-resolver.js.map +1 -1
- package/dist/skill-test/index.d.ts +1 -1
- package/dist/skill-test/index.d.ts.map +1 -1
- package/dist/skill-test/index.js +1 -1
- package/dist/skill-test/index.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +30 -1
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +71 -5
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/skill-test/transcript.d.ts +41 -2
- package/dist/skill-test/transcript.d.ts.map +1 -1
- package/dist/skill-test/transcript.js +55 -10
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/spawn-hardened.js +0 -1
- package/dist/spawn-hardened.js.map +1 -1
- package/dist/stdio-blocking.js +13 -14
- package/dist/stdio-blocking.js.map +1 -1
- package/dist/test-helpers.d.ts +135 -100
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +224 -146
- package/dist/test-helpers.js.map +1 -1
- package/dist/testing/executables.d.ts +32 -0
- package/dist/testing/executables.d.ts.map +1 -0
- package/dist/testing/executables.js +70 -0
- package/dist/testing/executables.js.map +1 -0
- package/dist/testing/hostile-tree.d.ts +79 -0
- package/dist/testing/hostile-tree.d.ts.map +1 -0
- package/dist/testing/hostile-tree.js +164 -0
- package/dist/testing/hostile-tree.js.map +1 -0
- package/dist/testing/platform-gates.d.ts +31 -0
- package/dist/testing/platform-gates.d.ts.map +1 -0
- package/dist/testing/platform-gates.js +31 -0
- package/dist/testing/platform-gates.js.map +1 -0
- package/dist/testing/temp-dir.d.ts +255 -0
- package/dist/testing/temp-dir.d.ts.map +1 -0
- package/dist/testing/temp-dir.js +363 -0
- package/dist/testing/temp-dir.js.map +1 -0
- package/dist/testing.d.ts +94 -3
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +119 -3
- package/dist/testing.js.map +1 -1
- package/dist/text-content.d.ts +197 -0
- package/dist/text-content.d.ts.map +1 -0
- package/dist/text-content.js +354 -0
- package/dist/text-content.js.map +1 -0
- package/dist/text-file.d.ts +45 -0
- package/dist/text-file.d.ts.map +1 -0
- package/dist/text-file.js +51 -0
- package/dist/text-file.js.map +1 -0
- package/dist/text.d.ts +16 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +16 -0
- package/dist/text.js.map +1 -0
- package/dist/timing-dump.d.ts +136 -0
- package/dist/timing-dump.d.ts.map +1 -0
- package/dist/timing-dump.js +229 -0
- package/dist/timing-dump.js.map +1 -0
- package/dist/windows-shell.d.ts +0 -1
- package/dist/windows-shell.d.ts.map +1 -1
- package/dist/windows-shell.js +0 -1
- package/dist/windows-shell.js.map +1 -1
- package/dist/zod-introspection.js +2 -2
- package/dist/zod-introspection.js.map +1 -1
- package/eslint/README.md +223 -49
- package/eslint/index.cjs +111 -97
- package/eslint/index.d.cts +16 -6
- package/eslint/rules/callee-name.cjs +23 -0
- package/eslint/rules/commands-import-boundary.cjs +148 -0
- package/eslint/rules/dead-import.cjs +1 -1
- package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
- package/eslint/rules/eslint-rule-factory.cjs +5 -2
- package/eslint/rules/exempt-path-matcher.cjs +2 -1
- package/eslint/rules/explicit-zod-strictness.cjs +120 -0
- package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
- package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
- package/eslint/rules/no-bare-symlink-in-tests.cjs +213 -0
- package/eslint/rules/no-blind-catch.cjs +171 -0
- package/eslint/rules/no-child-process-execSync.cjs +8 -0
- package/eslint/rules/no-decaying-referent.cjs +217 -0
- package/eslint/rules/no-dotdot-containment.cjs +160 -0
- package/eslint/rules/no-file-url-string-concat.cjs +4 -1
- package/eslint/rules/no-fragile-entrypoint-guard.cjs +228 -0
- package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
- package/eslint/rules/no-fs-promises-cp.cjs +7 -0
- package/eslint/rules/no-fs-realpathSync.cjs +8 -0
- package/eslint/rules/no-hardcoded-path-split.cjs +29 -18
- package/eslint/rules/no-io-in-unit-tier.cjs +129 -0
- package/eslint/rules/no-literal-process-exit.cjs +129 -0
- package/eslint/rules/no-manual-path-normalize.cjs +136 -107
- package/eslint/rules/no-os-tmpdir.cjs +8 -0
- package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
- package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
- package/eslint/rules/no-path-startswith.cjs +7 -2
- package/eslint/rules/no-process-exit-in-phase.cjs +127 -0
- package/eslint/rules/no-raw-node-path.cjs +563 -0
- package/eslint/rules/no-raw-text-decode.cjs +305 -0
- package/eslint/rules/no-registry-count-pin.cjs +255 -0
- package/eslint/rules/no-self-package-import.cjs +168 -0
- package/eslint/rules/no-test-scoped-functions.cjs +10 -3
- package/eslint/rules/no-unix-shell-commands.cjs +6 -2
- package/eslint/rules/no-unsafe-root-join.cjs +23 -2
- package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
- package/eslint/rules/no-version-literal.cjs +151 -0
- package/eslint/rules/prefer-startswith-over-regex.cjs +9 -0
- package/eslint/rules/require-justified-skip.cjs +8 -2
- package/eslint/rules/safe-import.cjs +1 -1
- package/package.json +16 -16
- package/dist/link-auth/build-headers.d.ts +0 -34
- package/dist/link-auth/build-headers.d.ts.map +0 -1
- package/dist/link-auth/build-headers.js +0 -58
- package/dist/link-auth/build-headers.js.map +0 -1
- package/dist/link-auth/expand-macro.d.ts +0 -38
- package/dist/link-auth/expand-macro.d.ts.map +0 -1
- package/dist/link-auth/expand-macro.js +0 -133
- package/dist/link-auth/expand-macro.js.map +0 -1
- package/dist/link-auth/macros.yaml +0 -50
- package/dist/link-auth/resolve-token.d.ts +0 -83
- package/dist/link-auth/resolve-token.d.ts.map +0 -1
- package/dist/link-auth/resolve-token.js +0 -101
- package/dist/link-auth/resolve-token.js.map +0 -1
- package/dist/link-auth/resolve.d.ts +0 -102
- package/dist/link-auth/resolve.d.ts.map +0 -1
- package/dist/link-auth/resolve.js +0 -66
- package/dist/link-auth/resolve.js.map +0 -1
- package/dist/link-auth/rewrite.d.ts +0 -52
- package/dist/link-auth/rewrite.d.ts.map +0 -1
- package/dist/link-auth/rewrite.js +0 -102
- package/dist/link-auth/rewrite.js.map +0 -1
- package/dist/link-auth/select-provider.d.ts +0 -30
- package/dist/link-auth/select-provider.d.ts.map +0 -1
- package/dist/link-auth/select-provider.js +0 -55
- package/dist/link-auth/select-provider.js.map +0 -1
- package/dist/link-auth/template.d.ts +0 -40
- package/dist/link-auth/template.d.ts.map +0 -1
- package/dist/link-auth/template.js +0 -89
- package/dist/link-auth/template.js.map +0 -1
- package/dist/link-auth/transforms.d.ts +0 -46
- package/dist/link-auth/transforms.d.ts.map +0 -1
- package/dist/link-auth/transforms.js +0 -52
- package/dist/link-auth/transforms.js.map +0 -1
- package/dist/template-entry.d.ts +0 -10
- package/dist/template-entry.d.ts.map +0 -1
- package/dist/template-entry.js +0 -10
- package/dist/template-entry.js.map +0 -1
- package/dist/template.d.ts +0 -7
- package/dist/template.d.ts.map +0 -1
- package/dist/template.js +0 -18
- package/dist/template.js.map +0 -1
- package/eslint/rules/no-path-join.cjs +0 -14
- package/eslint/rules/no-path-relative.cjs +0 -14
- package/eslint/rules/no-path-resolve.cjs +0 -14
- package/eslint/rules/path-function-rule-factory.cjs +0 -496
package/eslint/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
ESLint rules that enforce the cross-platform and agentic-code safety helpers in the rest of [`@vibe-agent-toolkit/utils`](https://www.npmjs.com/package/@vibe-agent-toolkit/utils).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Every rule here is derived from a bug that actually shipped: `os.tmpdir()` returning an 8.3 short path on a Windows CI runner, `path.join()` producing backslashes that then failed a string comparison, `await import(absolutePath)` throwing on Windows without a `file://` URL, `execSync()` interpolating a caller-controlled string into a shell. The [rule table](#rules) is generated from the rules' own metadata and carries the counts.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -25,7 +25,7 @@ export default [
|
|
|
25
25
|
];
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
`configs.recommended` registers the plugin under the `@vibe-agent-toolkit` namespace and enables the **cross-platform safety core
|
|
28
|
+
`configs.recommended` registers the plugin under the `@vibe-agent-toolkit` namespace and enables the **cross-platform safety core**: every rule whose `meta.docs.recommended` is true, at the severity it declares (see [Severities](#severities)). The rest are opt-in; the [rule table](#rules) marks each rule's `recommended` severity, and `—` means not in `recommended`.
|
|
29
29
|
|
|
30
30
|
To pick rules yourself, register the plugin and name them:
|
|
31
31
|
|
|
@@ -37,7 +37,7 @@ export default [
|
|
|
37
37
|
files: ['**/*.{ts,tsx,js,cjs,mjs}'],
|
|
38
38
|
plugins: { '@vibe-agent-toolkit': vat },
|
|
39
39
|
rules: {
|
|
40
|
-
'@vibe-agent-toolkit/no-path
|
|
40
|
+
'@vibe-agent-toolkit/no-raw-node-path': 'error',
|
|
41
41
|
'@vibe-agent-toolkit/no-os-tmpdir': 'error',
|
|
42
42
|
},
|
|
43
43
|
},
|
|
@@ -50,7 +50,7 @@ A rule that bans `os.tmpdir()` has to let *something* call it — the file that
|
|
|
50
50
|
|
|
51
51
|
```js
|
|
52
52
|
'@vibe-agent-toolkit/no-os-tmpdir': ['error', { exemptFiles: ['src/paths.ts'] }],
|
|
53
|
-
'@vibe-agent-toolkit/no-path
|
|
53
|
+
'@vibe-agent-toolkit/no-raw-node-path': ['error', { exemptFiles: [
|
|
54
54
|
'src/paths.ts',
|
|
55
55
|
'test/paths.test.ts', // asserts the platform-native behavior the wrapper hides
|
|
56
56
|
] }],
|
|
@@ -64,75 +64,216 @@ Taken to its limit, a **bare filename with no `/` exempts that filename everywhe
|
|
|
64
64
|
|
|
65
65
|
The option **replaces** any default rather than merging with it, and unknown option keys are a config error — a typo'd `exemptFile` must fail loudly rather than quietly exempt nothing.
|
|
66
66
|
|
|
67
|
-
The rules taking `exemptFiles` are `no-path
|
|
67
|
+
The rules taking `exemptFiles` are `no-raw-node-path`, `no-os-tmpdir`, `no-fs-mkdirSync`, `no-fs-realpathSync`, `no-child-process-execSync`, `no-fs-promises-cp`, `no-raw-text-decode` and `no-bare-symlink-in-tests`.
|
|
68
68
|
|
|
69
69
|
## Rules
|
|
70
70
|
|
|
71
|
-
The
|
|
71
|
+
The table is **generated** from each rule's `meta.docs` by `bun run generate:claude-md` (repo root; the `eslint-rules` block), and `validate-structure` fails when the committed copy drifts from the rules — so edit the rule, never the table. The "Subpath" column names the `@vibe-agent-toolkit/utils` subpath the replacement lives on; ✓ marks an auto-fix. The **`recommended`** column is the severity `configs.recommended` assigns — `—` means the rule is **not** in `recommended` and must be enabled explicitly.
|
|
72
72
|
|
|
73
73
|
**The auto-fix writes the import to the subpath in that column**, not to the barrel — `--fix` on a raw `path.join()` inserts `import { safePath } from '@vibe-agent-toolkit/utils/path'`. A file that already reaches the helper through the barrel keeps its existing import and only has the call rewritten: adding a second binding of the same name is a `SyntaxError`, not a redundant import.
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
<!-- gen:eslint-rules -->
|
|
76
|
+
35 rules; 7 auto-fix. `configs.recommended` enables 18 of them (16 at `error`, 2 at `warn`); `—` in the last column means the rule ships but must be enabled by name.
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
#### Path handling
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
81
|
+
|---|---|---|---|---|---|
|
|
82
|
+
| `no-hardcoded-path-split` | `split('/')` / `split('\\')` on a path | `path.basename()`, or `toForwardSlash()` / `toForwardSlashAnyPlatform()` first | `/path` | | `error` |
|
|
83
|
+
| `no-manual-path-normalize` | hand-rolled `.replace(/\\/g, '/')` / `split(path.sep).join('/')` | `toForwardSlash()` (native paths) / `toForwardSlashAnyPlatform()` (authored text) | `/path` | ✓ | `error` |
|
|
84
|
+
| `no-path-operations-in-comparisons` | raw `path.*()` results in string comparisons | wrap in `toForwardSlash()` | `/path` | | `error` |
|
|
85
|
+
| `no-path-sep-in-strings` | `path.sep` embedded in a string literal | `toForwardSlash()` | `/path` | | `error` |
|
|
86
|
+
| `no-path-startswith` | `path.startsWith()` on a raw path | `toForwardSlash()` first | `/path` | | `error` |
|
|
87
|
+
| `no-raw-node-path` | `path.join()`, `path.resolve()`, `path.relative()` | `safePath.join()` / `.resolve()` / `.relative()` | `/path` | ✓ | `warn` |
|
|
88
|
+
| `no-unsafe-root-join` | `safePath.join(someRoot, x)` where `x` can escape | `safePath.joinUnderRoot()` | `/path` | | — |
|
|
83
89
|
|
|
84
|
-
|
|
90
|
+
#### Filesystem and process
|
|
91
|
+
|
|
92
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
93
|
+
|---|---|---|---|---|---|
|
|
94
|
+
| `no-bare-executable-spawn` | Disallow spawning 'git' or 'node' by bare name — resolve the executable once (process.execPath; NODE_EXECUTABLE / gitExecutable() in tests) and spawn the absolute path | — | — | | `error` |
|
|
95
|
+
| `no-bare-symlink-in-tests` | unguarded `fs.symlinkSync()` / `fs.promises.symlink()` | in tests: `createSymlink(cap, …)` / `createSymlinkAsync(cap, …)`; in shipped code: a win32 junction, or a `catch` naming the privilege | `/testing` | | — |
|
|
96
|
+
| `no-child-process-execSync` | `child_process.execSync()` | `safeExecSync()` | `/process` | ✓ | `error` |
|
|
97
|
+
| `no-fs-mkdirSync` | `fs.mkdirSync()` | `mkdirSyncReal()` | `/fs` | ✓ | `error` |
|
|
98
|
+
| `no-fs-promises-cp` | `cp()` from `node:fs/promises` (drops nested files on Node 22) | `cpSync()` from `node:fs` | — | ✓ | `error` |
|
|
99
|
+
| `no-fs-realpathSync` | `fs.realpathSync()` | `normalizePath()` | `/fs` | ✓ | `error` |
|
|
100
|
+
| `no-os-tmpdir` | `os.tmpdir()` (8.3 short names on Windows) | `normalizedTmpdir()` | `/fs` | ✓ | `error` |
|
|
101
|
+
| `no-unix-shell-commands` | `tar`, `grep`, `rm`, `echo`, … spawned directly | Node APIs, or a portable script fixture | — | | `error` |
|
|
85
102
|
|
|
86
|
-
|
|
103
|
+
#### URLs and dynamic imports
|
|
87
104
|
|
|
88
|
-
|
|
105
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
106
|
+
|---|---|---|---|---|---|
|
|
107
|
+
| `no-bare-dynamic-import-path` | `await import(absolutePath)` | `dynamicImportPath()` / `pathToFileURL(p).href` | `/fs` | | `error` |
|
|
108
|
+
| `no-file-url-string-concat` | `` `file://${p}` `` | `pathToFileURL(p).href` | — | | `error` |
|
|
109
|
+
| `no-url-pathname-for-fs` | `new URL(x, import.meta.url).pathname` as a filesystem path | `resolveFromImportMeta()` / `fileURLToPath()` | `/fs` | | `error` |
|
|
89
110
|
|
|
90
|
-
|
|
111
|
+
#### Entrypoint guards
|
|
91
112
|
|
|
92
113
|
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
93
114
|
|---|---|---|---|---|---|
|
|
94
|
-
| `no-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
| `no-path-startswith` | `path.startsWith()` on a raw path | `toForwardSlash()` first | `/path` | | `error` |
|
|
98
|
-
| `no-hardcoded-path-split` | `split('/')` / `split('\\')` on a path | `path.basename()`, or `toForwardSlash()` first | `/path` | | `error` |
|
|
99
|
-
| `no-path-sep-in-strings` | `path.sep` embedded in a string literal | `toForwardSlash()` | `/path` | | `error` |
|
|
100
|
-
| `no-manual-path-normalize` | hand-rolled `.replace(/\\/g, '/')` | `toForwardSlash()` | `/path` | ✓ | `error` |
|
|
101
|
-
| `no-path-operations-in-comparisons` | raw `path.*()` results in string comparisons | wrap in `toForwardSlash()` | `/path` | | `error` |
|
|
102
|
-
| `no-unsafe-root-join` | `safePath.join(someRoot, x)` where `x` can escape | `safePath.joinUnderRoot()` | `/path` | | — |
|
|
103
|
-
|
|
104
|
-
### Filesystem and process
|
|
115
|
+
| `no-fragile-entrypoint-guard` | `import.meta.main`; `import.meta.url === pathToFileURL(process.argv[1]).href`; `fileURLToPath(import.meta.url) === process.argv[1]` | `isEntrypoint(import.meta.url)` | `/process` | | — |
|
|
116
|
+
|
|
117
|
+
#### Process control
|
|
105
118
|
|
|
106
119
|
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
107
120
|
|---|---|---|---|---|---|
|
|
108
|
-
| `no-
|
|
109
|
-
| `no-fs-realpathSync` | `fs.realpathSync()` | `normalizePath()` | `/fs` | ✓ | `error` |
|
|
110
|
-
| `no-fs-mkdirSync` | `fs.mkdirSync()` | `mkdirSyncReal()` | `/fs` | ✓ | `error` |
|
|
111
|
-
| `no-fs-promises-cp` | `cp()` from `node:fs/promises` (drops nested files on Node 22) | `cpSync()` from `node:fs` | — | ✓ | `error` |
|
|
112
|
-
| `no-child-process-execSync` | `child_process.execSync()` | `safeExecSync()` | `/process` | ✓ | `error` |
|
|
113
|
-
| `no-unix-shell-commands` | `tar`, `grep`, `rm`, `echo`, … spawned directly | Node APIs, or a portable script fixture | — | | `error` |
|
|
121
|
+
| `no-process-exit-in-phase` | `process.exit()` inside a function named `…Phase` | return the exit code from the phase; only the command wrapper exits | — | | — |
|
|
114
122
|
|
|
115
|
-
|
|
123
|
+
#### Error handling
|
|
116
124
|
|
|
117
|
-
|
|
125
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
126
|
+
|---|---|---|---|---|---|
|
|
127
|
+
| `no-blind-catch` | a `catch` that neither reads its error nor throws | narrow on the error and rethrow the rest, or carry it into the result | — | | `warn` |
|
|
128
|
+
|
|
129
|
+
#### Content decoding
|
|
130
|
+
|
|
131
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
132
|
+
|---|---|---|---|---|---|
|
|
133
|
+
| `no-raw-text-decode` | `buf.toString('utf-8')`, `new TextDecoder(…)`, `readFile(p, 'utf-8')` | one project-owned decoding seam | — | | — |
|
|
134
|
+
|
|
135
|
+
#### Build correctness
|
|
118
136
|
|
|
119
137
|
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
120
138
|
|---|---|---|---|---|---|
|
|
121
|
-
| `no-
|
|
122
|
-
| `no-bare-dynamic-import-path` | `await import(absolutePath)` | `dynamicImportPath()` / `pathToFileURL(p).href` | `/fs` | | `error` |
|
|
123
|
-
| `no-file-url-string-concat` | `` `file://${p}` `` | `pathToFileURL(p).href` | — | | `error` |
|
|
139
|
+
| `no-self-package-import` | importing the enclosing package by its own name | a relative path to the defining module | — | | — |
|
|
124
140
|
|
|
125
|
-
|
|
141
|
+
#### Code and test hygiene
|
|
126
142
|
|
|
127
143
|
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
128
144
|
|---|---|---|---|---|---|
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `require-justified-skip` | unannotated `it.skip`/`it.todo`, tautological assertions, empty test bodies | a `SKIP(#123): reason` annotation, or a real assertion | — |
|
|
145
|
+
| `no-test-scoped-functions` | helper functions declared inside `describe`/`it` | module scope | — | | — |
|
|
146
|
+
| `prefer-startswith-over-regex` | `/^foo/.test(s)`, `` /^\*glob/.test(s) ``, `const RE = /^foo/; RE.test(s)` | `s.startsWith('foo')` | — | | `error` |
|
|
147
|
+
| `require-justified-skip` | unannotated `it.skip`/`it.todo`, tautological assertions, empty test bodies | a `SKIP(#123): reason` annotation, or a real assertion | — | | — |
|
|
148
|
+
|
|
149
|
+
#### Other
|
|
150
|
+
|
|
151
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
152
|
+
|---|---|---|---|---|---|
|
|
153
|
+
| `commands-import-boundary` | Disallow filesystem and internal-module imports in command modules — a command calls a declared enumeration lane, it does not become one | — | — | | — |
|
|
154
|
+
| `dirent-type-needs-symlink-check` | Require an isSymbolicLink() check on a Dirent before isFile()/isDirectory() — both are false for a symlink, so an unchecked walk drops links silently | — | — | | — |
|
|
155
|
+
| `explicit-zod-strictness` | Require every z.object({...}) to declare its unknown-key policy in the same chain — .strict(), .passthrough(), .loose() or an explicit .strip() — because the default silently strips keys | — | — | | — |
|
|
156
|
+
| `no-decaying-referent` | Disallow issue/PR numbers, ISO dates and named people in src comments — they decay in place; the rule belongs in the comment and the history in the commit, CHANGELOG or docs | — | — | | — |
|
|
157
|
+
| `no-dotdot-containment` | Disallow startsWith('..') / includes('..') / split-and-hunt as a path containment check — use the realpath-based isUnderRoot() helper | — | — | | — |
|
|
158
|
+
| `no-io-in-unit-tier` | Disallow child_process imports and mkdtemp/spawn/exec calls in unit-tier test files — a test that spawns or writes to disk belongs in the integration or system tier | — | — | | — |
|
|
159
|
+
| `no-literal-process-exit` | Disallow process.exit(<number>) and process.exitCode = <number> — name the meaning with the ExitCode enum so every command shares one exit contract | — | — | | — |
|
|
160
|
+
| `no-registry-count-pin` | Disallow pinning the size of an imported registry with a literal in tests — toHaveLength(27) on something pulled from src is a change detector fixed by retyping | — | — | | — |
|
|
161
|
+
| `no-version-literal` | Disallow z.literal(<number>) on a version-named field and <X>_VERSION = <number> constants — a hand-bumped integer deciding data validity is the shape CLAUDE.md bans | — | — | | — |
|
|
162
|
+
<!-- /gen:eslint-rules -->
|
|
163
|
+
|
|
164
|
+
### Pointing the fix at your own re-export seam — `safeModule`
|
|
165
|
+
|
|
166
|
+
If your repo re-exports these helpers through its own module, the defaults above are wrong for you, and not merely stylistically: in a workspace with isolated `node_modules` (pnpm, Yarn PnP), an import of a package the receiving package does not declare **fails to resolve**. One adopter measured 620 files across 52 such packages. Point the rule at the module that resolves where the fix lands:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
'@vibe-agent-toolkit/no-raw-node-path': ['warn', { safeModule: '@acme/dev-tools/paths' }],
|
|
170
|
+
'@vibe-agent-toolkit/no-os-tmpdir': ['error', { safeModule: '@acme/dev-tools/fs' }],
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
It is **per-rule**, not one shared `settings` key, because a seam need not split its symbols the way this package does — an adopter whose narrow entry carried `normalizedTmpdir()` but not `safePath` needed the two rules pointed at different modules, which a single key cannot express. Note the two lines above therefore name *different* modules. It composes with `exemptFiles`, and it changes the error message as well as the fix, so the advice never names a module you don't use. Every rule that names a module accepts it, including the ones that only advise and never fix.
|
|
174
|
+
|
|
175
|
+
> **Point each rule at a module that exports the symbol *that rule writes*, not merely one that resolves.** This is the failure mode worth spending a minute on, because it is the quiet one. A `safeModule` that doesn't resolve fails loudly and immediately. A `safeModule` that resolves but lacks the symbol passes every resolution check — including an explicit `import()` probe — and then throws `Cannot read properties of undefined` at each call site, once per fixed file. Aim `no-raw-node-path` at an entry without `safePath` and you get a green `--fix`, a green install, and thousands of latent `TypeError`s. The rules cannot check this for you: verifying it would mean resolving and importing your module from inside the linter, which is neither its job nor reliable from wherever ESLint happens to be running.
|
|
176
|
+
|
|
177
|
+
Two ways your target can be wrong, which surface differently: `ERR_MODULE_NOT_FOUND` means the receiving package doesn't declare it at all, while `ERR_PACKAGE_PATH_NOT_EXPORTED` means it does — but the version resolved *at that location* doesn't export the subpath. In a monorepo mid-upgrade those differ, so the question is never "does the package declare it" but "does the resolved version there export it."
|
|
178
|
+
|
|
179
|
+
### Notes on individual rules
|
|
180
|
+
|
|
181
|
+
#### `no-raw-node-path`
|
|
182
|
+
|
|
183
|
+
One rule over the three `node:path` functions `safePath` wraps. Its `functions` option is the table (default `['join', 'resolve', 'relative']`; each maps to `safePath.<fn>()`), so one `--fix` pass carries every wrapped call in a file to the same inserted import. Narrow it to migrate one function at a time: `['warn', { functions: ['join'] }]`.
|
|
184
|
+
|
|
185
|
+
#### `no-bare-symlink-in-tests`
|
|
186
|
+
|
|
187
|
+
**`no-bare-symlink-in-tests` reports two different remedies, and the name is narrower than the rule.**
|
|
188
|
+
Creating a symlink on Windows requires `SeCreateSymbolicLinkPrivilege` — Developer Mode or an
|
|
189
|
+
elevated shell — which most user machines and CI agents lack. In a **test file** the fix is to probe
|
|
190
|
+
with `symlinkCapability()` and pass the resulting token to `createSymlink()`, so a host without the
|
|
191
|
+
privilege produces a visible `skip()` rather than a failure or a silently-swallowed one. In **shipped
|
|
192
|
+
code** there is deliberately no wrapper to route through: `createSymlink()` lives on the `/testing`
|
|
193
|
+
subpath, and pointing production code at a test helper would be worse advice than the bare call.
|
|
194
|
+
There the guidance is a junction for a directory link on win32 (no elevation required), or catching
|
|
195
|
+
the failure and naming the missing privilege. Declaring a platform out of scope is legitimate —
|
|
196
|
+
say so in an `eslint-disable` justification, which is what `vat agent install --dev` does.
|
|
197
|
+
|
|
198
|
+
`exemptFiles` matters here: the implementation file holding the sanctioned `symlinkSync` is not a
|
|
199
|
+
test file, so it needs an explicit exemption once the rule covers shipped code.
|
|
200
|
+
|
|
201
|
+
#### Member-call rules and the receiver
|
|
202
|
+
|
|
203
|
+
The member-call rules here check the **receiver**, not just the method name, so `env.tmpdir()` on some unrelated object is not a finding — and the namespace they check for can be bound by a static `import * as os`, by `const os = require('node:os')`, or by `const os = await import('node:os')`. The fix replaces the whole callee (`os.tmpdir()` → `normalizedTmpdir()`), which is correct however the binding was made. Matching the method name alone was the earlier behaviour and it produced `os.normalizedTmpdir()` — a method that does not exist, compiles, and throws.
|
|
204
|
+
|
|
205
|
+
#### `no-fragile-entrypoint-guard`
|
|
206
|
+
|
|
207
|
+
All three banned spellings fail the same way: the guard answers **false for the script it is guarding**, so the process exits 0 having run nothing — the quietest failure a CLI has.
|
|
208
|
+
|
|
209
|
+
`import.meta.main` shipped in Node **24.2 / 22.18**. Below that it is `undefined`, measured on a real 22.13.0, and this package's own floor is `>=22`. A repository-structure gate guarded that way printed nothing and exited 0 on the exact Node its CI job installed.
|
|
210
|
+
|
|
211
|
+
The other two are one defect wearing two spellings: a raw string comparison of where the module lives against `process.argv[1]`, with no realpath pass, written either in URL space (`pathToFileURL` the argv path) or in path space (`fileURLToPath` the module URL). A `node_modules/.bin` entry is a **symlink**, so `process.argv[1]` is the link and the module's own location is the resolved target: the strings differ and the guard is false, in either space. Measured false on Node 22.14.0 and 24.13.1 alike, where `isEntrypoint()` is true. Only `process.argv[1]` counts — `argv[2]` and up are ordinary CLI arguments. `import.meta.url`, `import.meta.dirname` and `import.meta.filename` are untouched; only `.main` is banned.
|
|
212
|
+
|
|
213
|
+
Not in `recommended` because the first half depends on **your** Node floor — at or above 24.2 / 22.18, `import.meta.main` is correct. The argv compares depend on nothing and are a defect everywhere; the two share a rule id, so enable it explicitly if your floor is below 24.2 / 22.18 or you ship a `bin`.
|
|
214
|
+
|
|
215
|
+
#### `no-blind-catch`
|
|
216
|
+
|
|
217
|
+
`try { return statSync(p); } catch { return null; }` answers three different questions with one word. *Not there* is what the author meant. *Refused* — `EACCES`, `EPERM`, `ELOOP` — is a file that exists and could not be read, now reported as absent. *Bug* — a `TypeError` two frames down — is a defect, now reported as a file that is not there. All three exit 0, and the tool is quietest exactly where it is most wrong.
|
|
218
|
+
|
|
219
|
+
The rule is a floor, deliberately syntactic: a catch is fine if it **references its error binding anywhere** in the body (`isFilesystemAccessError(e)`, `e instanceof X`, `e.code === 'ENOENT'`, `errors.push(String(e))`, `log.warn(e)`) or **throws at its own level** (a rethrow, or a translation into a louder error; a `throw` inside a nested function is a promise to fail later, not a rethrow). It cannot tell `narrow(e)` from `log(e)` and does not try. What it guarantees is the weaker, enforceable property — *the error was looked at before it was discarded* — which every one of the shipped defects lacked.
|
|
220
|
+
|
|
221
|
+
This is the one seam `tsc` cannot see. When a callee learns to throw where it used to return — a crawler that starts refusing an unreadable directory instead of skipping it — every caller whose contract changed by TYPE fails to compile and gets fixed; the caller with a blind `catch` compiles unchanged and absorbs the new refusal. That is how a refuse-by-default crawler shipped under a `catch { return null }` that turned `vat audit` into a scan of nothing.
|
|
222
|
+
|
|
223
|
+
There is no annotation escape hatch. Every flagged site has a legitimate rewrite, and an `eslint-disable-next-line` with a reason is the escape hatch ESLint already provides — visible in the diff, and countable with `rg 'eslint-disable.*no-blind-catch'`. `warn` in `recommended` because there is no autofix and an existing tree has many (this repo measured 202 across ~200k lines); the hazard is real at each one.
|
|
224
|
+
|
|
225
|
+
#### `no-raw-text-decode`
|
|
226
|
+
|
|
227
|
+
`buf.toString('utf-8')` ignores every byte-order mark and cannot express UTF-16BE at all — Node's `Buffer` has no such encoding. A UTF-16 document therefore decodes to NUL-interleaved mojibake, and whatever sniffs for binary content downstream believes it. PowerShell 5.1's `Out-File` and `>` write UTF-16LE by default, so this is a Windows-authored file, not an exotic one.
|
|
228
|
+
|
|
229
|
+
This rule has no wrapper to point at, because the seam is yours: write one decoder, name it with `safeModule`, and exempt its own file with `exemptFiles`. Put the decoder at the **bottom** of your dependency arrow — a seam in a leaf package cannot be imported by the primitive packages the rule also lints, and those files would then have no legal way to comply.
|
|
230
|
+
|
|
231
|
+
**Not every `'utf-8'` read is a content read**, and this is the distinction that decides whether the rule survives. Three categories:
|
|
232
|
+
|
|
233
|
+
1. **A document you did not write** — an adopter's markdown, config, schema, `.gitignore`, `package.json`. The encoding must be **discovered**. This is the rule's target.
|
|
234
|
+
2. **An artifact your project wrote** — its own cache entry, its own published asset. The encoding was **chosen at the write**; reading it back the same way is a closed loop.
|
|
235
|
+
3. **Bytes that were never a file** — subprocess stdout, an HTTP body, a Buffer you built. The **producer's contract** decides.
|
|
236
|
+
|
|
237
|
+
Static analysis cannot tell them apart, so the rule reports all three and you settle 2 and 3 at the call site with a one-line `eslint-disable-next-line` that **names the writer or the producer**:
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
// eslint-disable-next-line @vibe-agent-toolkit/no-raw-text-decode -- subprocess stdout; producer is the credential helper spawned above
|
|
241
|
+
const out = result.stdout.toString('utf8');
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
That gives a reviewer a falsifiable test: a justification that cannot name who wrote the bytes is a category-1 call wearing a disable comment. Do not settle these by adding paths to `exemptFiles` — that list is for the seam's own implementation file.
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
{
|
|
248
|
+
files: ['src/corpus/**/*.ts'],
|
|
249
|
+
rules: {
|
|
250
|
+
'@vibe-agent-toolkit/no-raw-text-decode': ['error', {
|
|
251
|
+
safeModule: '@my-org/resources',
|
|
252
|
+
exemptFiles: ['src/corpus/text-content.ts'],
|
|
253
|
+
}],
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Only a string **literal** encoding triggers it. `buf.toString(enc)` is deliberately not reported: without type information it is indistinguishable from `n.toString(radix)`, and `readFile(p, cb)` from `readFile(p, encoding)`.
|
|
259
|
+
|
|
260
|
+
#### `no-self-package-import`
|
|
261
|
+
|
|
262
|
+
A file inside `packages/foo` that writes `import … from '@scope/foo'` resolves out through `node_modules` to its own `package.json`, whose `types` point at `./dist/index.d.ts` — a file the compiler is in the middle of producing. It works only by a TypeScript courtesy: while `dist` **is** the running project's output path, that declaration is recognised as the project's own output and the import is redirected back to `src`, so it resolves with no `dist/` on disk.
|
|
263
|
+
|
|
264
|
+
Change `outDir` — to a staging directory that makes emit atomic, say — and the redirect is gone, tsc looks for a literal `dist/index.d.ts`, and a tree that has never been built has none:
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
error TS2307: Cannot find module '@scope/foo' or its corresponding type declarations.
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The knock-on `TS2339`s land wherever a local type extended one of the now-unresolved imports, which is what makes it read as a type bug in code nobody touched.
|
|
271
|
+
|
|
272
|
+
It is latent by construction, and worse, **it is invisible to any tree that has built before**: a stale `dist/` satisfies the literal lookup, so the build passes by typechecking against the *previous* build's declarations. In a monorepo whose worktrees live inside the main checkout, resolution walks up past the worktree and satisfies it from the *parent checkout's* `dist/`. Both are green locally and red in CI, which is the only genuinely pristine tree. Lint is the only stage that sees it on the author's machine.
|
|
132
273
|
|
|
133
274
|
### What `recommended` deliberately leaves out
|
|
134
275
|
|
|
135
|
-
|
|
276
|
+
Every rule marked `—` in the table ships without riding in `recommended`, and each states its reason beside its own `recommended: false` in `eslint/rules/<rule>.cjs`. The reasons fall into a few families:
|
|
136
277
|
|
|
137
278
|
**Test-style opinions** — `no-test-scoped-functions` (where a helper may be declared) and `require-justified-skip` (the annotation grammar for a disabled test). Neither is a portability or correctness fact, and installing this package for `safePath.join()` should not also import someone else's test conventions. Both are worth turning on deliberately.
|
|
138
279
|
|
|
@@ -147,7 +288,29 @@ safePath.join(base, userInput) // SILENT — the shape it exists to
|
|
|
147
288
|
|
|
148
289
|
A rule that misses its own target does not belong in a config named `recommended` at any severity: a safety core that cries wolf teaches people to ignore it, and that costs you the true positives too. It still ships, and it still earns `error` when scoped to directories where a path escape is a security boundary — which is how this repo uses it, on its skill-test staging code. It will return to `recommended` when it keys on taint rather than on naming.
|
|
149
290
|
|
|
150
|
-
|
|
291
|
+
**No wrapper to point at** — `no-raw-text-decode`. Every other rule in this pack names a replacement this package publishes; this one names a decoding seam that only exists once *you* write it. Shipped in `recommended`, its every message would read "use `decodeTextContent()` from your content-decoding module", which is advice nobody can follow. Turn it on with `safeModule` and `exemptFiles` set, as shown above.
|
|
292
|
+
|
|
293
|
+
**Needs an option, and only in the directories you compile** — `no-self-package-import`. The import it bans is a genuine build-breaker with no style opinion in it, but the rule cannot discover on its own which package a file is in: reading `package.json` would mean `require('node:fs')`, and every module on this subpath is plain data that requires *nothing* — not `eslint`, not a third-party package, not even a Node builtin. That is what keeps `eslint` an optional peer dependency and lets these rules ship as a subpath of a runtime package rather than as one of their own. So the caller names the package. The caller is a config file, which already runs in full Node and can read every manifest it likes:
|
|
294
|
+
|
|
295
|
+
```js
|
|
296
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
297
|
+
|
|
298
|
+
export default readdirSync('packages').flatMap((dir) => {
|
|
299
|
+
const { name } = JSON.parse(readFileSync(`packages/${dir}/package.json`, 'utf8'));
|
|
300
|
+
return [{
|
|
301
|
+
files: [`packages/${dir}/src/**/*.ts`],
|
|
302
|
+
rules: { '@vibe-agent-toolkit/no-self-package-import': ['error', { packageName: name }] },
|
|
303
|
+
}];
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Scope it to the sources you **compile**. Test and example trees — normally excluded from the build — import their own package by name **on purpose**, to exercise the public entry point exactly as a consumer does. This repo has ~10 such imports, every one of them correct.
|
|
308
|
+
|
|
309
|
+
**A convention of one repository, not a portable fact** — `no-process-exit-in-phase` keys on a `…Phase` function-name suffix, and `no-fragile-entrypoint-guard`'s `import.meta.main` half is only wrong below a Node floor that is yours to know. Both ship and are enabled here by name.
|
|
310
|
+
|
|
311
|
+
**Half its advice is unreachable without a helper you may not have** — `no-bare-symlink-in-tests`. In a test file it points at `createSymlink()` / `createSymlinkAsync()`, which live on *this* package's `./testing` subpath and route through a probed capability token; an adopter on a different test runner, or with no symlink-heavy tests, should not silently inherit that opinion — nor the vitest-specific `skip()` idiom the message names. Its **shipped-code** half (`unguardedSymlink`) carries neither dependency and is portable advice on its own, so this is the one exclusion that is half arbitrary; it stays out because the two halves cannot be enabled separately. This repo turns it on explicitly, scoped to its own test-file convention.
|
|
312
|
+
|
|
313
|
+
Enable any of them by naming it:
|
|
151
314
|
|
|
152
315
|
```js
|
|
153
316
|
import vat from '@vibe-agent-toolkit/utils/eslint';
|
|
@@ -160,6 +323,16 @@ export default [
|
|
|
160
323
|
'@vibe-agent-toolkit/require-justified-skip': 'error',
|
|
161
324
|
},
|
|
162
325
|
},
|
|
326
|
+
{
|
|
327
|
+
// Scope it to the code that reads files whose encoding you do not choose.
|
|
328
|
+
files: ['src/corpus/**/*.ts'],
|
|
329
|
+
rules: {
|
|
330
|
+
'@vibe-agent-toolkit/no-raw-text-decode': ['error', {
|
|
331
|
+
safeModule: '@my-org/resources',
|
|
332
|
+
exemptFiles: ['src/corpus/text-content.ts'],
|
|
333
|
+
}],
|
|
334
|
+
},
|
|
335
|
+
},
|
|
163
336
|
{
|
|
164
337
|
// Scope it to where an escape is a security boundary, not repo-wide.
|
|
165
338
|
files: ['src/staging/**/*.ts'],
|
|
@@ -170,17 +343,18 @@ export default [
|
|
|
170
343
|
|
|
171
344
|
### Severities
|
|
172
345
|
|
|
173
|
-
Within `recommended`, `error` is the default;
|
|
346
|
+
Within `recommended`, `error` is the default; two rules are `warn`:
|
|
174
347
|
|
|
175
|
-
- **`no-
|
|
348
|
+
- **`no-raw-node-path`** — by far the highest-churn rule; it fires on every raw `node:path` call in an existing codebase. Measured on a 4,670-file adopter tree: 3,963 + 372 + 1 findings across its three functions, **every one of them autofixable**. `warn` lets a project run `--fix` and burn the list down incrementally instead of blocking CI on day one.
|
|
349
|
+
- **`no-blind-catch`** — the same criterion without the autofix: each site is a decision about which failure its sentinel stands for, and an existing tree has many of them (202 in this repo's own ~200k lines when the rule landed).
|
|
176
350
|
|
|
177
|
-
Raise
|
|
351
|
+
Raise both to `error` once the backlog is clear. That is what this repo does.
|
|
178
352
|
|
|
179
353
|
The criterion for `warn` is **migration volume**, not how real the finding is — a rule whose findings we doubted would be out of `recommended` entirely, not demoted. Everything at `error` either prevents a bug or moves a static-analysis finding left of a merge.
|
|
180
354
|
|
|
181
355
|
### Running `--fix` over a large backlog
|
|
182
356
|
|
|
183
|
-
Every rule that rewrites a call *and* edits imports fixes **all** of a file's call sites in a single pass, and `packages/utils/test/eslint/
|
|
357
|
+
Every rule that rewrites a call *and* edits imports fixes **all** of a file's call sites in a single pass, and `packages/utils/test/eslint/autofix-fixpoint.test.ts` holds each of them to that: it runs `--fix` to its fixpoint and then asks `no-undef` whether the result still binds every identifier.
|
|
184
358
|
|
|
185
359
|
That test exists because the answer used to be no. ESLint merges the fixes one `fix()` yields into a **single range spanning `min..max`**, and applies only non-overlapping ranges per pass — so a fix touching both the import and its own call site spanned everything in between, N call sites produced N nested ranges, and ESLint kept one. The rule then went quiet, because the import specifier its detection keyed on was what had just been removed. `--fix` reached a stable fixpoint over source that no longer compiles and exited clean; you found out at `tsc`. An adopter measured **146 files left with a dangling reference** across one ~4,900-site sweep, worst single file 75 unrewritten calls.
|
|
186
360
|
|
|
@@ -210,7 +384,7 @@ What these rules buy is *when* you find out. Without them, a raw `os.tmpdir()` i
|
|
|
210
384
|
|
|
211
385
|
The second reason is agentic. When most code in a repo is written by an LLM, a convention that lives only in a style guide or a CLAUDE.md gets re-violated constantly — the model is confident, `path.join` is what it saw a million times in training, and the guidance was three thousand tokens back. A lint rule is feedback the model receives at the moment it is wrong, which is the only moment it can act on. Treat rules as the durable form of any convention you'd otherwise repeat in a prompt.
|
|
212
386
|
|
|
213
|
-
Most projects will want rules the ones here don't cover, for invariants only that project has: a deprecated internal API, a logging call that must carry a request ID, a module boundary nothing may import across. These rules are readable, small, and built on
|
|
387
|
+
Most projects will want rules the ones here don't cover, for invariants only that project has: a deprecated internal API, a logging call that must carry a request ID, a module boundary nothing may import across. These rules are readable, small, and built on one shared factory plus a segment-anchored path matcher — copy the shape. `eslint-rule-factory.cjs` handles "ban function X from module Y, suggest Z, fix the import"; `no-raw-node-path.cjs` is the worked example of a rule driven by an option table; `exempt-path-matcher.cjs` handles the exemption question every such rule eventually asks, and is worth reading before you write `filename.includes(...)`.
|
|
214
388
|
|
|
215
389
|
## License
|
|
216
390
|
|