@vibe-agent-toolkit/utils 0.1.41 → 0.1.42-rc.1

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 (115) hide show
  1. package/README.md +146 -31
  2. package/dist/asset.d.ts +9 -0
  3. package/dist/asset.d.ts.map +1 -0
  4. package/dist/asset.js +9 -0
  5. package/dist/asset.js.map +1 -0
  6. package/dist/crawl.d.ts +19 -0
  7. package/dist/crawl.d.ts.map +1 -0
  8. package/dist/crawl.js +19 -0
  9. package/dist/crawl.js.map +1 -0
  10. package/dist/file-crawler.d.ts +19 -2
  11. package/dist/file-crawler.d.ts.map +1 -1
  12. package/dist/file-crawler.js +19 -1
  13. package/dist/file-crawler.js.map +1 -1
  14. package/dist/fs-utils.d.ts +60 -3
  15. package/dist/fs-utils.d.ts.map +1 -1
  16. package/dist/fs-utils.js +88 -10
  17. package/dist/fs-utils.js.map +1 -1
  18. package/dist/fs.d.ts +7 -6
  19. package/dist/fs.d.ts.map +1 -1
  20. package/dist/fs.js +7 -6
  21. package/dist/fs.js.map +1 -1
  22. package/dist/git-utils.d.ts +2 -0
  23. package/dist/git-utils.d.ts.map +1 -1
  24. package/dist/git-utils.js +16 -0
  25. package/dist/git-utils.js.map +1 -1
  26. package/dist/git.d.ts +23 -0
  27. package/dist/git.d.ts.map +1 -0
  28. package/dist/git.js +23 -0
  29. package/dist/git.js.map +1 -0
  30. package/dist/gitignore-checker.d.ts +0 -9
  31. package/dist/gitignore-checker.d.ts.map +1 -1
  32. package/dist/gitignore-checker.js +0 -12
  33. package/dist/gitignore-checker.js.map +1 -1
  34. package/dist/glob/glob-pattern.js +1 -1
  35. package/dist/glob/glob-pattern.js.map +1 -1
  36. package/dist/glob.d.ts +8 -0
  37. package/dist/glob.d.ts.map +1 -0
  38. package/dist/glob.js +8 -0
  39. package/dist/glob.js.map +1 -0
  40. package/dist/path-core.d.ts +196 -0
  41. package/dist/path-core.d.ts.map +1 -0
  42. package/dist/path-core.js +251 -0
  43. package/dist/path-core.js.map +1 -0
  44. package/dist/path-utils.d.ts +12 -183
  45. package/dist/path-utils.d.ts.map +1 -1
  46. package/dist/path-utils.js +13 -237
  47. package/dist/path-utils.js.map +1 -1
  48. package/dist/path.d.ts +12 -0
  49. package/dist/path.d.ts.map +1 -0
  50. package/dist/path.js +12 -0
  51. package/dist/path.js.map +1 -0
  52. package/dist/process.d.ts +7 -4
  53. package/dist/process.d.ts.map +1 -1
  54. package/dist/process.js +7 -4
  55. package/dist/process.js.map +1 -1
  56. package/dist/project.d.ts +31 -0
  57. package/dist/project.d.ts.map +1 -0
  58. package/dist/project.js +31 -0
  59. package/dist/project.js.map +1 -0
  60. package/dist/safe-exec.d.ts.map +1 -1
  61. package/dist/safe-exec.js +8 -4
  62. package/dist/safe-exec.js.map +1 -1
  63. package/dist/spawn-hardened.d.ts.map +1 -1
  64. package/dist/spawn-hardened.js +4 -17
  65. package/dist/spawn-hardened.js.map +1 -1
  66. package/dist/template-entry.d.ts +10 -0
  67. package/dist/template-entry.d.ts.map +1 -0
  68. package/dist/template-entry.js +10 -0
  69. package/dist/template-entry.js.map +1 -0
  70. package/dist/testing.d.ts +8 -0
  71. package/dist/testing.d.ts.map +1 -0
  72. package/dist/testing.js +8 -0
  73. package/dist/testing.js.map +1 -0
  74. package/dist/windows-shell.d.ts +140 -8
  75. package/dist/windows-shell.d.ts.map +1 -1
  76. package/dist/windows-shell.js +218 -11
  77. package/dist/windows-shell.js.map +1 -1
  78. package/dist/yaml.d.ts +9 -0
  79. package/dist/yaml.d.ts.map +1 -0
  80. package/dist/yaml.js +9 -0
  81. package/dist/yaml.js.map +1 -0
  82. package/dist/zod.d.ts +10 -0
  83. package/dist/zod.d.ts.map +1 -0
  84. package/dist/zod.js +10 -0
  85. package/dist/zod.js.map +1 -0
  86. package/eslint/README.md +191 -0
  87. package/eslint/index.cjs +161 -0
  88. package/eslint/index.d.cts +60 -0
  89. package/eslint/rules/eslint-rule-factory.cjs +241 -0
  90. package/eslint/rules/exempt-path-matcher.cjs +265 -0
  91. package/eslint/rules/no-bare-dynamic-import-path.cjs +142 -0
  92. package/eslint/rules/no-child-process-execSync.cjs +23 -0
  93. package/eslint/rules/no-command-direct-factory.cjs +246 -0
  94. package/eslint/rules/no-file-url-string-concat.cjs +77 -0
  95. package/eslint/rules/no-fs-mkdirSync.cjs +23 -0
  96. package/eslint/rules/no-fs-promises-cp.cjs +36 -0
  97. package/eslint/rules/no-fs-realpathSync.cjs +23 -0
  98. package/eslint/rules/no-hardcoded-path-split.cjs +141 -0
  99. package/eslint/rules/no-manual-path-normalize.cjs +130 -0
  100. package/eslint/rules/no-os-tmpdir.cjs +24 -0
  101. package/eslint/rules/no-path-join.cjs +14 -0
  102. package/eslint/rules/no-path-operations-in-comparisons.cjs +148 -0
  103. package/eslint/rules/no-path-relative.cjs +14 -0
  104. package/eslint/rules/no-path-resolve.cjs +14 -0
  105. package/eslint/rules/no-path-sep-in-strings.cjs +130 -0
  106. package/eslint/rules/no-path-startswith.cjs +139 -0
  107. package/eslint/rules/no-test-scoped-functions.cjs +134 -0
  108. package/eslint/rules/no-unix-shell-commands.cjs +152 -0
  109. package/eslint/rules/no-unsafe-root-join.cjs +85 -0
  110. package/eslint/rules/no-url-pathname-for-fs.cjs +107 -0
  111. package/eslint/rules/path-function-rule-factory.cjs +216 -0
  112. package/eslint/rules/prefer-startswith-over-regex.cjs +111 -0
  113. package/eslint/rules/require-justified-skip.cjs +348 -0
  114. package/eslint/rules/safe-import.cjs +141 -0
  115. package/package.json +65 -7
