@eldrforge/git-tools 0.1.3 → 0.1.5
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 +6 -3
- package/.github/dependabot.yml +0 -12
- package/.github/workflows/npm-publish.yml +0 -48
- package/.github/workflows/test.yml +0 -33
- 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/src/index.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @eldrforge/git-tools
|
|
3
|
-
*
|
|
4
|
-
* Git utilities for automation - secure process execution and Git operations
|
|
5
|
-
*
|
|
6
|
-
* @module git-tools
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// Export logger interface and functions
|
|
10
|
-
export type { Logger } from './logger';
|
|
11
|
-
export {
|
|
12
|
-
ConsoleLogger,
|
|
13
|
-
setLogger,
|
|
14
|
-
getLogger
|
|
15
|
-
} from './logger';
|
|
16
|
-
|
|
17
|
-
// Export child process execution functions
|
|
18
|
-
export {
|
|
19
|
-
runSecure,
|
|
20
|
-
runSecureWithInheritedStdio,
|
|
21
|
-
run,
|
|
22
|
-
runWithInheritedStdio,
|
|
23
|
-
runWithDryRunSupport,
|
|
24
|
-
runSecureWithDryRunSupport,
|
|
25
|
-
validateGitRef,
|
|
26
|
-
validateFilePath,
|
|
27
|
-
escapeShellArg
|
|
28
|
-
} from './child';
|
|
29
|
-
|
|
30
|
-
// Export validation utilities
|
|
31
|
-
export {
|
|
32
|
-
safeJsonParse,
|
|
33
|
-
validateString,
|
|
34
|
-
validateHasProperty,
|
|
35
|
-
validatePackageJson
|
|
36
|
-
} from './validation';
|
|
37
|
-
|
|
38
|
-
// Export git utilities
|
|
39
|
-
export {
|
|
40
|
-
isValidGitRef,
|
|
41
|
-
findPreviousReleaseTag,
|
|
42
|
-
getCurrentVersion,
|
|
43
|
-
getDefaultFromRef,
|
|
44
|
-
getRemoteDefaultBranch,
|
|
45
|
-
localBranchExists,
|
|
46
|
-
remoteBranchExists,
|
|
47
|
-
getBranchCommitSha,
|
|
48
|
-
isBranchInSyncWithRemote,
|
|
49
|
-
safeSyncBranchWithRemote,
|
|
50
|
-
getCurrentBranch,
|
|
51
|
-
getGitStatusSummary,
|
|
52
|
-
getGloballyLinkedPackages,
|
|
53
|
-
getLinkedDependencies,
|
|
54
|
-
getLinkCompatibilityProblems,
|
|
55
|
-
getLinkProblems,
|
|
56
|
-
isNpmLinked
|
|
57
|
-
} from './git';
|
|
58
|
-
|
package/src/logger.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal logging interface that git-tools requires.
|
|
3
|
-
* Allows consumers to provide their own logger implementation (e.g., Winston, console, etc.)
|
|
4
|
-
*/
|
|
5
|
-
export interface Logger {
|
|
6
|
-
error(message: string, ...meta: any[]): void;
|
|
7
|
-
warn(message: string, ...meta: any[]): void;
|
|
8
|
-
info(message: string, ...meta: any[]): void;
|
|
9
|
-
verbose(message: string, ...meta: any[]): void;
|
|
10
|
-
debug(message: string, ...meta: any[]): void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Default console-based logger implementation
|
|
15
|
-
*/
|
|
16
|
-
export class ConsoleLogger implements Logger {
|
|
17
|
-
constructor(private level: string = 'info') {}
|
|
18
|
-
|
|
19
|
-
private shouldLog(level: string): boolean {
|
|
20
|
-
const levels = ['error', 'warn', 'info', 'verbose', 'debug'];
|
|
21
|
-
const currentLevelIndex = levels.indexOf(this.level);
|
|
22
|
-
const messageLevelIndex = levels.indexOf(level);
|
|
23
|
-
return messageLevelIndex <= currentLevelIndex;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
error(message: string, ...meta: any[]): void {
|
|
27
|
-
if (this.shouldLog('error')) {
|
|
28
|
-
// eslint-disable-next-line no-console
|
|
29
|
-
console.error(message, ...meta);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
warn(message: string, ...meta: any[]): void {
|
|
34
|
-
if (this.shouldLog('warn')) {
|
|
35
|
-
// eslint-disable-next-line no-console
|
|
36
|
-
console.warn(message, ...meta);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
info(message: string, ...meta: any[]): void {
|
|
41
|
-
if (this.shouldLog('info')) {
|
|
42
|
-
// eslint-disable-next-line no-console
|
|
43
|
-
console.info(message, ...meta);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
verbose(message: string, ...meta: any[]): void {
|
|
48
|
-
if (this.shouldLog('verbose')) {
|
|
49
|
-
// eslint-disable-next-line no-console
|
|
50
|
-
console.log('[VERBOSE]', message, ...meta);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
debug(message: string, ...meta: any[]): void {
|
|
55
|
-
if (this.shouldLog('debug')) {
|
|
56
|
-
// eslint-disable-next-line no-console
|
|
57
|
-
console.log('[DEBUG]', message, ...meta);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Global logger instance - defaults to console logger
|
|
64
|
-
* Use setLogger() to replace with your own implementation
|
|
65
|
-
*/
|
|
66
|
-
let globalLogger: Logger = new ConsoleLogger();
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Set the global logger instance
|
|
70
|
-
*/
|
|
71
|
-
export function setLogger(logger: Logger): void {
|
|
72
|
-
globalLogger = logger;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Get the global logger instance
|
|
77
|
-
*/
|
|
78
|
-
export function getLogger(): Logger {
|
|
79
|
-
return globalLogger;
|
|
80
|
-
}
|
|
81
|
-
|
package/src/validation.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime validation utilities for safe type handling
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Safely parses JSON with error handling
|
|
7
|
-
*/
|
|
8
|
-
export const safeJsonParse = <T = any>(jsonString: string, context?: string): T => {
|
|
9
|
-
try {
|
|
10
|
-
const parsed = JSON.parse(jsonString);
|
|
11
|
-
if (parsed === null || parsed === undefined) {
|
|
12
|
-
throw new Error('Parsed JSON is null or undefined');
|
|
13
|
-
}
|
|
14
|
-
return parsed;
|
|
15
|
-
} catch (error) {
|
|
16
|
-
const contextStr = context ? ` (${context})` : '';
|
|
17
|
-
throw new Error(`Failed to parse JSON${contextStr}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Validates that a value is a non-empty string
|
|
23
|
-
*/
|
|
24
|
-
export const validateString = (value: any, fieldName: string): string => {
|
|
25
|
-
if (typeof value !== 'string') {
|
|
26
|
-
throw new Error(`${fieldName} must be a string, got ${typeof value}`);
|
|
27
|
-
}
|
|
28
|
-
if (value.trim() === '') {
|
|
29
|
-
throw new Error(`${fieldName} cannot be empty`);
|
|
30
|
-
}
|
|
31
|
-
return value;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Validates that a value exists and has a specific property
|
|
36
|
-
*/
|
|
37
|
-
export const validateHasProperty = (obj: any, property: string, context?: string): void => {
|
|
38
|
-
if (!obj || typeof obj !== 'object') {
|
|
39
|
-
const contextStr = context ? ` in ${context}` : '';
|
|
40
|
-
throw new Error(`Object is null or not an object${contextStr}`);
|
|
41
|
-
}
|
|
42
|
-
if (!(property in obj)) {
|
|
43
|
-
const contextStr = context ? ` in ${context}` : '';
|
|
44
|
-
throw new Error(`Missing required property '${property}'${contextStr}`);
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Validates package.json structure has basic required fields
|
|
50
|
-
*/
|
|
51
|
-
export const validatePackageJson = (data: any, context?: string, requireName: boolean = true): any => {
|
|
52
|
-
if (!data || typeof data !== 'object') {
|
|
53
|
-
const contextStr = context ? ` (${context})` : '';
|
|
54
|
-
throw new Error(`Invalid package.json${contextStr}: not an object`);
|
|
55
|
-
}
|
|
56
|
-
if (requireName && typeof data.name !== 'string') {
|
|
57
|
-
const contextStr = context ? ` (${context})` : '';
|
|
58
|
-
throw new Error(`Invalid package.json${contextStr}: name must be a string`);
|
|
59
|
-
}
|
|
60
|
-
return data;
|
|
61
|
-
};
|
|
62
|
-
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { runSecure, runSecureWithInheritedStdio, validateGitRef, validateFilePath, escapeShellArg } from '../src/child';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Integration tests that actually execute commands (not mocked)
|
|
6
|
-
* These test the real implementation paths
|
|
7
|
-
*/
|
|
8
|
-
describe('child.ts - integration tests', () => {
|
|
9
|
-
describe('runSecure', () => {
|
|
10
|
-
it('should execute simple echo command', async () => {
|
|
11
|
-
const { stdout } = await runSecure('echo', ['hello']);
|
|
12
|
-
expect(stdout.trim()).toBe('hello');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should handle multiple arguments', async () => {
|
|
16
|
-
const { stdout } = await runSecure('echo', ['hello', 'world']);
|
|
17
|
-
expect(stdout.trim()).toContain('hello');
|
|
18
|
-
expect(stdout.trim()).toContain('world');
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('should capture stderr', async () => {
|
|
22
|
-
// Use a command that writes to stderr
|
|
23
|
-
const { stderr } = await runSecure('node', ['-e', 'console.error("test error")']);
|
|
24
|
-
expect(stderr.trim()).toBe('test error');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should throw error for non-existent command', async () => {
|
|
28
|
-
await expect(runSecure('nonexistent-command-12345', [])).rejects.toThrow();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should throw error for command with non-zero exit code', async () => {
|
|
32
|
-
await expect(runSecure('node', ['-e', 'process.exit(1)'])).rejects.toThrow();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should handle empty args array', async () => {
|
|
36
|
-
const { stdout } = await runSecure('pwd', []);
|
|
37
|
-
expect(stdout).toBeTruthy();
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('runSecureWithInheritedStdio', () => {
|
|
42
|
-
it('should execute command successfully', async () => {
|
|
43
|
-
await expect(runSecureWithInheritedStdio('echo', ['test'])).resolves.toBeUndefined();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should throw error for command with non-zero exit code', async () => {
|
|
47
|
-
await expect(runSecureWithInheritedStdio('node', ['-e', 'process.exit(1)'])).rejects.toThrow();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should throw error for non-existent command', async () => {
|
|
51
|
-
await expect(runSecureWithInheritedStdio('nonexistent-cmd-98765', [])).rejects.toThrow();
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe('validateGitRef', () => {
|
|
56
|
-
it('should return true for valid simple ref', () => {
|
|
57
|
-
expect(validateGitRef('main')).toBe(true);
|
|
58
|
-
expect(validateGitRef('feature-branch')).toBe(true);
|
|
59
|
-
expect(validateGitRef('v1.2.3')).toBe(true);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should return true for refs with slashes', () => {
|
|
63
|
-
expect(validateGitRef('refs/heads/main')).toBe(true);
|
|
64
|
-
expect(validateGitRef('origin/main')).toBe(true);
|
|
65
|
-
expect(validateGitRef('release/v1.2.x')).toBe(true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('should return true for refs with dots', () => {
|
|
69
|
-
expect(validateGitRef('v1.2.3')).toBe(true);
|
|
70
|
-
expect(validateGitRef('release/1.2.x')).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should return false for refs with double dots (directory traversal)', () => {
|
|
74
|
-
expect(validateGitRef('../etc/passwd')).toBe(false);
|
|
75
|
-
expect(validateGitRef('refs/../../../etc')).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should return false for refs starting with dash (flag injection)', () => {
|
|
79
|
-
expect(validateGitRef('-rf')).toBe(false);
|
|
80
|
-
expect(validateGitRef('--delete')).toBe(false);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should return false for refs with shell metacharacters', () => {
|
|
84
|
-
expect(validateGitRef('main; rm -rf /')).toBe(false);
|
|
85
|
-
expect(validateGitRef('main|cat /etc/passwd')).toBe(false);
|
|
86
|
-
expect(validateGitRef('main&whoami')).toBe(false);
|
|
87
|
-
expect(validateGitRef('main`whoami`')).toBe(false);
|
|
88
|
-
expect(validateGitRef('main$(whoami)')).toBe(false);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should return false for refs with spaces', () => {
|
|
92
|
-
expect(validateGitRef('main branch')).toBe(false);
|
|
93
|
-
expect(validateGitRef('feature test')).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should return false for refs with brackets', () => {
|
|
97
|
-
expect(validateGitRef('main[test]')).toBe(false);
|
|
98
|
-
expect(validateGitRef('branch{test}')).toBe(false);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should return true for valid refs with underscores', () => {
|
|
102
|
-
expect(validateGitRef('feature_branch')).toBe(true);
|
|
103
|
-
expect(validateGitRef('test_123')).toBe(true);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should return true for valid refs with numbers', () => {
|
|
107
|
-
expect(validateGitRef('v1234567890')).toBe(true);
|
|
108
|
-
expect(validateGitRef('release123')).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('validateFilePath', () => {
|
|
113
|
-
it('should return true for valid file paths', () => {
|
|
114
|
-
expect(validateFilePath('/path/to/file.txt')).toBe(true);
|
|
115
|
-
expect(validateFilePath('./relative/path.js')).toBe(true);
|
|
116
|
-
expect(validateFilePath('../parent/file.md')).toBe(true);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should return false for paths with shell metacharacters', () => {
|
|
120
|
-
expect(validateFilePath('/path;rm -rf /')).toBe(false);
|
|
121
|
-
expect(validateFilePath('/path|cat /etc/passwd')).toBe(false);
|
|
122
|
-
expect(validateFilePath('/path&whoami')).toBe(false);
|
|
123
|
-
expect(validateFilePath('/path`whoami`')).toBe(false);
|
|
124
|
-
expect(validateFilePath('/path$(whoami)')).toBe(false);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('should return false for paths with brackets', () => {
|
|
128
|
-
expect(validateFilePath('/path[test]')).toBe(false);
|
|
129
|
-
expect(validateFilePath('/path{test}')).toBe(false);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should return true for paths with spaces and dots', () => {
|
|
133
|
-
expect(validateFilePath('/path with spaces/file.txt')).toBe(true);
|
|
134
|
-
expect(validateFilePath('/path/to/../file.txt')).toBe(true);
|
|
135
|
-
expect(validateFilePath('./file.txt')).toBe(true);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe('escapeShellArg', () => {
|
|
140
|
-
it('should escape single quotes on Unix', () => {
|
|
141
|
-
if (process.platform !== 'win32') {
|
|
142
|
-
const result = escapeShellArg("test'string");
|
|
143
|
-
expect(result).toContain("'\\''");
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should escape double quotes on Windows', () => {
|
|
148
|
-
if (process.platform === 'win32') {
|
|
149
|
-
const result = escapeShellArg('test"string');
|
|
150
|
-
expect(result).toContain('\\"');
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('should handle normal strings without special chars', () => {
|
|
155
|
-
const result = escapeShellArg('normal-string-123');
|
|
156
|
-
expect(result).toBeTruthy();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should handle empty string', () => {
|
|
160
|
-
const result = escapeShellArg('');
|
|
161
|
-
expect(result).toBeTruthy();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should handle strings with spaces', () => {
|
|
165
|
-
const result = escapeShellArg('string with spaces');
|
|
166
|
-
expect(result).toBeTruthy();
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|