@vibe-agent-toolkit/utils 0.2.0-rc.7 → 0.2.0-rc.8
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 +22 -3
- package/dist/asset-reference.js +3 -11
- package/dist/asset-reference.js.map +1 -1
- package/dist/crawl-timing.d.ts +3 -3
- package/dist/crawl-timing.js +3 -3
- 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/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.js +9 -9
- 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 +16 -18
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +56 -80
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +2 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +2 -1
- package/dist/fs.js.map +1 -1
- package/dist/git-run.d.ts +1 -1
- package/dist/git-run.js +1 -1
- package/dist/git-tracker.d.ts +1 -1
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +1 -2
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +0 -1
- package/dist/git-utils.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +9 -3
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -4
- package/dist/index.js.map +1 -1
- package/dist/listing-refusal.d.ts +2 -1
- package/dist/listing-refusal.d.ts.map +1 -1
- package/dist/listing-refusal.js +3 -3
- package/dist/listing-refusal.js.map +1 -1
- 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 +77 -3
- package/dist/path-core.d.ts.map +1 -1
- package/dist/path-core.js +96 -6
- 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/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +9 -6
- package/dist/project-utils.js.map +1 -1
- package/dist/safe-exec.d.ts +2 -1
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +25 -7
- 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/spawn-claude.d.ts +2 -1
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +20 -3
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/skill-test/transcript.d.ts.map +1 -1
- package/dist/skill-test/transcript.js +18 -17
- package/dist/skill-test/transcript.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 +47 -204
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +105 -310
- 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 +5 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +10 -1
- package/dist/testing.js.map +1 -1
- package/dist/text-content.d.ts.map +1 -1
- package/dist/text-content.js +7 -1
- package/dist/text-content.js.map +1 -1
- package/dist/text-file.d.ts.map +1 -1
- package/dist/text-file.js +0 -2
- package/dist/text-file.js.map +1 -1
- package/dist/timing-dump.d.ts +1 -1
- package/dist/timing-dump.d.ts.map +1 -1
- package/dist/timing-dump.js +1 -2
- package/dist/timing-dump.js.map +1 -1
- 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 +125 -67
- package/eslint/index.cjs +111 -162
- package/eslint/index.d.cts +14 -17
- 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 +1 -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 +16 -3
- 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 +216 -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 +14 -1
- 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 +7 -2
- package/eslint/rules/no-io-in-unit-tier.cjs +128 -0
- package/eslint/rules/no-literal-process-exit.cjs +129 -0
- package/eslint/rules/no-manual-path-normalize.cjs +7 -2
- 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 +12 -2
- package/eslint/rules/no-raw-node-path.cjs +563 -0
- package/eslint/rules/no-raw-text-decode.cjs +10 -1
- package/eslint/rules/no-registry-count-pin.cjs +255 -0
- package/eslint/rules/no-self-package-import.cjs +13 -2
- 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 +4 -9
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zod-introspection.js","sourceRoot":"","sources":["../src/zod-introspection.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,WAAW;IACnB,OAAO,EAAE,YAAY;IACrB,KAAK,EAAE,UAAU;IACjB,MAAM,EAAE,WAAW;IACnB,IAAI,EAAE,SAAS;IACf,QAAQ,EAAE,aAAa;IACvB,QAAQ,EAAE,aAAa;IACvB,IAAI,EAAE,SAAS;IACf,OAAO,EAAE,YAAY;IACrB,KAAK,EAAE,UAAU;IACjB,YAAY,EAAE,iBAAiB;IAC/B,KAAK,EAAE,UAAU;IACjB,MAAM,EAAE,WAAW;IACnB,GAAG,EAAE,QAAQ;IACb,GAAG,EAAE,QAAQ;IACb,QAAQ,EAAE,aAAa;IACvB,IAAI,EAAE,SAAS;IACf,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,YAAY;IACrB,QAAQ,EAAE,aAAa;IACvB,QAAQ,EAAE,aAAa;IACvB,MAAM,EAAE,WAAW;IACnB,SAAS,EAAE,cAAc;IACzB,IAAI,EAAE,SAAS;IACf,GAAG,EAAE,QAAQ;IACb,OAAO,EAAE,YAAY;IACrB,KAAK,EAAE,UAAU;IACjB,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,WAAW;IACnB,OAAO,EAAE,YAAY;IACrB,SAAS,EAAE,eAAe;IAC1B,mBAAmB,EAAE,uBAAuB;IAC5C,OAAO,EAAE,YAAY;IACrB,KAAK,EAAE,UAAU;IACjB,GAAG,EAAE,QAAQ;CACL,CAAC;AAOX;;;;;;;;;;;;;;;;GAgBG;AACH,SAAS,iBAAiB,CAAC,MAAc;IACvC,6CAA6C;IAC7C,MAAM,YAAY,GAA2B;QAC3C,UAAU,EAAE,eAAe;QAC3B,MAAM,EAAE,WAAW;KACpB,CAAC;IAEF,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;QACzB,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;IAED,wDAAwD;IACxD,OAAO,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,cAAc,CAAC,OAAgB;IAC7C,
|
|
1
|
+
{"version":3,"file":"zod-introspection.js","sourceRoot":"","sources":["../src/zod-introspection.ts"],"names":[],"mappings":"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"}
|
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,54 +64,125 @@ 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
|
+
<!-- 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.
|
|
77
|
+
|
|
78
|
+
#### Path handling
|
|
79
|
+
|
|
80
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
81
|
+
|---|---|---|---|---|---|
|
|
82
|
+
| `no-hardcoded-path-split` | `split('/')` / `split('\\')` on a path | `path.basename()`, or `toForwardSlash()` first | `/path` | | `error` |
|
|
83
|
+
| `no-manual-path-normalize` | hand-rolled `.replace(/\\/g, '/')` | `toForwardSlash()` | `/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` | | — |
|
|
89
|
+
|
|
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` |
|
|
102
|
+
|
|
103
|
+
#### URLs and dynamic imports
|
|
104
|
+
|
|
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` |
|
|
110
|
+
|
|
111
|
+
#### Entrypoint guards
|
|
112
|
+
|
|
113
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
114
|
+
|---|---|---|---|---|---|
|
|
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
|
|
118
|
+
|
|
119
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
120
|
+
|---|---|---|---|---|---|
|
|
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 | — | | — |
|
|
122
|
+
|
|
123
|
+
#### Error handling
|
|
124
|
+
|
|
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
|
|
136
|
+
|
|
137
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
138
|
+
|---|---|---|---|---|---|
|
|
139
|
+
| `no-self-package-import` | importing the enclosing package by its own name | a relative path to the defining module | — | | — |
|
|
140
|
+
|
|
141
|
+
#### Code and test hygiene
|
|
142
|
+
|
|
143
|
+
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
144
|
+
|---|---|---|---|---|---|
|
|
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
|
+
|
|
75
164
|
### Pointing the fix at your own re-export seam — `safeModule`
|
|
76
165
|
|
|
77
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:
|
|
78
167
|
|
|
79
168
|
```js
|
|
80
|
-
'@vibe-agent-toolkit/no-path
|
|
169
|
+
'@vibe-agent-toolkit/no-raw-node-path': ['warn', { safeModule: '@acme/dev-tools/paths' }],
|
|
81
170
|
'@vibe-agent-toolkit/no-os-tmpdir': ['error', { safeModule: '@acme/dev-tools/fs' }],
|
|
82
171
|
```
|
|
83
172
|
|
|
84
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.
|
|
85
174
|
|
|
86
|
-
> **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-path
|
|
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.
|
|
87
176
|
|
|
88
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."
|
|
89
178
|
|
|
90
|
-
###
|
|
179
|
+
### Notes on individual rules
|
|
91
180
|
|
|
92
|
-
|
|
93
|
-
|---|---|---|---|---|---|
|
|
94
|
-
| `no-path-join` | `path.join()` | `safePath.join()` | `/path` | ✓ | `warn` |
|
|
95
|
-
| `no-path-resolve` | `path.resolve()` | `safePath.resolve()` | `/path` | ✓ | `warn` |
|
|
96
|
-
| `no-path-relative` | `path.relative()` | `safePath.relative()` | `/path` | ✓ | `warn` |
|
|
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` | | — |
|
|
181
|
+
#### `no-raw-node-path`
|
|
103
182
|
|
|
104
|
-
|
|
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'] }]`.
|
|
105
184
|
|
|
106
|
-
|
|
107
|
-
|---|---|---|---|---|---|
|
|
108
|
-
| `no-os-tmpdir` | `os.tmpdir()` (8.3 short names on Windows) | `normalizedTmpdir()` | `/fs` | ✓ | `error` |
|
|
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` |
|
|
114
|
-
| `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` | | — |
|
|
185
|
+
#### `no-bare-symlink-in-tests`
|
|
115
186
|
|
|
116
187
|
**`no-bare-symlink-in-tests` reports two different remedies, and the name is narrower than the rule.**
|
|
117
188
|
Creating a symlink on Windows requires `SeCreateSymbolicLinkPrivilege` — Developer Mode or an
|
|
@@ -127,21 +198,11 @@ say so in an `eslint-disable` justification, which is what `vat agent install --
|
|
|
127
198
|
`exemptFiles` matters here: the implementation file holding the sanctioned `symlinkSync` is not a
|
|
128
199
|
test file, so it needs an explicit exemption once the rule covers shipped code.
|
|
129
200
|
|
|
130
|
-
|
|
201
|
+
#### Member-call rules and the receiver
|
|
131
202
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
135
|
-
|---|---|---|---|---|---|
|
|
136
|
-
| `no-url-pathname-for-fs` | `new URL(x, import.meta.url).pathname` as a filesystem path | `resolveFromImportMeta()` / `fileURLToPath()` | `/fs` | | `error` |
|
|
137
|
-
| `no-bare-dynamic-import-path` | `await import(absolutePath)` | `dynamicImportPath()` / `pathToFileURL(p).href` | `/fs` | | `error` |
|
|
138
|
-
| `no-file-url-string-concat` | `` `file://${p}` `` | `pathToFileURL(p).href` | — | | `error` |
|
|
139
|
-
|
|
140
|
-
### Entrypoint guards
|
|
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.
|
|
141
204
|
|
|
142
|
-
|
|
143
|
-
|---|---|---|---|---|---|
|
|
144
|
-
| `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` | | — |
|
|
205
|
+
#### `no-fragile-entrypoint-guard`
|
|
145
206
|
|
|
146
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.
|
|
147
208
|
|
|
@@ -151,11 +212,17 @@ The other two are one defect wearing two spellings: a raw string comparison of w
|
|
|
151
212
|
|
|
152
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`.
|
|
153
214
|
|
|
154
|
-
|
|
215
|
+
#### `no-blind-catch`
|
|
155
216
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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`
|
|
159
226
|
|
|
160
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.
|
|
161
228
|
|
|
@@ -190,11 +257,7 @@ That gives a reviewer a falsifiable test: a justification that cannot name who w
|
|
|
190
257
|
|
|
191
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)`.
|
|
192
259
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
196
|
-
|---|---|---|---|---|---|
|
|
197
|
-
| `no-self-package-import` | importing the enclosing package by its own name | a relative path to the defining module | — | | — (needs `packageName`) |
|
|
260
|
+
#### `no-self-package-import`
|
|
198
261
|
|
|
199
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.
|
|
200
263
|
|
|
@@ -208,17 +271,9 @@ The knock-on `TS2339`s land wherever a local type extended one of the now-unreso
|
|
|
208
271
|
|
|
209
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.
|
|
210
273
|
|
|
211
|
-
### Code and test hygiene
|
|
212
|
-
|
|
213
|
-
| Rule | Bans | Use instead | Subpath | Fix | `recommended` |
|
|
214
|
-
|---|---|---|---|---|---|
|
|
215
|
-
| `prefer-startswith-over-regex` | `/^foo/.test(s)`, `` /^\*glob/.test(s) ``, `const RE = /^foo/; RE.test(s)` | `s.startsWith('foo')` | — | | `error` |
|
|
216
|
-
| `no-test-scoped-functions` | helper functions declared inside `describe`/`it` | module scope | — | | — |
|
|
217
|
-
| `require-justified-skip` | unannotated `it.skip`/`it.todo`, tautological assertions, empty test bodies | a `SKIP(#123): reason` annotation, or a real assertion | — | | — |
|
|
218
|
-
|
|
219
274
|
### What `recommended` deliberately leaves out
|
|
220
275
|
|
|
221
|
-
|
|
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:
|
|
222
277
|
|
|
223
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.
|
|
224
279
|
|
|
@@ -251,9 +306,11 @@ export default readdirSync('packages').flatMap((dir) => {
|
|
|
251
306
|
|
|
252
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.
|
|
253
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
|
+
|
|
254
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.
|
|
255
312
|
|
|
256
|
-
Enable any of
|
|
313
|
+
Enable any of them by naming it:
|
|
257
314
|
|
|
258
315
|
```js
|
|
259
316
|
import vat from '@vibe-agent-toolkit/utils/eslint';
|
|
@@ -286,17 +343,18 @@ export default [
|
|
|
286
343
|
|
|
287
344
|
### Severities
|
|
288
345
|
|
|
289
|
-
Within `recommended`, `error` is the default;
|
|
346
|
+
Within `recommended`, `error` is the default; two rules are `warn`:
|
|
290
347
|
|
|
291
|
-
- **`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).
|
|
292
350
|
|
|
293
|
-
Raise
|
|
351
|
+
Raise both to `error` once the backlog is clear. That is what this repo does.
|
|
294
352
|
|
|
295
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.
|
|
296
354
|
|
|
297
355
|
### Running `--fix` over a large backlog
|
|
298
356
|
|
|
299
|
-
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.
|
|
300
358
|
|
|
301
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.
|
|
302
360
|
|
|
@@ -326,7 +384,7 @@ What these rules buy is *when* you find out. Without them, a raw `os.tmpdir()` i
|
|
|
326
384
|
|
|
327
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.
|
|
328
386
|
|
|
329
|
-
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(...)`.
|
|
330
388
|
|
|
331
389
|
## License
|
|
332
390
|
|