@@ -0,0 +1,77 @@
1
+ /**
2
+ * ESLint rule: no-file-url-string-concat
3
+ *
4
+ * Disallow constructing `file://` URLs by string concatenation or template
5
+ * literal interpolation. Filesystem paths on Windows contain `\` and `C:`,
6
+ * and naively prefixing them with `file://` produces non-canonical URLs that
7
+ * never compare equal to `import.meta.url`, never round-trip through `new URL`,
8
+ * and silently break entry-point checks like:
9
+ *
10
+ * if (import.meta.url === `file://${process.argv[1]}`) { ... } // ❌ Windows
11
+ *
12
+ * `pathToFileURL()` is the only correct constructor: it percent-encodes
13
+ * separators, prepends the right number of slashes for drive letters, and
14
+ * matches what Node produces for `import.meta.url`.
15
+ *
16
+ * @example
17
+ * // ❌ BAD — Windows-broken
18
+ * const u = `file://${process.argv[1]}`;
19
+ * const u2 = 'file://' + somePath;
20
+ *
21
+ * // ✅ GOOD
22
+ * import { pathToFileURL } from 'node:url';
23
+ * const u = pathToFileURL(process.argv[1]).href;
24
+ */
25
+
26
+ 'use strict';
27
+
28
+ const FILE_URL_PREFIX = 'file://';
29
+
30
+ function literalStartsWithFileUrl(node) {
31
+ if (!node) return false;
32
+ if (node.type === 'Literal') {
33
+ return typeof node.value === 'string' && node.value.startsWith(FILE_URL_PREFIX);
34
+ }
35
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0 && node.quasis.length > 0) {
36
+ const cooked = node.quasis[0].value.cooked;
37
+ return typeof cooked === 'string' && cooked.startsWith(FILE_URL_PREFIX);
38
+ }
39
+ return false;
40
+ }
41
+
42
+ module.exports = {
43
+ meta: {
44
+ type: 'problem',
45
+ docs: {
46
+ description:
47
+ 'Disallow constructing `file://` URLs by string concatenation; use `pathToFileURL()` from `node:url`.',
48
+ category: 'Cross-platform compatibility',
49
+ recommended: true,
50
+ },
51
+ messages: {
52
+ useFileUrlBuilder:
53
+ 'Do not build `file://` URLs by string concatenation — on Windows, paths contain `\\` and drive letters and produce non-canonical URLs that never compare equal to `import.meta.url`. Use `pathToFileURL(path).href` from `node:url`.',
54
+ },
55
+ schema: [],
56
+ },
57
+
58
+ create(context) {
59
+ return {
60
+ TemplateLiteral(node) {
61
+ if (node.expressions.length === 0) return;
62
+ if (node.quasis.length === 0) return;
63
+ const cooked = node.quasis[0].value.cooked;
64
+ if (typeof cooked === 'string' && cooked.startsWith(FILE_URL_PREFIX)) {
65
+ context.report({ node, messageId: 'useFileUrlBuilder' });
66
+ }
67
+ },
68
+
69
+ BinaryExpression(node) {
70
+ if (node.operator !== '+') return;
71
+ if (literalStartsWithFileUrl(node.left) || literalStartsWithFileUrl(node.right)) {
72
+ context.report({ node, messageId: 'useFileUrlBuilder' });
73
+ }
74
+ },
75
+ };
76
+ },
77
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ESLint rule: no-fs-mkdirSync
3
+ *
4
+ * Prevents usage of fs.mkdirSync() in favor of mkdirSyncReal() from `@vibe-agent-toolkit/utils/fs`
5
+ *
6
+ * Why: After mkdirSync(), the path might not match what the filesystem uses on Windows.
7
+ * mkdirSyncReal() returns the real (normalized) path to handle 8.3 short name issues.
8
+ *
9
+ * Auto-fix: Replaces fs.mkdirSync() with mkdirSyncReal() and adds required import.
10
+ */
11
+
12
+ const factory = require('./eslint-rule-factory.cjs');
13
+ const { SAFE_FS_MODULE } = require('./safe-import.cjs');
14
+
15
+ module.exports = factory({
16
+ unsafeFn: 'mkdirSync',
17
+ unsafeModule: 'node:fs',
18
+ safeFn: 'mkdirSyncReal',
19
+ safeModule: SAFE_FS_MODULE,
20
+ message: 'Use mkdirSyncReal() from {{safeModule}} instead of fs.mkdirSync() for Windows path normalization',
21
+ // No baked-in exemption: the file that implements mkdirSyncReal() is
22
+ // repo-specific. Consumers declare it as { exemptFiles: [...] }.
23
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ESLint rule: no-fs-promises-cp
3
+ *
4
+ * Prevents usage of cp() from node:fs/promises in favor of cpSync() from node:fs.
5
+ *
6
+ * Why: Node 22's async cp() with { recursive: true } silently drops files in
7
+ * nested directories (observed with .mjs files in deeply nested paths).
8
+ * The files are simply missing from the destination with no error thrown.
9
+ * cpSync() from node:fs does not have this bug and works correctly across
10
+ * all Node versions (22, 24+).
11
+ *
12
+ * This is a known Node.js issue, not a VAT bug. Until the Node.js team fixes
13
+ * the async cp() implementation, cpSync() is the safe default.
14
+ *
15
+ * If you need async cp() for a specific use case and have verified it works
16
+ * correctly with your file structure, disable this rule with an eslint-disable
17
+ * comment explaining why async is required.
18
+ *
19
+ * Auto-fix: Replaces cp() with cpSync() and updates import from node:fs/promises to node:fs.
20
+ */
21
+
22
+ const factory = require('./eslint-rule-factory.cjs');
23
+
24
+ module.exports = factory({
25
+ unsafeFn: 'cp',
26
+ unsafeModule: 'node:fs/promises',
27
+ safeFn: 'cpSync',
28
+ safeModule: 'node:fs',
29
+ // `{{safeModule}}` resolves to `node:fs` unless overridden. A builtin always
30
+ // resolves, so this rule has no seam problem — it takes the option only
31
+ // because it shares the factory.
32
+ message:
33
+ 'Use cpSync() from {{safeModule}} instead of cp() from node:fs/promises. ' +
34
+ 'Node 22 async cp({ recursive: true }) silently drops files in nested directories. ' +
35
+ 'cpSync() works correctly across all Node versions.',
36
+ });
@@ -0,0 +1,23 @@
1
+ /**
2
+ * ESLint rule: no-fs-realpathSync
3
+ *
4
+ * Prevents usage of fs.realpathSync() in favor of normalizePath() from `@vibe-agent-toolkit/utils/fs`
5
+ *
6
+ * Why: realpathSync() doesn't consistently resolve Windows 8.3 short paths across Node versions.
7
+ * normalizePath() uses realpathSync.native() with fallbacks for better cross-platform compatibility.
8
+ *
9
+ * Auto-fix: Replaces fs.realpathSync() with normalizePath() and adds required import.
10
+ */
11
+
12
+ const factory = require('./eslint-rule-factory.cjs');
13
+ const { SAFE_FS_MODULE } = require('./safe-import.cjs');
14
+
15
+ module.exports = factory({
16
+ unsafeFn: 'realpathSync',
17
+ unsafeModule: 'node:fs',
18
+ safeFn: 'normalizePath',
19
+ safeModule: SAFE_FS_MODULE,
20
+ message: 'Use normalizePath() from {{safeModule}} instead of fs.realpathSync() for consistent Windows 8.3 path resolution',
21
+ // No baked-in exemption: the file that implements normalizePath() is
22
+ // repo-specific. Consumers declare it as { exemptFiles: [...] }.
23
+ });
@@ -0,0 +1,141 @@
1
+ /**
2
+ * ESLint rule to disallow splitting strings by hardcoded path separators
3
+ *
4
+ * Using .split('/') or .split('\\') on file paths breaks on Windows/Unix.
5
+ * Use path.basename(), path.dirname(), or normalize with toForwardSlash() first.
6
+ *
7
+ * This rule is smart enough to detect when paths are already normalized:
8
+ * - Inline: toForwardSlash(path).split('/') ✅
9
+ * - Variable: const normalized = toForwardSlash(path); normalized.split('/') ✅
10
+ *
11
+ * @example
12
+ * // ❌ BAD - breaks on Windows (paths use backslashes)
13
+ * const filename = filePath.split('/').pop();
14
+ * const parts = filePath.split('\\');
15
+ *
16
+ * // ✅ GOOD - use path.basename() for filename
17
+ * import { basename } from 'node:path';
18
+ * const filename = basename(filePath);
19
+ *
20
+ * // ✅ GOOD - normalize then split (inline)
21
+ * import { toForwardSlash } from '@vibe-agent-toolkit/utils/path';
22
+ * const parts = toForwardSlash(filePath).split('/');
23
+ *
24
+ * // ✅ GOOD - normalize then split (variable)
25
+ * const normalizedPath = toForwardSlash(filePath);
26
+ * const parts = normalizedPath.split('/');
27
+ */
28
+
29
+ const {
30
+ SAFE_MODULE_ONLY_SCHEMA,
31
+ SAFE_PATH_MODULE,
32
+ resolveSafeModule,
33
+ } = require('./safe-import.cjs');
34
+
35
+ module.exports = {
36
+ meta: {
37
+ type: 'problem',
38
+ docs: {
39
+ description: 'Disallow splitting strings by hardcoded path separators',
40
+ category: 'Cross-platform compatibility',
41
+ recommended: true,
42
+ },
43
+ messages: {
44
+ noHardcodedSplit:
45
+ String.raw`Avoid .split('/') or .split('\') on file paths (breaks on Windows/Unix). ` +
46
+ 'Use path.basename() to extract filename, or toForwardSlash() from {{safeModule}} to normalize paths first.',
47
+ },
48
+ schema: [SAFE_MODULE_ONLY_SCHEMA],
49
+ },
50
+
51
+ create(context) {
52
+ const reportData = { safeModule: resolveSafeModule(context, SAFE_PATH_MODULE) };
53
+ // Track variables that were assigned from toForwardSlash()
54
+ const normalizedVariables = new Set();
55
+
56
+ return {
57
+ // Track variable declarations
58
+ VariableDeclarator(node) {
59
+ // Check if this variable is assigned from toForwardSlash()
60
+ if (
61
+ node.init?.type === 'CallExpression' &&
62
+ node.init.callee.type === 'Identifier' &&
63
+ node.init.callee.name === 'toForwardSlash' &&
64
+ node.id.type === 'Identifier'
65
+ ) {
66
+ normalizedVariables.add(node.id.name);
67
+ }
68
+ },
69
+
70
+ CallExpression(node) {
71
+ // Check if this is a .split() call
72
+ if (
73
+ node.callee.type !== 'MemberExpression' ||
74
+ node.callee.property.type !== 'Identifier' ||
75
+ node.callee.property.name !== 'split'
76
+ ) {
77
+ return;
78
+ }
79
+
80
+ // Check if the argument is a hardcoded path separator
81
+ const firstArg = node.arguments[0];
82
+ if (!firstArg) {
83
+ return;
84
+ }
85
+
86
+ // Check for literal '/' or '\\'
87
+ const isPathSeparator =
88
+ (firstArg.type === 'Literal' &&
89
+ typeof firstArg.value === 'string' &&
90
+ (firstArg.value === '/' || firstArg.value === '\\')) ||
91
+ // Check for regex like /[/\\]/ or /\//
92
+ (firstArg.type === 'Literal' &&
93
+ firstArg.value instanceof RegExp &&
94
+ (firstArg.value.source.includes('/') ||
95
+ firstArg.value.source.includes('\\\\')));
96
+
97
+ if (!isPathSeparator) {
98
+ return;
99
+ }
100
+
101
+ // Check if this is a safe usage (normalized path)
102
+ const object = node.callee.object;
103
+
104
+ // Case 1: Inline normalization - toForwardSlash(...).split('/')
105
+ if (
106
+ object.type === 'CallExpression' &&
107
+ object.callee.type === 'Identifier' &&
108
+ object.callee.name === 'toForwardSlash'
109
+ ) {
110
+ return; // Safe - normalized inline
111
+ }
112
+
113
+ // Case 2: Variable that was normalized earlier
114
+ if (
115
+ object.type === 'Identifier' &&
116
+ normalizedVariables.has(object.name)
117
+ ) {
118
+ return; // Safe - variable was normalized
119
+ }
120
+
121
+ // Case 3: Check if variable name suggests normalization
122
+ // Common patterns: normalizedPath, unixPath, forwardSlashPath
123
+ if (
124
+ object.type === 'Identifier' &&
125
+ (object.name.toLowerCase().includes('normalized') ||
126
+ object.name.toLowerCase().includes('unix') ||
127
+ object.name.toLowerCase().includes('forward'))
128
+ ) {
129
+ return; // Safe - variable naming suggests normalization
130
+ }
131
+
132
+ // Not safe - report the issue
133
+ context.report({
134
+ node,
135
+ messageId: 'noHardcodedSplit',
136
+ data: reportData,
137
+ });
138
+ },
139
+ };
140
+ },
141
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * ESLint rule to enforce using toForwardSlash() instead of manual normalization
3
+ *
4
+ * Detects manual path normalization patterns and suggests using the utility function.
5
+ *
6
+ * @example
7
+ * // ❌ BAD - manual normalization
8
+ * const normalized = relativePath.split(path.sep).join('/');
9
+ * const normalized = somePath.split('\\').join('/');
10
+ *
11
+ * // ✅ GOOD - use utility function
12
+ * import { toForwardSlash } from '@vibe-agent-toolkit/utils/path';
13
+ * const normalized = toForwardSlash(relativePath);
14
+ */
15
+
16
+ const {
17
+ SAFE_MODULE_ONLY_SCHEMA,
18
+ SAFE_PATH_MODULE,
19
+ isNameAlreadyBound,
20
+ resolveSafeModule,
21
+ } = require('./safe-import.cjs');
22
+
23
+ const SAFE_FN = 'toForwardSlash';
24
+
25
+ module.exports = {
26
+ meta: {
27
+ type: 'problem',
28
+ docs: {
29
+ description: 'Disallow manual path normalization patterns',
30
+ category: 'Cross-platform compatibility',
31
+ recommended: true,
32
+ },
33
+ fixable: 'code',
34
+ messages: {
35
+ useToForwardSlash:
36
+ 'Use toForwardSlash() from {{safeModule}} instead of manual path normalization. ' +
37
+ 'Manual normalization is error-prone and less maintainable.',
38
+ },
39
+ schema: [SAFE_MODULE_ONLY_SCHEMA],
40
+ },
41
+
42
+ create(context) {
43
+ const sourceCode = context.getSourceCode();
44
+ const targetModule = resolveSafeModule(context, SAFE_PATH_MODULE);
45
+ // Seeded from SCOPE: a file that already imports `toForwardSlash` from the
46
+ // barrel must have the call rewritten WITHOUT gaining a second binding of
47
+ // the same name — that is a SyntaxError. See `safe-import.cjs`.
48
+ let hasToForwardSlashImport = isNameAlreadyBound(sourceCode, SAFE_FN);
49
+ let utilsImportNode = null;
50
+
51
+ return {
52
+ ImportDeclaration(node) {
53
+ if (node.source.value === targetModule) {
54
+ utilsImportNode = node;
55
+ for (const spec of node.specifiers) {
56
+ if (spec.type === 'ImportSpecifier' && spec.imported.name === SAFE_FN) {
57
+ hasToForwardSlashImport = true;
58
+ }
59
+ }
60
+ }
61
+ },
62
+
63
+ CallExpression(node) {
64
+ // Check for .split(...).join('/') pattern
65
+ if (
66
+ node.callee.type === 'MemberExpression' &&
67
+ node.callee.property.name === 'join' &&
68
+ node.arguments.length === 1 &&
69
+ node.arguments[0].type === 'Literal' &&
70
+ node.arguments[0].value === '/'
71
+ ) {
72
+ // Check if the object is a .split() call
73
+ const splitCall = node.callee.object;
74
+ if (
75
+ splitCall.type === 'CallExpression' &&
76
+ splitCall.callee.type === 'MemberExpression' &&
77
+ splitCall.callee.property.name === 'split' &&
78
+ splitCall.arguments.length === 1
79
+ ) {
80
+ const splitArg = splitCall.arguments[0];
81
+
82
+ // Check if splitting by path.sep, '\\', or '\\\\'
83
+ const isSplittingByPathSep =
84
+ (splitArg.type === 'MemberExpression' &&
85
+ splitArg.object.name === 'path' &&
86
+ splitArg.property.name === 'sep') ||
87
+ (splitArg.type === 'Literal' && (splitArg.value === '\\' || splitArg.value === '\\\\'));
88
+
89
+ if (isSplittingByPathSep) {
90
+ const variableBeingSplit = splitCall.callee.object;
91
+
92
+ context.report({
93
+ node,
94
+ messageId: 'useToForwardSlash',
95
+ data: { safeModule: targetModule },
96
+ fix(fixer) {
97
+ const fixes = [];
98
+
99
+ // Replace the entire .split(...).join('/') with toForwardSlash(...)
100
+ const originalVar = sourceCode.getText(variableBeingSplit);
101
+ fixes.push(fixer.replaceText(node, `${SAFE_FN}(${originalVar})`));
102
+
103
+ // Add import if needed
104
+ if (!hasToForwardSlashImport) {
105
+ if (utilsImportNode) {
106
+ // Add to existing utils import
107
+ const lastSpecifier = utilsImportNode.specifiers.at(-1);
108
+ fixes.push(fixer.insertTextAfter(lastSpecifier, `, ${SAFE_FN}`));
109
+ } else {
110
+ // Create new import at the top
111
+ const firstNode = sourceCode.ast.body[0];
112
+ const newImport = `import { ${SAFE_FN} } from '${targetModule}';\n`;
113
+ fixes.push(fixer.insertTextBefore(firstNode, newImport));
114
+ }
115
+ // Multiple reports in one pass share this closure; without
116
+ // this, a second occurrence in the same file inserts the
117
+ // import a second time.
118
+ hasToForwardSlashImport = true;
119
+ }
120
+
121
+ return fixes;
122
+ },
123
+ });
124
+ }
125
+ }
126
+ }
127
+ },
128
+ };
129
+ },
130
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * ESLint rule: no-os-tmpdir
3
+ *
4
+ * Prevents usage of os.tmpdir() in favor of normalizedTmpdir() from `@vibe-agent-toolkit/utils/fs`
5
+ *
6
+ * Why: os.tmpdir() returns Windows 8.3 short paths (RUNNER~1) which cause module loading
7
+ * errors when paths are passed to child processes or used with import statements.
8
+ *
9
+ * Auto-fix: Replaces os.tmpdir() with normalizedTmpdir() and adds required import.
10
+ */
11
+
12
+ const factory = require('./eslint-rule-factory.cjs');
13
+ const { SAFE_FS_MODULE } = require('./safe-import.cjs');
14
+
15
+ module.exports = factory({
16
+ unsafeFn: 'tmpdir',
17
+ unsafeModule: 'node:os',
18
+ safeFn: 'normalizedTmpdir',
19
+ safeModule: SAFE_FS_MODULE,
20
+ message: 'Use normalizedTmpdir() from {{safeModule}} instead of os.tmpdir() for Windows compatibility (prevents 8.3 short name issues like RUNNER~1)',
21
+ // No baked-in exemption: the file that implements normalizedTmpdir() is
22
+ // repo-specific. Consumers declare it as { exemptFiles: [...] }.
23
+ checkMemberExpression: true, // Catch os.tmpdir() pattern
24
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ESLint rule: no-path-join
3
+ *
4
+ * Bans path.join() from node:path. Use safePath.join() from `@vibe-agent-toolkit/utils/path`.
5
+ * safePath.join() wraps path.join() + toForwardSlash() to prevent Windows backslash bugs.
6
+ */
7
+ const factory = require('./path-function-rule-factory.cjs');
8
+
9
+ module.exports = factory({
10
+ unsafeFn: 'join',
11
+ message:
12
+ 'Use safePath.join() from {{safeModule}} instead of path.join(). ' +
13
+ 'path.join() returns backslashes on Windows, causing Map key mismatches and path comparison bugs.',
14
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * ESLint rule to enforce normalizing path operations before string comparisons
3
+ *
4
+ * path.relative(), path.dirname(), path.basename(), path.join() return OS-specific separators.
5
+ * When comparing with literal strings (especially in markdown), normalize to forward slashes.
6
+ *
7
+ * @example
8
+ * // ❌ BAD - path.relative() returns backslashes on Windows
9
+ * const relativePath = path.relative(baseDir, filePath);
10
+ * if (content.includes(relativePath)) { ... } // FAILS on Windows!
11
+ *
12
+ * // ✅ GOOD - normalize before comparison
13
+ * import { toForwardSlash } from '@vibe-agent-toolkit/utils/path';
14
+ * const relativePath = toForwardSlash(path.relative(baseDir, filePath));
15
+ * if (content.includes(relativePath)) { ... }
16
+ */
17
+
18
+ const {
19
+ SAFE_MODULE_ONLY_SCHEMA,
20
+ SAFE_PATH_MODULE,
21
+ resolveSafeModule,
22
+ } = require('./safe-import.cjs');
23
+
24
+ module.exports = {
25
+ meta: {
26
+ type: 'problem',
27
+ docs: {
28
+ description: 'Disallow using path operations directly in string comparisons',
29
+ category: 'Cross-platform compatibility',
30
+ recommended: true,
31
+ },
32
+ messages: {
33
+ normalizePathOperation:
34
+ 'Wrap path.{{method}}() with toForwardSlash() from {{safeModule}} before using in string operations. ' +
35
+ 'Path operations return OS-specific separators that fail in cross-platform comparisons.',
36
+ },
37
+ schema: [SAFE_MODULE_ONLY_SCHEMA],
38
+ },
39
+
40
+ create(context) {
41
+ const safeModule = resolveSafeModule(context, SAFE_PATH_MODULE);
42
+ // Path methods that return paths with OS-specific separators
43
+ const pathMethodsReturningPaths = new Set([
44
+ 'relative',
45
+ 'dirname',
46
+ 'basename',
47
+ 'join',
48
+ 'resolve',
49
+ 'normalize',
50
+ ]);
51
+
52
+ // String methods that compare/search strings
53
+ const stringComparisonMethods = new Set([
54
+ 'includes',
55
+ 'indexOf',
56
+ 'lastIndexOf',
57
+ 'startsWith',
58
+ 'endsWith',
59
+ 'split',
60
+ 'replace',
61
+ 'replaceAll',
62
+ 'match',
63
+ 'search',
64
+ ]);
65
+
66
+ // Track variables that hold unwrapped path operation results
67
+ const unwrappedPathVariables = new Set();
68
+ const wrappedPathVariables = new Set();
69
+
70
+ // If `node` is a `path.<method>(...)` call where method returns
71
+ // OS-specific separators, return that method name. Otherwise undefined.
72
+ const unnormalizedPathCallMethod = (node) =>
73
+ node?.type === 'CallExpression' &&
74
+ node.callee.type === 'MemberExpression' &&
75
+ node.callee.object.type === 'Identifier' &&
76
+ node.callee.object.name === 'path' &&
77
+ pathMethodsReturningPaths.has(node.callee.property.name)
78
+ ? node.callee.property.name
79
+ : undefined;
80
+
81
+ const isStringComparisonCall = (node) =>
82
+ node.callee.type === 'MemberExpression' &&
83
+ stringComparisonMethods.has(node.callee.property.name);
84
+
85
+ const reportPathArgument = (arg) => {
86
+ const method = unnormalizedPathCallMethod(arg);
87
+ if (method !== undefined) {
88
+ context.report({ node: arg, messageId: 'normalizePathOperation', data: { method, safeModule } });
89
+ return;
90
+ }
91
+ if (
92
+ arg.type === 'Identifier' &&
93
+ unwrappedPathVariables.has(arg.name) &&
94
+ !wrappedPathVariables.has(arg.name)
95
+ ) {
96
+ context.report({
97
+ node: arg,
98
+ messageId: 'normalizePathOperation',
99
+ data: { method: 'operation', safeModule },
100
+ });
101
+ }
102
+ };
103
+
104
+ const reportTemplateLiteralExpressions = (templateLiteral) => {
105
+ for (const expr of templateLiteral.expressions) {
106
+ const method = unnormalizedPathCallMethod(expr);
107
+ if (method !== undefined) {
108
+ context.report({
109
+ node: expr,
110
+ messageId: 'normalizePathOperation',
111
+ data: { method, safeModule },
112
+ });
113
+ }
114
+ }
115
+ };
116
+
117
+ return {
118
+ VariableDeclarator(node) {
119
+ if (!node.init || node.id.type !== 'Identifier') return;
120
+
121
+ if (unnormalizedPathCallMethod(node.init) !== undefined) {
122
+ unwrappedPathVariables.add(node.id.name);
123
+ return;
124
+ }
125
+
126
+ // Initializer is `toForwardSlash(<expr>)` — treat the variable as wrapped.
127
+ if (
128
+ node.init.type === 'CallExpression' &&
129
+ node.init.callee.type === 'Identifier' &&
130
+ node.init.callee.name === 'toForwardSlash' &&
131
+ node.init.arguments.length === 1
132
+ ) {
133
+ wrappedPathVariables.add(node.id.name);
134
+ }
135
+ },
136
+
137
+ CallExpression(node) {
138
+ if (!isStringComparisonCall(node)) return;
139
+ for (const arg of node.arguments) {
140
+ reportPathArgument(arg);
141
+ if (arg.type === 'TemplateLiteral') {
142
+ reportTemplateLiteralExpressions(arg);
143
+ }
144
+ }
145
+ },
146
+ };
147
+ },
148
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ESLint rule: no-path-relative
3
+ *
4
+ * Bans path.relative() from node:path. Use safePath.relative() from `@vibe-agent-toolkit/utils/path`.
5
+ * safePath.relative() wraps path.relative() + toForwardSlash() to prevent Windows backslash bugs.
6
+ */
7
+ const factory = require('./path-function-rule-factory.cjs');
8
+
9
+ module.exports = factory({
10
+ unsafeFn: 'relative',
11
+ message:
12
+ 'Use safePath.relative() from {{safeModule}} instead of path.relative(). ' +
13
+ 'path.relative() returns backslashes on Windows, causing Map key mismatches and path comparison bugs.',
14
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ESLint rule: no-path-resolve
3
+ *
4
+ * Bans path.resolve() from node:path. Use safePath.resolve() from `@vibe-agent-toolkit/utils/path`.
5
+ * safePath.resolve() wraps path.resolve() + toForwardSlash() to prevent Windows backslash bugs.
6
+ */
7
+ const factory = require('./path-function-rule-factory.cjs');
8
+
9
+ module.exports = factory({
10
+ unsafeFn: 'resolve',
11
+ message:
12
+ 'Use safePath.resolve() from {{safeModule}} instead of path.resolve(). ' +
13
+ 'path.resolve() returns backslashes on Windows, causing Map key mismatches and path comparison bugs.',
14
+ });