@sun-asterisk/sunlint 1.3.34 → 1.3.36

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 (90) hide show
  1. package/core/architecture-integration.js +16 -7
  2. package/core/auto-performance-manager.js +1 -1
  3. package/core/cli-action-handler.js +102 -2
  4. package/core/cli-program.js +102 -138
  5. package/core/file-targeting-service.js +62 -4
  6. package/core/git-utils.js +19 -12
  7. package/core/github-annotate-service.js +326 -11
  8. package/core/html-report-generator.js +326 -731
  9. package/core/impact-integration.js +551 -0
  10. package/core/output-service.js +293 -21
  11. package/core/scoring-service.js +3 -2
  12. package/engines/arch-detect/core/analyzer.js +413 -0
  13. package/engines/arch-detect/core/index.js +22 -0
  14. package/engines/arch-detect/engine/hybrid-detector.js +176 -0
  15. package/engines/arch-detect/engine/index.js +24 -0
  16. package/engines/arch-detect/engine/rule-executor.js +228 -0
  17. package/engines/arch-detect/engine/score-calculator.js +214 -0
  18. package/engines/arch-detect/engine/violation-detector.js +616 -0
  19. package/engines/arch-detect/index.js +50 -0
  20. package/engines/arch-detect/rules/base-rule.js +187 -0
  21. package/engines/arch-detect/rules/index.js +35 -0
  22. package/engines/arch-detect/rules/layered/index.js +28 -0
  23. package/engines/arch-detect/rules/layered/l001-presentation-layer.js +237 -0
  24. package/engines/arch-detect/rules/layered/l002-business-layer.js +215 -0
  25. package/engines/arch-detect/rules/layered/l003-data-layer.js +229 -0
  26. package/engines/arch-detect/rules/layered/l004-model-layer.js +204 -0
  27. package/engines/arch-detect/rules/layered/l005-layer-separation.js +215 -0
  28. package/engines/arch-detect/rules/layered/l006-dependency-direction.js +221 -0
  29. package/engines/arch-detect/rules/layered/layered-rules-collection.js +445 -0
  30. package/engines/arch-detect/rules/modular/index.js +27 -0
  31. package/engines/arch-detect/rules/modular/m001-feature-modules.js +238 -0
  32. package/engines/arch-detect/rules/modular/m002-core-module.js +169 -0
  33. package/engines/arch-detect/rules/modular/m003-module-declaration.js +186 -0
  34. package/engines/arch-detect/rules/modular/m004-public-api.js +171 -0
  35. package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +220 -0
  36. package/engines/arch-detect/rules/modular/modular-rules-collection.js +357 -0
  37. package/engines/arch-detect/rules/presentation/index.js +27 -0
  38. package/engines/arch-detect/rules/presentation/pr001-view-layer.js +221 -0
  39. package/engines/arch-detect/rules/presentation/pr002-presentation-logic.js +192 -0
  40. package/engines/arch-detect/rules/presentation/pr004-data-binding.js +187 -0
  41. package/engines/arch-detect/rules/presentation/pr006-router-layer.js +185 -0
  42. package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +181 -0
  43. package/engines/arch-detect/rules/presentation/presentation-rules-collection.js +507 -0
  44. package/engines/arch-detect/rules/project-scanner/index.js +31 -0
  45. package/engines/arch-detect/rules/project-scanner/ps001-project-root.js +213 -0
  46. package/engines/arch-detect/rules/project-scanner/ps002-language-detection.js +192 -0
  47. package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +339 -0
  48. package/engines/arch-detect/rules/project-scanner/ps004-build-system.js +171 -0
  49. package/engines/arch-detect/rules/project-scanner/ps005-source-directory.js +163 -0
  50. package/engines/arch-detect/rules/project-scanner/ps006-test-directory.js +184 -0
  51. package/engines/arch-detect/rules/project-scanner/ps007-documentation.js +149 -0
  52. package/engines/arch-detect/rules/project-scanner/ps008-cicd-detection.js +163 -0
  53. package/engines/arch-detect/rules/project-scanner/ps009-code-quality.js +152 -0
  54. package/engines/arch-detect/rules/project-scanner/ps010-statistics.js +180 -0
  55. package/engines/arch-detect/rules/rule-registry.js +111 -0
  56. package/engines/arch-detect/types/context.types.js +60 -0
  57. package/engines/arch-detect/types/enums.js +161 -0
  58. package/engines/arch-detect/types/index.js +25 -0
  59. package/engines/arch-detect/types/result.types.js +7 -0
  60. package/engines/arch-detect/types/rule.types.js +7 -0
  61. package/engines/arch-detect/utils/file-scanner.js +411 -0
  62. package/engines/arch-detect/utils/index.js +23 -0
  63. package/engines/arch-detect/utils/pattern-matcher.js +328 -0
  64. package/engines/impact/cli.js +106 -0
  65. package/engines/impact/config/default-config.js +54 -0
  66. package/engines/impact/core/change-detector.js +258 -0
  67. package/engines/impact/core/detectors/database-detector.js +1317 -0
  68. package/engines/impact/core/detectors/endpoint-detector.js +55 -0
  69. package/engines/impact/core/impact-analyzer.js +124 -0
  70. package/engines/impact/core/report-generator.js +462 -0
  71. package/engines/impact/core/utils/ast-parser.js +241 -0
  72. package/engines/impact/core/utils/dependency-graph.js +159 -0
  73. package/engines/impact/core/utils/file-utils.js +116 -0
  74. package/engines/impact/core/utils/git-utils.js +203 -0
  75. package/engines/impact/core/utils/logger.js +13 -0
  76. package/engines/impact/core/utils/method-call-graph.js +1192 -0
  77. package/engines/impact/index.js +135 -0
  78. package/engines/impact/package.json +29 -0
  79. package/package.json +18 -43
  80. package/scripts/build-release.sh +0 -0
  81. package/scripts/copy-impact-analyzer.js +135 -0
  82. package/scripts/install.sh +0 -0
  83. package/scripts/manual-release.sh +0 -0
  84. package/scripts/pre-release-test.sh +0 -0
  85. package/scripts/prepare-release.sh +0 -0
  86. package/scripts/quick-performance-test.js +0 -0
  87. package/scripts/setup-github-registry.sh +0 -0
  88. package/scripts/trigger-release.sh +0 -0
  89. package/scripts/verify-install.sh +0 -0
  90. package/templates/combined-report.html +1418 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * AST Parser Utilities
