@vibe-agent-toolkit/utils 0.1.41 → 0.1.42

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 (116) 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 +217 -0
  87. package/eslint/index.cjs +161 -0
  88. package/eslint/index.d.cts +60 -0
  89. package/eslint/rules/dead-import.cjs +201 -0
  90. package/eslint/rules/eslint-rule-factory.cjs +410 -0
  91. package/eslint/rules/exempt-path-matcher.cjs +265 -0
  92. package/eslint/rules/no-bare-dynamic-import-path.cjs +142 -0
  93. package/eslint/rules/no-child-process-execSync.cjs +23 -0
  94. package/eslint/rules/no-command-direct-factory.cjs +246 -0
  95. package/eslint/rules/no-file-url-string-concat.cjs +77 -0
  96. package/eslint/rules/no-fs-mkdirSync.cjs +23 -0
  97. package/eslint/rules/no-fs-promises-cp.cjs +36 -0
  98. package/eslint/rules/no-fs-realpathSync.cjs +23 -0
  99. package/eslint/rules/no-hardcoded-path-split.cjs +141 -0
  100. package/eslint/rules/no-manual-path-normalize.cjs +161 -0
  101. package/eslint/rules/no-os-tmpdir.cjs +24 -0
  102. package/eslint/rules/no-path-join.cjs +14 -0
  103. package/eslint/rules/no-path-operations-in-comparisons.cjs +148 -0
  104. package/eslint/rules/no-path-relative.cjs +14 -0
  105. package/eslint/rules/no-path-resolve.cjs +14 -0
  106. package/eslint/rules/no-path-sep-in-strings.cjs +130 -0
  107. package/eslint/rules/no-path-startswith.cjs +139 -0
  108. package/eslint/rules/no-test-scoped-functions.cjs +134 -0
  109. package/eslint/rules/no-unix-shell-commands.cjs +152 -0
  110. package/eslint/rules/no-unsafe-root-join.cjs +85 -0
  111. package/eslint/rules/no-url-pathname-for-fs.cjs +107 -0
  112. package/eslint/rules/path-function-rule-factory.cjs +417 -0
  113. package/eslint/rules/prefer-startswith-over-regex.cjs +257 -0
  114. package/eslint/rules/require-justified-skip.cjs +348 -0
  115. package/eslint/rules/safe-import.cjs +164 -0
  116. package/package.json +65 -7
