@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.
- package/README.md +146 -31
- package/dist/asset.d.ts +9 -0
- package/dist/asset.d.ts.map +1 -0
- package/dist/asset.js +9 -0
- package/dist/asset.js.map +1 -0
- package/dist/crawl.d.ts +19 -0
- package/dist/crawl.d.ts.map +1 -0
- package/dist/crawl.js +19 -0
- package/dist/crawl.js.map +1 -0
- package/dist/file-crawler.d.ts +19 -2
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +19 -1
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +60 -3
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +88 -10
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +7 -6
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +7 -6
- package/dist/fs.js.map +1 -1
- package/dist/git-utils.d.ts +2 -0
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +16 -0
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +23 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +23 -0
- package/dist/git.js.map +1 -0
- package/dist/gitignore-checker.d.ts +0 -9
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +0 -12
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/glob/glob-pattern.js +1 -1
- package/dist/glob/glob-pattern.js.map +1 -1
- package/dist/glob.d.ts +8 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +8 -0
- package/dist/glob.js.map +1 -0
- package/dist/path-core.d.ts +196 -0
- package/dist/path-core.d.ts.map +1 -0
- package/dist/path-core.js +251 -0
- package/dist/path-core.js.map +1 -0
- package/dist/path-utils.d.ts +12 -183
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +13 -237
- package/dist/path-utils.js.map +1 -1
- package/dist/path.d.ts +12 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +12 -0
- package/dist/path.js.map +1 -0
- package/dist/process.d.ts +7 -4
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +7 -4
- package/dist/process.js.map +1 -1
- package/dist/project.d.ts +31 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +31 -0
- package/dist/project.js.map +1 -0
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +8 -4
- package/dist/safe-exec.js.map +1 -1
- package/dist/spawn-hardened.d.ts.map +1 -1
- package/dist/spawn-hardened.js +4 -17
- package/dist/spawn-hardened.js.map +1 -1
- package/dist/template-entry.d.ts +10 -0
- package/dist/template-entry.d.ts.map +1 -0
- package/dist/template-entry.js +10 -0
- package/dist/template-entry.js.map +1 -0
- package/dist/testing.d.ts +8 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +8 -0
- package/dist/testing.js.map +1 -0
- package/dist/windows-shell.d.ts +140 -8
- package/dist/windows-shell.d.ts.map +1 -1
- package/dist/windows-shell.js +218 -11
- package/dist/windows-shell.js.map +1 -1
- package/dist/yaml.d.ts +9 -0
- package/dist/yaml.d.ts.map +1 -0
- package/dist/yaml.js +9 -0
- package/dist/yaml.js.map +1 -0
- package/dist/zod.d.ts +10 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +10 -0
- package/dist/zod.js.map +1 -0
- package/eslint/README.md +217 -0
- package/eslint/index.cjs +161 -0
- package/eslint/index.d.cts +60 -0
- package/eslint/rules/dead-import.cjs +201 -0
- package/eslint/rules/eslint-rule-factory.cjs +410 -0
- package/eslint/rules/exempt-path-matcher.cjs +265 -0
- package/eslint/rules/no-bare-dynamic-import-path.cjs +142 -0
- package/eslint/rules/no-child-process-execSync.cjs +23 -0
- package/eslint/rules/no-command-direct-factory.cjs +246 -0
- package/eslint/rules/no-file-url-string-concat.cjs +77 -0
- package/eslint/rules/no-fs-mkdirSync.cjs +23 -0
- package/eslint/rules/no-fs-promises-cp.cjs +36 -0
- package/eslint/rules/no-fs-realpathSync.cjs +23 -0
- package/eslint/rules/no-hardcoded-path-split.cjs +141 -0
- package/eslint/rules/no-manual-path-normalize.cjs +161 -0
- package/eslint/rules/no-os-tmpdir.cjs +24 -0
- package/eslint/rules/no-path-join.cjs +14 -0
- package/eslint/rules/no-path-operations-in-comparisons.cjs +148 -0
- package/eslint/rules/no-path-relative.cjs +14 -0
- package/eslint/rules/no-path-resolve.cjs +14 -0
- package/eslint/rules/no-path-sep-in-strings.cjs +130 -0
- package/eslint/rules/no-path-startswith.cjs +139 -0
- package/eslint/rules/no-test-scoped-functions.cjs +134 -0
- package/eslint/rules/no-unix-shell-commands.cjs +152 -0
- package/eslint/rules/no-unsafe-root-join.cjs +85 -0
- package/eslint/rules/no-url-pathname-for-fs.cjs +107 -0
- package/eslint/rules/path-function-rule-factory.cjs +417 -0
- package/eslint/rules/prefer-startswith-over-regex.cjs +257 -0
- package/eslint/rules/require-justified-skip.cjs +348 -0
- package/eslint/rules/safe-import.cjs +164 -0
- package/package.json +65 -7
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-bare-dynamic-import-path
|
|
3
|
+
*
|
|
4
|
+
* Prevents `await import(p)` where `p` is a raw filesystem path.
|
|
5
|
+
* On Windows, ESM dynamic import of a bare path fails — it requires a `file://`
|
|
6
|
+
* URL. Observed bug: `await import('D:\\a\\repo\\config.js')` throws.
|
|
7
|
+
*
|
|
8
|
+
* Fix: `await import(pathToFileURL(p).href)` from `node:url`, or
|
|
9
|
+
* `await dynamicImportPath(p)` from `@vibe-agent-toolkit/utils/fs`.
|
|
10
|
+
*
|
|
11
|
+
* Heuristic (intentionally narrow — some false positives are preferable to
|
|
12
|
+
* false negatives for a shift-left lint rule, but the user can suppress per line):
|
|
13
|
+
* - String literal starting with `/` or `C:\` style absolute path → flag
|
|
14
|
+
* - Call to `path.join`, `path.resolve`, `join`, `resolve`, `safePath.*` → flag
|
|
15
|
+
* - Identifier whose name matches /path|file|config|module|dir/i → flag
|
|
16
|
+
* (will have false positives on well-named variables that already hold
|
|
17
|
+
* file:// URLs; suppress those call-sites with eslint-disable-next-line.)
|
|
18
|
+
* - Template literal that embeds a path-shaped call → flag
|
|
19
|
+
*
|
|
20
|
+
* Does NOT flag:
|
|
21
|
+
* - Relative module specifiers (`./foo.js`, `../bar.js`)
|
|
22
|
+
* - Bare package names (`some-pkg`)
|
|
23
|
+
* - Expressions ending in `.href` (assume correct `pathToFileURL(x).href`)
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
SAFE_FS_MODULE,
|
|
30
|
+
SAFE_MODULE_ONLY_SCHEMA,
|
|
31
|
+
resolveSafeModule,
|
|
32
|
+
} = require('./safe-import.cjs');
|
|
33
|
+
|
|
34
|
+
const PATH_CALL_NAMES = new Set(['join', 'resolve']);
|
|
35
|
+
const PATH_OBJECT_NAMES = new Set(['path', 'safePath']);
|
|
36
|
+
const PATH_SHAPED_IDENTIFIER = /path|file|config|module|dir/i;
|
|
37
|
+
const ABSOLUTE_PATH_LITERAL = /^(\/|[A-Za-z]:[\\/])/;
|
|
38
|
+
|
|
39
|
+
function isPathCallExpression(node) {
|
|
40
|
+
if (node?.type !== 'CallExpression') return false;
|
|
41
|
+
const callee = node.callee;
|
|
42
|
+
// bare call: join(...), resolve(...)
|
|
43
|
+
if (callee.type === 'Identifier' && PATH_CALL_NAMES.has(callee.name)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
// member call: path.join, path.resolve, safePath.join, safePath.resolve
|
|
47
|
+
if (
|
|
48
|
+
callee.type === 'MemberExpression' &&
|
|
49
|
+
callee.object.type === 'Identifier' &&
|
|
50
|
+
PATH_OBJECT_NAMES.has(callee.object.name) &&
|
|
51
|
+
callee.property.type === 'Identifier'
|
|
52
|
+
) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isHrefAccess(node) {
|
|
59
|
+
return (
|
|
60
|
+
node?.type === 'MemberExpression' &&
|
|
61
|
+
node.property.type === 'Identifier' &&
|
|
62
|
+
node.property.name === 'href'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isRelativeOrBareSpecifier(value) {
|
|
67
|
+
if (typeof value !== 'string' || value.length === 0) return false;
|
|
68
|
+
if (value.startsWith('./') || value.startsWith('../') || value === '.' || value === '..') {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
// Bare package name — no leading slash, no drive letter.
|
|
72
|
+
if (ABSOLUTE_PATH_LITERAL.test(value)) return false;
|
|
73
|
+
return !value.includes('\\');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function templateHasPathCall(node) {
|
|
77
|
+
if (node?.type !== 'TemplateLiteral') return false;
|
|
78
|
+
return node.expressions.some((expr) => isPathCallExpression(expr));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function classifyImportArgument(arg) {
|
|
82
|
+
if (!arg) return null;
|
|
83
|
+
|
|
84
|
+
// Literal string specifier — flag only if absolute path-shaped.
|
|
85
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
86
|
+
if (isRelativeOrBareSpecifier(arg.value)) return null;
|
|
87
|
+
if (ABSOLUTE_PATH_LITERAL.test(arg.value)) return 'absolute-literal';
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Template literal with a path call embedded.
|
|
92
|
+
if (arg.type === 'TemplateLiteral') {
|
|
93
|
+
if (templateHasPathCall(arg)) return 'template-with-path-call';
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// `pathToFileURL(x).href` or similar — correct form, do not flag.
|
|
98
|
+
if (isHrefAccess(arg)) return null;
|
|
99
|
+
|
|
100
|
+
// path.join(...) / path.resolve(...) / join(...) / resolve(...)
|
|
101
|
+
if (isPathCallExpression(arg)) return 'path-call';
|
|
102
|
+
|
|
103
|
+
// Bare identifier whose name hints at a filesystem path.
|
|
104
|
+
if (arg.type === 'Identifier' && PATH_SHAPED_IDENTIFIER.test(arg.name)) {
|
|
105
|
+
return 'path-shaped-identifier';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
meta: {
|
|
113
|
+
type: 'problem',
|
|
114
|
+
docs: {
|
|
115
|
+
description:
|
|
116
|
+
'Disallow dynamic `import()` of a filesystem path; wrap with `pathToFileURL(p).href`.',
|
|
117
|
+
category: 'Cross-platform compatibility',
|
|
118
|
+
recommended: true,
|
|
119
|
+
},
|
|
120
|
+
messages: {
|
|
121
|
+
useFileUrl:
|
|
122
|
+
'Dynamic `import()` of a filesystem path fails on Windows. ' +
|
|
123
|
+
'Wrap with `pathToFileURL(p).href` from `node:url`, or use `dynamicImportPath(p)` from `{{safeModule}}`.',
|
|
124
|
+
},
|
|
125
|
+
schema: [SAFE_MODULE_ONLY_SCHEMA],
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
create(context) {
|
|
129
|
+
return {
|
|
130
|
+
ImportExpression(node) {
|
|
131
|
+
const kind = classifyImportArgument(node.source);
|
|
132
|
+
if (kind) {
|
|
133
|
+
context.report({
|
|
134
|
+
node,
|
|
135
|
+
messageId: 'useFileUrl',
|
|
136
|
+
data: { safeModule: resolveSafeModule(context, SAFE_FS_MODULE) },
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: no-child-process-execSync
|
|
3
|
+
*
|
|
4
|
+
* Prevents usage of child_process.execSync() in favor of safeExecSync() from `@vibe-agent-toolkit/utils/process`
|
|
5
|
+
*
|
|
6
|
+
* Why: execSync() uses shell interpreter which enables command injection attacks.
|
|
7
|
+
* safeExecSync() uses direct spawn (no shell) with 'which' pattern for security.
|
|
8
|
+
*
|
|
9
|
+
* Auto-fix: Replaces execSync() with safeExecSync() and adds required import.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const factory = require('./eslint-rule-factory.cjs');
|
|
13
|
+
const { SAFE_PROCESS_MODULE } = require('./safe-import.cjs');
|
|
14
|
+
|
|
15
|
+
module.exports = factory({
|
|
16
|
+
unsafeFn: 'execSync',
|
|
17
|
+
unsafeModule: 'node:child_process',
|
|
18
|
+
safeFn: 'safeExecSync',
|
|
19
|
+
safeModule: SAFE_PROCESS_MODULE,
|
|
20
|
+
message: 'Use safeExecSync() from {{safeModule}} instead of child_process.execSync() to prevent command injection (security + cross-platform)',
|
|
21
|
+
// No baked-in exemption: the file that implements safeExecSync() is
|
|
22
|
+
// repo-specific. Consumers declare it as { exemptFiles: [...] }.
|
|
23
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule Factory - Command Execution Checking
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for:
|
|
5
|
+
* 1. How commands can be executed (safeExecSync, spawn, execSync, etc.)
|
|
6
|
+
* 2. How to check those execution patterns
|
|
7
|
+
* 3. Creating rules that enforce command restrictions
|
|
8
|
+
*
|
|
9
|
+
* This module exports both the factory function and helper utilities
|
|
10
|
+
* so all command-checking rules reference the same execution patterns.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { createExemptDirectoryMatcher } = require('./exempt-path-matcher.cjs');
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// COMMAND EXECUTION PATTERNS - Single Source of Truth
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// If we add new execution methods (e.g., safeExecAsync), update this list
|
|
19
|
+
// and all dependent rules automatically benefit.
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if execSync command string starts with the target command
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} firstArg - AST node for first argument
|
|
25
|
+
* @param {string} command - Command to check for (e.g., 'git')
|
|
26
|
+
* @returns {boolean} True if command matches
|
|
27
|
+
*/
|
|
28
|
+
function commandMatchesExecSync(firstArg, command) {
|
|
29
|
+
// Check string literals: execSync('git status')
|
|
30
|
+
if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
|
|
31
|
+
return firstArg.value.startsWith(`${command} `) || firstArg.value === command;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check template literals: execSync(`git ${args}`)
|
|
35
|
+
if (firstArg.type === 'TemplateLiteral' && firstArg.quasis.length > 0) {
|
|
36
|
+
const firstQuasi = firstArg.quasis[0].value.cooked || firstArg.quasis[0].value.raw;
|
|
37
|
+
return firstQuasi.startsWith(`${command} `) || firstQuasi === command;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check shell-free command execution (safeExecSync, spawn, etc.)
|
|
45
|
+
* @private
|
|
46
|
+
*/
|
|
47
|
+
function checkShellFreeExecution(node, context, shouldReport, messageId) {
|
|
48
|
+
const firstArg = node.arguments[0];
|
|
49
|
+
|
|
50
|
+
if (firstArg.type !== 'Literal') {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const result = shouldReport(node, firstArg);
|
|
55
|
+
if (result) {
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
messageId,
|
|
59
|
+
data: typeof result === 'object' ? result : undefined,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check execSync command execution (shell-based)
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
function checkExecSyncExecution(node, context, shouldReport, messageId) {
|
|
69
|
+
const firstArg = node.arguments[0];
|
|
70
|
+
const result = shouldReport(node, firstArg, commandMatchesExecSync);
|
|
71
|
+
|
|
72
|
+
if (result) {
|
|
73
|
+
context.report({
|
|
74
|
+
node,
|
|
75
|
+
messageId,
|
|
76
|
+
data: typeof result === 'object' ? result : undefined,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Check if function is a shell-free execution method
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
function isShellFreeExecution(functionName) {
|
|
86
|
+
return (
|
|
87
|
+
functionName === 'safeExecSync' ||
|
|
88
|
+
functionName === 'safeExecResult' ||
|
|
89
|
+
functionName === 'spawn' ||
|
|
90
|
+
functionName === 'spawnSync'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a CallExpression checker that validates command execution calls
|
|
96
|
+
*
|
|
97
|
+
* This centralizes the logic for detecting command execution patterns across
|
|
98
|
+
* different ESLint rules, eliminating code duplication.
|
|
99
|
+
*
|
|
100
|
+
* @param {Object} options - Configuration options
|
|
101
|
+
* @param {Object} options.context - ESLint rule context
|
|
102
|
+
* @param {Function} options.shouldReport - Predicate that determines if violation should be reported
|
|
103
|
+
* Can return:
|
|
104
|
+
* - boolean: true to report with default messageData
|
|
105
|
+
* - object: messageData to use for error message
|
|
106
|
+
* - falsy: don't report
|
|
107
|
+
* Signature: (node, firstArg, matchesExecSync) => boolean | object | falsy
|
|
108
|
+
* @param {string} options.messageId - ESLint message ID to report
|
|
109
|
+
* @returns {Object} ESLint CallExpression visitor
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* // Simple boolean check:
|
|
113
|
+
* return {
|
|
114
|
+
* CallExpression: createCommandChecker({
|
|
115
|
+
* context,
|
|
116
|
+
* shouldReport: (node, firstArg) => firstArg.value === 'git',
|
|
117
|
+
* messageId: 'noGitDirect',
|
|
118
|
+
* }),
|
|
119
|
+
* };
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* // With dynamic messageData:
|
|
123
|
+
* return {
|
|
124
|
+
* CallExpression: createCommandChecker({
|
|
125
|
+
* context,
|
|
126
|
+
* shouldReport: (node, firstArg) => {
|
|
127
|
+
* if (UNIX_COMMANDS.includes(firstArg.value)) {
|
|
128
|
+
* return { command: firstArg.value, alternative: getAlt(firstArg.value) };
|
|
129
|
+
* }
|
|
130
|
+
* return false;
|
|
131
|
+
* },
|
|
132
|
+
* messageId: 'unixCommand',
|
|
133
|
+
* }),
|
|
134
|
+
* };
|
|
135
|
+
*/
|
|
136
|
+
function createCommandChecker(options) {
|
|
137
|
+
const { context, shouldReport, messageId } = options;
|
|
138
|
+
|
|
139
|
+
return function CallExpression(node) {
|
|
140
|
+
const functionName = node.callee.name;
|
|
141
|
+
|
|
142
|
+
if (node.arguments.length === 0) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check for shell-free command execution patterns:
|
|
147
|
+
// - safeExecSync('cmd', ...)
|
|
148
|
+
// - safeExecResult('cmd', ...)
|
|
149
|
+
// - spawn('cmd', ...)
|
|
150
|
+
// - spawnSync('cmd', ...)
|
|
151
|
+
if (isShellFreeExecution(functionName)) {
|
|
152
|
+
checkShellFreeExecution(node, context, shouldReport, messageId);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check for execSync('cmd ...') or execSync(`cmd ...`)
|
|
157
|
+
// Note: execSync is discouraged for security reasons (uses shell)
|
|
158
|
+
if (functionName === 'execSync') {
|
|
159
|
+
checkExecSyncExecution(node, context, shouldReport, messageId);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// FACTORY FUNCTION - Create Rules for Specific Commands
|
|
166
|
+
// ============================================================================
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Factory: Create rule that prevents direct command execution
|
|
170
|
+
*
|
|
171
|
+
* @param {Object} config - Rule configuration
|
|
172
|
+
* @param {string} config.command - Command name to detect (e.g., 'git', 'gh')
|
|
173
|
+
* @param {string} config.packageName - Package containing wrappers (e.g., '@my-org/git')
|
|
174
|
+
* @param {string[]} config.availableFunctions - List of available wrapper functions
|
|
175
|
+
* @param {string} [config.exemptPackage] - Repo-relative directory of the package that
|
|
176
|
+
* OWNS the wrappers, e.g. `'packages/git/'`. Matched at a path-segment boundary by
|
|
177
|
+
* `exempt-path-matcher.cjs` — NOT as a substring, so `vendor/copy-packages/git/` is
|
|
178
|
+
* still linted.
|
|
179
|
+
* @returns {Object} ESLint rule definition
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* // no-git-commands-direct.cjs
|
|
183
|
+
* const { createNoCommandDirectRule } = require('./no-command-direct-factory.cjs');
|
|
184
|
+
* module.exports = createNoCommandDirectRule({
|
|
185
|
+
* command: 'git',
|
|
186
|
+
* packageName: '@my-org/git',
|
|
187
|
+
* availableFunctions: ['executeGitCommand()', 'getTreeHash()', 'addNote()'],
|
|
188
|
+
* exemptPackage: 'packages/git/',
|
|
189
|
+
* });
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
function createNoCommandDirectRule(config) {
|
|
193
|
+
const { command, packageName, availableFunctions, exemptPackage } = config;
|
|
194
|
+
|
|
195
|
+
const isUnderExemptPackage = createExemptDirectoryMatcher(exemptPackage ? [exemptPackage] : []);
|
|
196
|
+
const functionList = availableFunctions.join(', ');
|
|
197
|
+
const messageId = `no${command.charAt(0).toUpperCase()}${command.slice(1)}Direct`;
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
meta: {
|
|
201
|
+
type: 'problem',
|
|
202
|
+
docs: {
|
|
203
|
+
description: `Enforce use of ${packageName} functions instead of direct ${command} command execution`,
|
|
204
|
+
category: 'Architecture',
|
|
205
|
+
recommended: true,
|
|
206
|
+
},
|
|
207
|
+
fixable: null, // No auto-fix - requires manual refactoring
|
|
208
|
+
schema: [],
|
|
209
|
+
messages: {
|
|
210
|
+
[messageId]: `Use functions from ${packageName} instead of calling ${command} commands directly. Available functions: ${functionList}.`,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
create(context) {
|
|
215
|
+
// Exempt the package itself (where centralization happens)
|
|
216
|
+
if (isUnderExemptPackage(context.getFilename())) {
|
|
217
|
+
return {};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
CallExpression: createCommandChecker({
|
|
222
|
+
context,
|
|
223
|
+
shouldReport: (node, firstArg, matchesExecSync) => {
|
|
224
|
+
// For shell-free execution (safeExecSync, spawn, etc.)
|
|
225
|
+
if (!matchesExecSync) {
|
|
226
|
+
return firstArg.value === command;
|
|
227
|
+
}
|
|
228
|
+
// For execSync (shell-based)
|
|
229
|
+
return matchesExecSync(firstArg, command);
|
|
230
|
+
},
|
|
231
|
+
messageId,
|
|
232
|
+
}),
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// EXPORTS - Factory + Helpers
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Export factory as default (for backward compatibility with existing rules)
|
|
242
|
+
// and attach helpers as properties (for new rules like no-unix-shell-commands)
|
|
243
|
+
|
|
244
|
+
module.exports = createNoCommandDirectRule;
|
|
245
|
+
module.exports.createCommandChecker = createCommandChecker;
|
|
246
|
+
module.exports.commandMatchesExecSync = commandMatchesExecSync;
|
|
@@ -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
|
+
});
|