3
+ * Handles parsing and symbol extraction from JavaScript/TypeScript code
4
+ */
5
+
6
+ import { parse } from '@babel/parser';
7
+ import traverse from '@babel/traverse';
8
+
9
+ export class ASTParser {
10
+ /**
11
+ * Parse code and extract symbols
12
+ */
13
+ static extractSymbols(content, filePath) {
14
+ try {
15
+ const ast = parse(content, {
16
+ sourceType: 'module',
17
+ plugins: [
18
+ 'jsx',
19
+ 'typescript',
20
+ 'decorators-legacy',
21
+ 'classProperties',
22
+ 'dynamicImport',
23
+ 'optionalChaining',
24
+ 'nullishCoalescingOperator',
25
+ ],
26
+ });
27
+
28
+ return this.traverseAST(ast, filePath);
29
+ } catch (error) {
30
+ console.warn(`Failed to parse ${filePath}:`, error.message);
31
+ return [];
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Traverse AST and extract symbols
37
+ */
38
+ static traverseAST(ast, filePath) {
39
+ const symbols = [];
40
+ const self = ASTParser;
41
+
42
+ traverse.default(ast, {
43
+ // Function declarations
44
+ FunctionDeclaration(path) {
45
+ if (path.node.id) {
46
+ symbols.push({
47
+ type: 'function',
48
+ name: path.node.id.name,
49
+ signature: self.getSignature(path),
50
+ startLine: path.node.loc?.start.line || 0,
51
+ endLine: path.node.loc?.end.line || 0,
52
+ file: filePath,
53
+ });
54
+ }
55
+ },
56
+
57
+ // Arrow functions and function expressions assigned to variables
58
+ VariableDeclarator(path) {
59
+ if (
60
+ path.node.id.type === 'Identifier' &&
61
+ (path.node.init?.type === 'ArrowFunctionExpression' ||
62
+ path.node.init?.type === 'FunctionExpression')
63
+ ) {
64
+ symbols.push({
65
+ type: 'function',
66
+ name: path.node.id.name,
67
+ signature: self.getSignature(path.get('init')),
68
+ startLine: path.node.loc?.start.line || 0,
69
+ endLine: path.node.loc?.end.line || 0,
70
+ file: filePath,
71
+ });
72
+ }
73
+ },
74
+
75
+ // Class declarations
76
+ ClassDeclaration(path) {
77
+ if (path.node.id) {
78
+ symbols.push({
79
+ type: 'class',
80
+ name: path.node.id.name,
81
+ startLine: path.node.loc?.start.line || 0,
82
+ endLine: path.node.loc?.end.line || 0,
83
+ file: filePath,
84
+ });
85
+
86
+ // Extract class methods
87
+ path.node.body.body.forEach((member) => {
88
+ if (member.type === 'ClassMethod' && member.key.type === 'Identifier') {
89
+ symbols.push({
90
+ type: 'method',
91
+ name: `${path.node.id.name}.${member.key.name}`,
92
+ signature: self.getMethodSignature(member),
93
+ startLine: member.loc?.start.line || 0,
94
+ endLine: member.loc?.end.line || 0,
95
+ file: filePath,
96
+ });
97
+ }
98
+ });
99
+ }
100
+ },
101
+
102
+ // Export declarations
103
+ ExportNamedDeclaration(path) {
104
+ if (path.node.declaration) {
105
+ // Handle exported functions, classes, etc.
106
+ const declaration = path.node.declaration;
107
+ if (declaration.id) {
108
+ symbols.push({
109
+ type: declaration.type.toLowerCase().replace('declaration', ''),
110
+ name: declaration.id.name,
111
+ exported: true,
112
+ startLine: declaration.loc?.start.line || 0,
113
+ endLine: declaration.loc?.end.line || 0,
114
+ file: filePath,
115
+ });
116
+ }
117
+ }
118
+ },
119
+ });
120
+
121
+ return symbols;
122
+ }
123
+
124
+ /**
125
+ * Get function signature
126
+ */
127
+ static getSignature(path) {
128
+ try {
129
+ if (!path || !path.node) {
130
+ return '(...)';
131
+ }
132
+ const params = path.node.params || [];
133
+ const paramNames = params.map(p => {
134
+ if (p.type === 'Identifier') return p.name;
135
+ if (p.type === 'RestElement') return `...${p.argument.name}`;
136
+ return '_';
137
+ });
138
+ return `(${paramNames.join(', ')})`;
139
+ } catch (error) {
140
+ return '(...)';
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Get method signature
146
+ */
147
+ static getMethodSignature(node) {
148
+ try {
149
+ const params = node.params || [];
150
+ const paramNames = params.map(p => p.name || '_');
151
+ return `(${paramNames.join(', ')})`;
152
+ } catch (error) {
153
+ return '(...)';
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Extract imports from code
159
+ */
160
+ static extractImports(content) {
161
+ const imports = [];
162
+
163
+ try {
164
+ const ast = parse(content, {
165
+ sourceType: 'module',
166
+ plugins: ['jsx', 'typescript', 'dynamicImport'],
167
+ });
168
+
169
+ traverse.default(ast, {
170
+ ImportDeclaration(path) {
171
+ imports.push({
172
+ source: path.node.source.value,
173
+ specifiers: path.node.specifiers.map(s => ({
174
+ imported: s.imported?.name || s.local.name,
175
+ local: s.local.name,
176
+ })),
177
+ });
178
+ },
179
+
180
+ // Dynamic imports
181
+ CallExpression(path) {
182
+ if (path.node.callee.type === 'Import') {
183
+ const arg = path.node.arguments[0];
184
+ if (arg?.type === 'StringLiteral') {
185
+ imports.push({
186
+ source: arg.value,
187
+ dynamic: true,
188
+ });
189
+ }
190
+ }
191
+ },
192
+ });
193
+ } catch (error) {
194
+ // Ignore parse errors
195
+ }
196
+
197
+ return imports;
198
+ }
199
+
200
+ /**
201
+ * Find function calls in code
202
+ */
203
+ static findFunctionCalls(content, functionName) {
204
+ const calls = [];
205
+
206
+ try {
207
+ const ast = parse(content, {
208
+ sourceType: 'module',
209
+ plugins: ['jsx', 'typescript'],
210
+ });
211
+
212
+ traverse.default(ast, {
213
+ CallExpression(path) {
214
+ const callee = path.node.callee;
215
+
216
+ // Direct function call
217
+ if (callee.type === 'Identifier' && callee.name === functionName) {
218
+ calls.push({
219
+ line: path.node.loc?.start.line || 0,
220
+ type: 'direct',
221
+ });
222
+ }
223
+
224
+ // Method call
225
+ if (callee.type === 'MemberExpression' &&
226
+ callee.property.type === 'Identifier' &&
227
+ callee.property.name === functionName) {
228
+ calls.push({
229
+ line: path.node.loc?.start.line || 0,
230
+ type: 'method',
231
+ });
232
+ }
233
+ },
234
+ });
235
+ } catch (error) {
236
+ // Ignore parse errors
237
+ }
238
+
239
+ return calls;
240
+ }
241
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Dependency Graph Builder
3
+ * Builds and queries dependency relationships between code modules
4
+ */
5
+
6
+ export class DependencyGraph {
7
+ constructor() {
8
+ this.graph = new Map(); // file -> Set of dependencies
9
+ this.reverseGraph = new Map(); // file -> Set of dependents
10
+ this.symbolGraph = new Map(); // symbol -> Set of callers
11
+ }
12
+
13
+ /**
14
+ * Add a dependency edge
15
+ */
16
+ addDependency(from, to) {
17
+ if (! this.graph.has(from)) {
18
+ this.graph.set(from, new Set());
19
+ }
20
+ this.graph.get(from).add(to);
21
+
22
+ // Build reverse graph
23
+ if (!this.reverseGraph.has(to)) {
24
+ this.reverseGraph.set(to, new Set());
25
+ }
26
+ this.reverseGraph.get(to).add(from);
27
+ }
28
+
29
+ /**
30
+ * Add symbol usage
31
+ */
32
+ addSymbolUsage(symbol, usedIn) {
33
+ if (!this.symbolGraph.has(symbol)) {
34
+ this.symbolGraph.set(symbol, new Set());
35
+ }
36
+ this.symbolGraph.get(symbol).add(usedIn);
37
+ }
38
+
39
+ /**
40
+ * Get dependencies of a file
41
+ */
42
+ getDependencies(file) {
43
+ return Array.from(this.graph.get(file) || []);
44
+ }
45
+
46
+ /**
47
+ * Get dependents of a file (files that depend on this file)
48
+ */
49
+ getDependents(file) {
50
+ return Array.from(this.reverseGraph.get(file) || []);
51
+ }
52
+
53
+ /**
54
+ * Get callers of a symbol
55
+ */
56
+ getSymbolCallers(symbol) {
57
+ return Array.from(this.symbolGraph.get(symbol) || []);
58
+ }
59
+
60
+ /**
61
+ * Get transitive dependencies up to maxDepth
62
+ */
63
+ getTransitiveDependencies(file, maxDepth = 3) {
64
+ const visited = new Set();
65
+ const result = new Set();
66
+
67
+ const traverse = (current, depth) => {
68
+ if (depth > maxDepth || visited.has(current)) return;
69
+
70
+ visited.add(current);
71
+ const deps = this.getDependencies(current);
72
+
73
+ for (const dep of deps) {
74
+ result.add(dep);
75
+ traverse(dep, depth + 1);
76
+ }
77
+ };
78
+
79
+ traverse(file, 0);
80
+ return Array.from(result);
81
+ }
82
+
83
+ /**
84
+ * Get transitive dependents (reverse dependencies)
85
+ */
86
+ getTransitiveDependents(file, maxDepth = 3) {
87
+ const visited = new Set();
88
+ const result = new Set();
89
+
90
+ const traverse = (current, depth) => {
91
+ if (depth > maxDepth || visited.has(current)) return;
92
+
93
+ visited.add(current);
94
+ const dependents = this.getDependents(current);
95
+
96
+ for (const dependent of dependents) {
97
+ result.add(dependent);
98
+ traverse(dependent, depth + 1);
99
+ }
100
+ };
101
+
102
+ traverse(file, 0);
103
+ return Array.from(result);
104
+ }
105
+
106
+ /**
107
+ * Calculate impact radius for a file
108
+ */
109
+ getImpactRadius(file) {
110
+ const directDeps = this.getDependents(file);
111
+ const transitiveDeps = this.getTransitiveDependents(file, 2);
112
+
113
+ return {
114
+ direct: directDeps.length,
115
+ transitive: transitiveDeps.length,
116
+ total: transitiveDeps.length + directDeps.length,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Find path between two files
122
+ */
123
+ findPath(from, to, maxDepth = 5) {
124
+ const queue = [[from]];
125
+ const visited = new Set([from]);
126
+
127
+ while (queue.length > 0) {
128
+ const path = queue.shift();
129
+ const current = path[path.length - 1];
130
+
131
+ if (path.length > maxDepth) continue;
132
+ if (current === to) return path;
133
+
134
+ const deps = this.getDependencies(current);
135
+ for (const dep of deps) {
136
+ if (!visited.has(dep)) {
137
+ visited.add(dep);
138
+ queue.push([...path, dep]);
139
+ }
140
+ }
141
+ }
142
+
143
+ return null; // No path found
144
+ }
145
+
146
+ /**
147
+ * Get graph statistics
148
+ */
149
+ getStats() {
150
+ return {
151
+ totalFiles: this.graph.size,
152
+ totalEdges: Array.from(this.graph.values()).reduce((sum, set) => sum + set.size, 0),
153
+ totalSymbols: this.symbolGraph.size,
154
+ avgDependencies: this.graph.size > 0
155
+ ? Array.from(this.graph.values()).reduce((sum, set) => sum + set.size, 0) / this.graph.size
156
+ : 0,
157
+ };
158
+ }
159
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * File Utilities
3
+ * Helper functions for file operations
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { glob } from 'glob';
9
+
10
+ export class FileUtils {
11
+ /**
12
+ * Check if file is a source code file
13
+ */
14
+ static isSourceFile(filePath) {
15
+ const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
16
+ return extensions.some(ext => filePath.endsWith(ext));
17
+ }
18
+
19
+ /**
20
+ * Check if file is a test file
21
+ */
22
+ static isTestFile(filePath) {
23
+ return (
24
+ filePath.includes('.test.') ||
25
+ filePath.includes('.spec.') ||
26
+ filePath.includes('__tests__') ||
27
+ filePath.includes('__mocks__')
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Check if file is a config file
33
+ */
34
+ static isConfigFile(filePath) {
35
+ return (
36
+ filePath.includes('config') ||
37
+ filePath.endsWith('.config.js') ||
38
+ filePath.endsWith('.config.ts') ||
39
+ filePath.includes('jest.') ||
40
+ filePath.includes('webpack.') ||
41
+ filePath.includes('vite.')
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Categorize file type
47
+ */
48
+ static categorizeFile(filePath) {
49
+ if (this.isTestFile(filePath)) return 'test';
50
+ if (this.isConfigFile(filePath)) return 'config';
51
+ if (this.isSourceFile(filePath)) return 'source';
52
+ return 'other';
53
+ }
54
+
55
+ /**
56
+ * Get all source files in directory
57
+ */
58
+ static getAllSourceFiles(sourceDir, excludePaths = []) {
59
+ const pattern = `${sourceDir}/**/*.{js,jsx,ts,tsx,mjs,cjs}`;
60
+ const ignore = excludePaths.map(p => `${sourceDir}/**/${p}/**`);
61
+
62
+ try {
63
+ return glob.sync(pattern, { ignore, absolute: true });
64
+ } catch (error) {
65
+ console.error('Error scanning source files:', error.message);
66
+ return [];
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Read file content safely
72
+ */
73
+ static readFile(filePath) {
74
+ try {
75
+ return fs.readFileSync(filePath, 'utf-8');
76
+ } catch (error) {
77
+ console.warn(`Failed to read ${filePath}:`, error.message);
78
+ return '';
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Calculate line changes between two file versions
84
+ */
85
+ static calculateLineChanges(oldContent, newContent) {
86
+ const oldLines = oldContent ? oldContent.split('\n').length : 0;
87
+ const newLines = newContent ? newContent.split('\n').length : 0;
88
+
89
+ return {
90
+ added: Math.max(0, newLines - oldLines),
91
+ deleted: Math.max(0, oldLines - newLines),
92
+ total: Math.abs(newLines - oldLines),
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Extract module name from file path
98
+ */
99
+ static extractModuleName(filePath) {
100
+ const parts = filePath.split(path.sep);
101
+ const srcIndex = parts.indexOf('src');
102
+
103
+ if (srcIndex !== -1 && parts.length > srcIndex + 1) {
104
+ return parts[srcIndex + 1];
105
+ }
106
+
107
+ return path.basename(filePath, path.extname(filePath));
108
+ }
109
+
110
+ /**
111
+ * Get relative path from project root
112
+ */
113
+ static getRelativePath(filePath) {
114
+ return path.relative(process.cwd(), filePath);
115
+ }
116
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Git Utilities
3
+ * Helper functions for git operations
4
+ */
5
+
6
+ import { execSync } from 'child_process';
7
+ import path from 'path';
8
+ import fs from 'fs';
9
+
10
+ export class GitUtils {
11
+ /**
12
+ * Get list of changed files between two refs
13
+ * @param {string} baseRef - Base git reference
14
+ * @param {string} headRef - Head git reference (empty = working directory)
15
+ * @param {string} workDir - Working directory (optional, defaults to cwd)
16
+ */
17
+ static getChangedFiles(baseRef, headRef, workDir = null) {
18
+ try {
19
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
20
+
21
+ // Verify the directory exists and is a git repo
22
+ if (! fs.existsSync(cwd)) {
23
+ throw new Error(`Directory does not exist: ${cwd}`);
24
+ }
25
+
26
+ // If headRef is empty, compare baseRef with working directory
27
+ const diffCommand = headRef
28
+ ? `git diff --name-status ${baseRef}...${headRef}`
29
+ : `git diff --name-status ${baseRef}`;
30
+
31
+ const diffOutput = execSync(diffCommand, {
32
+ encoding: 'utf-8',
33
+ cwd: cwd
34
+ });
35
+
36
+ const changedFiles = [];
37
+ const lines = diffOutput.trim().split('\n').filter(line => line);
38
+
39
+ for (const line of lines) {
40
+ const parts = line.split('\t');
41
+ const status = parts[0];
42
+ const filePath = parts[1];
43
+
44
+ changedFiles.push({
45
+ status: this.parseStatus(status),
46
+ path: filePath,
47
+ });
48
+ }
49
+
50
+ return changedFiles;
51
+ } catch (error) {
52
+ console.error('Error getting changed files:', error.message);
53
+ console.error(' Working directory:', workDir || process.cwd());
54
+ console.error(' Base ref:', baseRef);
55
+ console.error(' Head ref:', headRef || '(working directory)');
56
+ return [];
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Get file content at specific ref
62
+ * @param {string} filePath - Relative file path
63
+ * @param {string} ref - Git reference
64
+ * @param {string} workDir - Working directory (optional)
65
+ */
66
+ static getFileContent(filePath, ref, workDir = null) {
67
+ try {
68
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
69
+
70
+ return execSync(`git show ${ref}:${filePath}`, {
71
+ encoding: 'utf-8',
72
+ cwd: cwd // FIXED: Execute in source directory
73
+ });
74
+ } catch (error) {
75
+ // File might be new or deleted
76
+ return '';
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get diff between two versions of a file
82
+ * @param {string} filePath - File path relative to git root (e.g., 'src/modules/file.ts')
83
+ * @param {string} baseRef - Base git reference
84
+ * @param {string} headRef - Head git reference (empty = working directory)
85
+ * @param {string} workDir - Working directory to run git from (optional)
86
+ */
87
+ static getFileDiff(filePath, baseRef, headRef, workDir = null) {
88
+ try {
89
+ // Determine working directory (should be git root or a path inside it)
90
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
91
+
92
+ // Get git root from the working directory
93
+ const gitRoot = this.getGitRoot(cwd);
94
+
95
+ if (!gitRoot) {
96
+ console.error('Not a git repository:', cwd);
97
+ return '';
98
+ }
99
+
100
+ // If headRef is empty, compare with working directory
101
+ const diffCommand = headRef
102
+ ? `git diff ${baseRef}...${headRef} -- ${filePath}`
103
+ : `git diff ${baseRef} -- ${filePath}`;
104
+
105
+ const result = execSync(diffCommand, {
106
+ encoding: 'utf-8',
107
+ cwd: gitRoot,
108
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large diffs
109
+ });
110
+
111
+ return result;
112
+ } catch (error) {
113
+ console.error('Error getting file diff:', error.message);
114
+ console.error(' File:', filePath);
115
+ console.error(' Base:', baseRef);
116
+ console.error(' Head:', headRef || '(working directory)');
117
+ console.error(' Working dir:', workDir);
118
+ return '';
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Parse git status code
124
+ */
125
+ static parseStatus(statusCode) {
126
+ const statusMap = {
127
+ 'A': 'added',
128
+ 'M': 'modified',
129
+ 'D': 'deleted',
130
+ 'R': 'renamed',
131
+ 'C': 'copied',
132
+ };
133
+ return statusMap[statusCode[0]] || 'unknown';
134
+ }
135
+
136
+ /**
137
+ * Get current branch name
138
+ */
139
+ static getCurrentBranch(workDir = null) {
140
+ try {
141
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
142
+
143
+ return execSync('git rev-parse --abbrev-ref HEAD', {
144
+ encoding: 'utf-8',
145
+ cwd: cwd
146
+ }).trim();
147
+ } catch (error) {
148
+ return 'unknown';
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Check if ref exists
154
+ */
155
+ static refExists(ref, workDir = null) {
156
+ try {
157
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
158
+
159
+ execSync(`git rev-parse --verify ${ref}`, {
160
+ encoding: 'utf-8',
161
+ stdio: 'ignore',
162
+ cwd: cwd
163
+ });
164
+ return true;
165
+ } catch (error) {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Check if directory is a git repository
172
+ */
173
+ static isGitRepo(workDir = null) {
174
+ try {
175
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
176
+
177
+ execSync('git rev-parse --git-dir', {
178
+ encoding: 'utf-8',
179
+ stdio: 'ignore',
180
+ cwd: cwd
181
+ });
182
+ return true;
183
+ } catch (error) {
184
+ return false;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Get git root directory
190
+ */
191
+ static getGitRoot(workDir = null) {
192
+ try {
193
+ const cwd = workDir ? path.resolve(workDir) : process.cwd();
194
+
195
+ return execSync('git rev-parse --show-toplevel', {
196
+ encoding: 'utf-8',
197
+ cwd: cwd
198
+ }).trim();
199
+ } catch (error) {
200
+ return null;
201
+ }
202
+ }
203
+ }