@eldrforge/git-tools 0.1.1 → 0.1.4
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/package.json +4 -1
- package/.github/dependabot.yml +0 -12
- package/.github/workflows/npm-publish.yml +0 -48
- package/.github/workflows/test.yml +0 -29
- package/CODE-DIFF-SUMMARY.md +0 -93
- package/MIGRATION-VERIFICATION.md +0 -436
- package/eslint.config.mjs +0 -84
- package/src/child.ts +0 -249
- package/src/git.ts +0 -1120
- package/src/index.ts +0 -58
- package/src/logger.ts +0 -81
- package/src/validation.ts +0 -62
- package/tests/child.integration.test.ts +0 -170
- package/tests/child.test.ts +0 -1035
- package/tests/git.test.ts +0 -1931
- package/tests/logger.test.ts +0 -211
- package/tests/setup.ts +0 -17
- package/tests/validation.test.ts +0 -152
- package/tsconfig.json +0 -35
- package/vite.config.ts +0 -78
- package/vitest.config.ts +0 -37
package/eslint.config.mjs
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
-
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
|
3
|
-
import importPlugin from "eslint-plugin-import";
|
|
4
|
-
import globals from "globals";
|
|
5
|
-
import tsParser from "@typescript-eslint/parser";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import js from "@eslint/js";
|
|
9
|
-
import { FlatCompat } from "@eslint/eslintrc";
|
|
10
|
-
|
|
11
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
-
const __dirname = path.dirname(__filename);
|
|
13
|
-
const compat = new FlatCompat({
|
|
14
|
-
baseDirectory: __dirname,
|
|
15
|
-
recommendedConfig: js.configs.recommended,
|
|
16
|
-
allConfig: js.configs.all
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export default defineConfig([
|
|
20
|
-
globalIgnores([
|
|
21
|
-
"dist/**",
|
|
22
|
-
"node_modules/**",
|
|
23
|
-
"**/*.test.ts",
|
|
24
|
-
"temp-dist/**",
|
|
25
|
-
]),
|
|
26
|
-
{
|
|
27
|
-
extends: compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
|
|
28
|
-
|
|
29
|
-
plugins: {
|
|
30
|
-
"@typescript-eslint": typescriptEslint,
|
|
31
|
-
"import": importPlugin,
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
languageOptions: {
|
|
35
|
-
globals: {
|
|
36
|
-
...globals.node,
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
parser: tsParser,
|
|
40
|
-
ecmaVersion: "latest",
|
|
41
|
-
sourceType: "module",
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
rules: {
|
|
45
|
-
"@typescript-eslint/no-explicit-any": "off",
|
|
46
|
-
"@typescript-eslint/explicit-function-return-type": "off",
|
|
47
|
-
|
|
48
|
-
"@typescript-eslint/no-unused-vars": ["warn", {
|
|
49
|
-
argsIgnorePattern: "^_",
|
|
50
|
-
}],
|
|
51
|
-
|
|
52
|
-
indent: ["warn", 4, {
|
|
53
|
-
SwitchCase: 1,
|
|
54
|
-
}],
|
|
55
|
-
|
|
56
|
-
"import/extensions": ["error", "never", {
|
|
57
|
-
ignorePackages: true,
|
|
58
|
-
pattern: {
|
|
59
|
-
"js": "never",
|
|
60
|
-
"ts": "never",
|
|
61
|
-
"d": "always"
|
|
62
|
-
}
|
|
63
|
-
}],
|
|
64
|
-
|
|
65
|
-
"import/no-extraneous-dependencies": ["error", {
|
|
66
|
-
devDependencies: true,
|
|
67
|
-
optionalDependencies: false,
|
|
68
|
-
peerDependencies: false,
|
|
69
|
-
}],
|
|
70
|
-
|
|
71
|
-
"no-console": ["error"],
|
|
72
|
-
|
|
73
|
-
"no-restricted-imports": ["error", {
|
|
74
|
-
paths: [],
|
|
75
|
-
patterns: [
|
|
76
|
-
{
|
|
77
|
-
group: ["src/**"],
|
|
78
|
-
message: "Use absolute imports instead of relative imports"
|
|
79
|
-
}
|
|
80
|
-
]
|
|
81
|
-
}]
|
|
82
|
-
},
|
|
83
|
-
}]);
|
|
84
|
-
|
package/src/child.ts
DELETED
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import child_process, { exec, spawn } from 'child_process';
|
|
3
|
-
import util from 'util';
|
|
4
|
-
import { getLogger } from './logger';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Escapes shell arguments to prevent command injection
|
|
8
|
-
*/
|
|
9
|
-
function escapeShellArg(arg: string): string {
|
|
10
|
-
// For Windows, we need different escaping
|
|
11
|
-
if (process.platform === 'win32') {
|
|
12
|
-
// Escape double quotes and backslashes
|
|
13
|
-
return `"${arg.replace(/[\\"]/g, '\\$&')}"`;
|
|
14
|
-
} else {
|
|
15
|
-
// For Unix-like systems, escape single quotes
|
|
16
|
-
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Validates git references to prevent injection
|
|
22
|
-
*/
|
|
23
|
-
function validateGitRef(ref: string): boolean {
|
|
24
|
-
// Git refs can contain letters, numbers, hyphens, underscores, slashes, and dots
|
|
25
|
-
// But cannot contain certain dangerous characters
|
|
26
|
-
const validRefPattern = /^[a-zA-Z0-9._/-]+$/;
|
|
27
|
-
const invalidPatterns = [
|
|
28
|
-
/\.\./, // No double dots (directory traversal)
|
|
29
|
-
/^-/, // Cannot start with dash (flag injection)
|
|
30
|
-
/[\s;<>|&`$(){}[\]]/ // No shell metacharacters
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
if (!validRefPattern.test(ref)) {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return !invalidPatterns.some(pattern => pattern.test(ref));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Validates file paths to prevent injection
|
|
42
|
-
*/
|
|
43
|
-
function validateFilePath(filePath: string): boolean {
|
|
44
|
-
// Basic validation - no shell metacharacters
|
|
45
|
-
const invalidChars = /[;<>|&`$(){}[\]]/;
|
|
46
|
-
return !invalidChars.test(filePath);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Securely executes a command with arguments array (no shell injection risk)
|
|
51
|
-
*/
|
|
52
|
-
export async function runSecure(
|
|
53
|
-
command: string,
|
|
54
|
-
args: string[] = [],
|
|
55
|
-
options: child_process.SpawnOptions = {}
|
|
56
|
-
): Promise<{ stdout: string; stderr: string }> {
|
|
57
|
-
const logger = getLogger();
|
|
58
|
-
|
|
59
|
-
return new Promise((resolve, reject) => {
|
|
60
|
-
logger.verbose(`Executing command securely: ${command} ${args.join(' ')}`);
|
|
61
|
-
logger.verbose(`Working directory: ${options?.cwd || process.cwd()}`);
|
|
62
|
-
|
|
63
|
-
const child = spawn(command, args, {
|
|
64
|
-
...options,
|
|
65
|
-
shell: false, // CRITICAL: Never use shell for user input
|
|
66
|
-
stdio: 'pipe'
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
let stdout = '';
|
|
70
|
-
let stderr = '';
|
|
71
|
-
|
|
72
|
-
if (child.stdout) {
|
|
73
|
-
child.stdout.on('data', (data) => {
|
|
74
|
-
stdout += data.toString();
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (child.stderr) {
|
|
79
|
-
child.stderr.on('data', (data) => {
|
|
80
|
-
stderr += data.toString();
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
child.on('close', (code) => {
|
|
85
|
-
if (code === 0) {
|
|
86
|
-
logger.verbose(`Command completed successfully`);
|
|
87
|
-
logger.verbose(`stdout: ${stdout}`);
|
|
88
|
-
if (stderr) {
|
|
89
|
-
logger.verbose(`stderr: ${stderr}`);
|
|
90
|
-
}
|
|
91
|
-
resolve({ stdout, stderr });
|
|
92
|
-
} else {
|
|
93
|
-
logger.error(`Command failed with exit code ${code}`);
|
|
94
|
-
logger.error(`stdout: ${stdout}`);
|
|
95
|
-
logger.error(`stderr: ${stderr}`);
|
|
96
|
-
reject(new Error(`Command "${[command, ...args].join(' ')}" failed with exit code ${code}`));
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
child.on('error', (error) => {
|
|
101
|
-
logger.error(`Command failed to start: ${error.message}`);
|
|
102
|
-
reject(error);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Securely executes a command with inherited stdio (no shell injection risk)
|
|
109
|
-
*/
|
|
110
|
-
export async function runSecureWithInheritedStdio(
|
|
111
|
-
command: string,
|
|
112
|
-
args: string[] = [],
|
|
113
|
-
options: child_process.SpawnOptions = {}
|
|
114
|
-
): Promise<void> {
|
|
115
|
-
const logger = getLogger();
|
|
116
|
-
|
|
117
|
-
return new Promise((resolve, reject) => {
|
|
118
|
-
logger.verbose(`Executing command securely with inherited stdio: ${command} ${args.join(' ')}`);
|
|
119
|
-
logger.verbose(`Working directory: ${options?.cwd || process.cwd()}`);
|
|
120
|
-
|
|
121
|
-
const child = spawn(command, args, {
|
|
122
|
-
...options,
|
|
123
|
-
shell: false, // CRITICAL: Never use shell for user input
|
|
124
|
-
stdio: 'inherit'
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
child.on('close', (code) => {
|
|
128
|
-
if (code === 0) {
|
|
129
|
-
logger.verbose(`Command completed successfully with code ${code}`);
|
|
130
|
-
resolve();
|
|
131
|
-
} else {
|
|
132
|
-
logger.error(`Command failed with exit code ${code}`);
|
|
133
|
-
reject(new Error(`Command "${[command, ...args].join(' ')}" failed with exit code ${code}`));
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
child.on('error', (error) => {
|
|
138
|
-
logger.error(`Command failed to start: ${error.message}`);
|
|
139
|
-
reject(error);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export async function run(command: string, options: child_process.ExecOptions = {}): Promise<{ stdout: string; stderr: string }> {
|
|
145
|
-
const logger = getLogger();
|
|
146
|
-
const execPromise = util.promisify(exec);
|
|
147
|
-
|
|
148
|
-
// Ensure encoding is set to 'utf8' to get string output instead of Buffer
|
|
149
|
-
const execOptions = { encoding: 'utf8' as const, ...options };
|
|
150
|
-
|
|
151
|
-
logger.verbose(`Executing command: ${command}`);
|
|
152
|
-
logger.verbose(`Working directory: ${execOptions?.cwd || process.cwd()}`);
|
|
153
|
-
logger.verbose(`Environment variables: ${Object.keys(execOptions?.env || process.env).length} variables`);
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
const result = await execPromise(command, execOptions);
|
|
157
|
-
logger.verbose(`Command completed successfully`);
|
|
158
|
-
logger.verbose(`stdout: ${result.stdout}`);
|
|
159
|
-
if (result.stderr) {
|
|
160
|
-
logger.verbose(`stderr: ${result.stderr}`);
|
|
161
|
-
}
|
|
162
|
-
// Ensure result is properly typed as strings
|
|
163
|
-
return {
|
|
164
|
-
stdout: String(result.stdout),
|
|
165
|
-
stderr: String(result.stderr)
|
|
166
|
-
};
|
|
167
|
-
} catch (error: any) {
|
|
168
|
-
logger.error(`Command failed: ${command}`);
|
|
169
|
-
logger.error(`Error: ${error.message}`);
|
|
170
|
-
logger.error(`Exit code: ${error.code}`);
|
|
171
|
-
logger.error(`Signal: ${error.signal}`);
|
|
172
|
-
if (error.stdout) {
|
|
173
|
-
logger.error(`stdout: ${error.stdout}`);
|
|
174
|
-
}
|
|
175
|
-
if (error.stderr) {
|
|
176
|
-
logger.error(`stderr: ${error.stderr}`);
|
|
177
|
-
}
|
|
178
|
-
throw error;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* @deprecated Use runSecureWithInheritedStdio instead for better security
|
|
184
|
-
* Legacy function for backward compatibility - parses shell command string
|
|
185
|
-
*/
|
|
186
|
-
export async function runWithInheritedStdio(command: string, options: child_process.ExecOptions = {}): Promise<void> {
|
|
187
|
-
|
|
188
|
-
// Parse command to extract command and arguments safely
|
|
189
|
-
const parts = command.trim().split(/\s+/);
|
|
190
|
-
if (parts.length === 0) {
|
|
191
|
-
throw new Error('Empty command provided');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const cmd = parts[0];
|
|
195
|
-
const args = parts.slice(1);
|
|
196
|
-
|
|
197
|
-
// Use the secure version
|
|
198
|
-
return runSecureWithInheritedStdio(cmd, args, options);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export async function runWithDryRunSupport(
|
|
202
|
-
command: string,
|
|
203
|
-
isDryRun: boolean,
|
|
204
|
-
options: child_process.ExecOptions = {},
|
|
205
|
-
useInheritedStdio: boolean = false
|
|
206
|
-
): Promise<{ stdout: string; stderr: string }> {
|
|
207
|
-
const logger = getLogger();
|
|
208
|
-
|
|
209
|
-
if (isDryRun) {
|
|
210
|
-
logger.info(`DRY RUN: Would execute command: ${command}`);
|
|
211
|
-
return { stdout: '', stderr: '' };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (useInheritedStdio) {
|
|
215
|
-
await runWithInheritedStdio(command, options);
|
|
216
|
-
return { stdout: '', stderr: '' }; // No output captured when using inherited stdio
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return run(command, options);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Secure version of runWithDryRunSupport using argument arrays
|
|
224
|
-
*/
|
|
225
|
-
export async function runSecureWithDryRunSupport(
|
|
226
|
-
command: string,
|
|
227
|
-
args: string[] = [],
|
|
228
|
-
isDryRun: boolean,
|
|
229
|
-
options: child_process.SpawnOptions = {},
|
|
230
|
-
useInheritedStdio: boolean = false
|
|
231
|
-
): Promise<{ stdout: string; stderr: string }> {
|
|
232
|
-
const logger = getLogger();
|
|
233
|
-
|
|
234
|
-
if (isDryRun) {
|
|
235
|
-
logger.info(`DRY RUN: Would execute command: ${command} ${args.join(' ')}`);
|
|
236
|
-
return { stdout: '', stderr: '' };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (useInheritedStdio) {
|
|
240
|
-
await runSecureWithInheritedStdio(command, args, options);
|
|
241
|
-
return { stdout: '', stderr: '' }; // No output captured when using inherited stdio
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return runSecure(command, args, options);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Export validation functions for use in other modules
|
|
248
|
-
export { validateGitRef, validateFilePath, escapeShellArg };
|
|
249
|
-
|