@@ -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,161 @@
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
+ DEAD_UNSAFE_IMPORT,
18
+ DEAD_UNSAFE_IMPORT_MESSAGE,
19
+ reportDeadUnsafeImports,
20
+ } = require('./dead-import.cjs');
21
+ const {
22
+ SAFE_MODULE_ONLY_SCHEMA,
23
+ SAFE_PATH_MODULE,
24
+ insertAboveWithComments,
25
+ isNameAlreadyBound,
26
+ resolveSafeModule,
27
+ } = require('./safe-import.cjs');
28
+
29
+ const SAFE_FN = 'toForwardSlash';
30
+ const PATH_MODULES = new Set(['node:path', 'path']);
31
+
32
+ module.exports = {
33
+ meta: {
34
+ type: 'problem',
35
+ docs: {
36
+ description: 'Disallow manual path normalization patterns',
37
+ category: 'Cross-platform compatibility',
38
+ recommended: true,
39
+ },
40
+ fixable: 'code',
41
+ messages: {
42
+ useToForwardSlash:
43
+ 'Use toForwardSlash() from {{safeModule}} instead of manual path normalization. ' +
44
+ 'Manual normalization is error-prone and less maintainable.',
45
+ [DEAD_UNSAFE_IMPORT]: DEAD_UNSAFE_IMPORT_MESSAGE,
46
+ },
47
+ schema: [SAFE_MODULE_ONLY_SCHEMA],
48
+ },
49
+
50
+ create(context) {
51
+ const sourceCode = context.getSourceCode();
52
+ const targetModule = resolveSafeModule(context, SAFE_PATH_MODULE);
53
+ // Seeded from SCOPE: a file that already imports `toForwardSlash` from the
54
+ // barrel must have the call rewritten WITHOUT gaining a second binding of
55
+ // the same name — that is a SyntaxError. See `safe-import.cjs`.
56
+ let hasToForwardSlashImport = isNameAlreadyBound(sourceCode, SAFE_FN);
57
+ // Never mutated — the dead-import leg must not be armed by a flag that a
58
+ // suppressed report's `fix()` can spend. See `dead-import.cjs`.
59
+ const safeBoundInSource = hasToForwardSlashImport;
60
+ let utilsImportNode = null;
61
+ // `path.sep` is the last `path.*` reference in plenty of files, and
62
+ // `toForwardSlash(raw)` consumes it — leaving the same dead `node:path`
63
+ // binding the `safePath` rules used to leave.
64
+ const pathImportNodes = [];
65
+
66
+ return {
67
+ 'Program:exit'() {
68
+ reportDeadUnsafeImports(context, sourceCode, pathImportNodes, safeBoundInSource);
69
+ },
70
+
71
+ ImportDeclaration(node) {
72
+ if (PATH_MODULES.has(node.source.value)) {
73
+ pathImportNodes.push(node);
74
+ }
75
+ if (node.source.value === targetModule) {
76
+ utilsImportNode = node;
77
+ for (const spec of node.specifiers) {
78
+ if (spec.type === 'ImportSpecifier' && spec.imported.name === SAFE_FN) {
79
+ hasToForwardSlashImport = true;
80
+ }
81
+ }
82
+ }
83
+ },
84
+
85
+ CallExpression(node) {
86
+ // Check for .split(...).join('/') pattern
87
+ if (
88
+ node.callee.type === 'MemberExpression' &&
89
+ node.callee.property.name === 'join' &&
90
+ node.arguments.length === 1 &&
91
+ node.arguments[0].type === 'Literal' &&
92
+ node.arguments[0].value === '/'
93
+ ) {
94
+ // Check if the object is a .split() call
95
+ const splitCall = node.callee.object;
96
+ if (
97
+ splitCall.type === 'CallExpression' &&
98
+ splitCall.callee.type === 'MemberExpression' &&
99
+ splitCall.callee.property.name === 'split' &&
100
+ splitCall.arguments.length === 1
101
+ ) {
102
+ const splitArg = splitCall.arguments[0];
103
+
104
+ // Check if splitting by path.sep, '\\', or '\\\\'
105
+ const isSplittingByPathSep =
106
+ (splitArg.type === 'MemberExpression' &&
107
+ splitArg.object.name === 'path' &&
108
+ splitArg.property.name === 'sep') ||
109
+ (splitArg.type === 'Literal' && (splitArg.value === '\\' || splitArg.value === '\\\\'));
110
+
111
+ if (isSplittingByPathSep) {
112
+ const variableBeingSplit = splitCall.callee.object;
113
+
114
+ context.report({
115
+ node,
116
+ messageId: 'useToForwardSlash',
117
+ data: { safeModule: targetModule },
118
+ fix(fixer) {
119
+ const fixes = [];
120
+
121
+ // Replace the entire .split(...).join('/') with toForwardSlash(...)
122
+ const originalVar = sourceCode.getText(variableBeingSplit);
123
+ fixes.push(fixer.replaceText(node, `${SAFE_FN}(${originalVar})`));
124
+
125
+ // Add import if needed
126
+ if (!hasToForwardSlashImport) {
127
+ if (utilsImportNode) {
128
+ // Add to existing utils import
129
+ const lastSpecifier = utilsImportNode.specifiers.at(-1);
130
+ fixes.push(fixer.insertTextAfter(lastSpecifier, `, ${SAFE_FN}`));
131
+ } else {
132
+ // Create new import at the top
133
+ const firstNode = sourceCode.ast.body[0];
134
+ const newImport = `import { ${SAFE_FN} } from '${targetModule}';\n`;
135
+ fixes.push(insertAboveWithComments(fixer, sourceCode, firstNode, newImport));
136
+ }
137
+ // NOT latched. The comment here used to claim that without a
138
+ // `hasToForwardSlashImport = true` a second occurrence would
139
+ // insert the import twice; an adversarial run could not
140
+ // reproduce that at any occurrence count. It cannot happen:
141
+ // both reports insert identical text at the identical anchor,
142
+ // so the ranges coincide and ESLint applies one and drops the
143
+ // other as overlapping.
144
+ //
145
+ // Latching it is not free, either. ESLint runs `fix()` for a
146
+ // SUPPRESSED problem before the `eslint-disable` filter
147
+ // discards it, so the first report could spend the flag and
148
+ // then be thrown away — leaving later occurrences rewritten
149
+ // to a `toForwardSlash` nothing imports.
150
+ }
151
+
152
+ return fixes;
153
+ },
154
+ });
155
+ }
156
+ }
157
+ }
158
+ },
159
+ };
160
+ },
161
+ };
@@ -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
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * ESLint rule to disallow using path.sep in string operations
3
+ *
4
+ * path.sep is '\\' on Windows and '/' on Unix, which causes cross-platform issues
5
+ * when used in string operations. Use toForwardSlash() to normalize paths instead.
6
+ *
7
+ * @example
8
+ * // ❌ BAD - path.sep varies by platform
9
+ * const parts = filePath.split(path.sep);
10
+ * if (filePath.includes(path.sep)) { ... }
11
+ *
12
+ * // ✅ GOOD - normalize then split
13
+ * import { toForwardSlash } from '@vibe-agent-toolkit/utils/path';
14
+ * const parts = toForwardSlash(filePath).split('/');
15
+ * if (toForwardSlash(filePath).includes('/')) { ... }
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.sep in string operations',
29
+ category: 'Cross-platform compatibility',
30
+ recommended: true,
31
+ },
32
+ messages: {
33
+ noPathSep:
34
+ 'Avoid using path.sep in string operations (split, includes, indexOf, etc.). ' +
35
+ 'Use toForwardSlash() from {{safeModule}} to normalize paths to forward slashes first.',
36
+ },
37
+ schema: [SAFE_MODULE_ONLY_SCHEMA],
38
+ },
39
+
40
+ create(context) {
41
+ const reportData = { safeModule: resolveSafeModule(context, SAFE_PATH_MODULE) };
42
+ // String methods that take a separator/search argument
43
+ const stringMethodsWithSeparator = new Set([
44
+ 'split',
45
+ 'includes',
46
+ 'indexOf',
47
+ 'lastIndexOf',
48
+ 'startsWith',
49
+ 'endsWith',
50
+ 'replace',
51
+ 'replaceAll',
52
+ ]);
53
+
54
+ return {
55
+ CallExpression(node) {
56
+ // Check if this is a string method call
57
+ if (
58
+ node.callee.type !== 'MemberExpression' ||
59
+ node.callee.property.type !== 'Identifier' ||
60
+ !stringMethodsWithSeparator.has(node.callee.property.name)
61
+ ) {
62
+ return;
63
+ }
64
+
65
+ // Check if any argument is path.sep
66
+ const hasPathSepArg = node.arguments.some(
67
+ (arg) =>
68
+ arg.type === 'MemberExpression' &&
69
+ arg.object.type === 'Identifier' &&
70
+ arg.object.name === 'path' &&
71
+ arg.property.type === 'Identifier' &&
72
+ arg.property.name === 'sep',
73
+ );
74
+
75
+ if (hasPathSepArg) {
76
+ context.report({
77
+ node,
78
+ messageId: 'noPathSep',
79
+ data: reportData,
80
+ });
81
+ }
82
+ },
83
+
84
+ // Also catch template literals and binary expressions using path.sep
85
+ TemplateLiteral(node) {
86
+ const hasPathSep = node.expressions.some(
87
+ (expr) =>
88
+ expr.type === 'MemberExpression' &&
89
+ expr.object.type === 'Identifier' &&
90
+ expr.object.name === 'path' &&
91
+ expr.property.type === 'Identifier' &&
92
+ expr.property.name === 'sep',
93
+ );
94
+
95
+ if (hasPathSep) {
96
+ context.report({
97
+ node,
98
+ messageId: 'noPathSep',
99
+ data: reportData,
100
+ });
101
+ }
102
+ },
103
+
104
+ BinaryExpression(node) {
105
+ // Check for string concatenation with path.sep
106
+ if (node.operator === '+') {
107
+ const hasPathSep =
108
+ (node.left.type === 'MemberExpression' &&
109
+ node.left.object.type === 'Identifier' &&
110
+ node.left.object.name === 'path' &&
111
+ node.left.property.type === 'Identifier' &&
112
+ node.left.property.name === 'sep') ||
113
+ (node.right.type === 'MemberExpression' &&
114
+ node.right.object.type === 'Identifier' &&
115
+ node.right.object.name === 'path' &&
116
+ node.right.property.type === 'Identifier' &&
117
+ node.right.property.name === 'sep');
118
+
119
+ if (hasPathSep) {
120
+ context.report({
121
+ node,
122
+ messageId: 'noPathSep',
123
+ data: reportData,
124
+ });
125
+ }
126
+ }
127
+ },
128
+ };
129
+ },
130
+ };