@m3hti/commit-genie 3.0.1 → 3.1.1

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 (57) hide show
  1. package/dist/services/analyzerService.d.ts +0 -141
  2. package/dist/services/analyzerService.d.ts.map +1 -1
  3. package/dist/services/analyzerService.js +23 -1880
  4. package/dist/services/analyzerService.js.map +1 -1
  5. package/dist/services/analyzerService.test.js +97 -0
  6. package/dist/services/analyzerService.test.js.map +1 -1
  7. package/dist/services/breakingChangeDetector.d.ts +9 -0
  8. package/dist/services/breakingChangeDetector.d.ts.map +1 -0
  9. package/dist/services/breakingChangeDetector.js +76 -0
  10. package/dist/services/breakingChangeDetector.js.map +1 -0
  11. package/dist/services/commitTypeDetector.d.ts +32 -0
  12. package/dist/services/commitTypeDetector.d.ts.map +1 -0
  13. package/dist/services/commitTypeDetector.js +490 -0
  14. package/dist/services/commitTypeDetector.js.map +1 -0
  15. package/dist/services/descriptionGenerator.d.ts +58 -0
  16. package/dist/services/descriptionGenerator.d.ts.map +1 -0
  17. package/dist/services/descriptionGenerator.js +569 -0
  18. package/dist/services/descriptionGenerator.js.map +1 -0
  19. package/dist/services/fileContentAnalyzer.d.ts +10 -0
  20. package/dist/services/fileContentAnalyzer.d.ts.map +1 -0
  21. package/dist/services/fileContentAnalyzer.js +170 -0
  22. package/dist/services/fileContentAnalyzer.js.map +1 -0
  23. package/dist/services/fileContentAnalyzer.test.d.ts +2 -0
  24. package/dist/services/fileContentAnalyzer.test.d.ts.map +1 -0
  25. package/dist/services/fileContentAnalyzer.test.js +118 -0
  26. package/dist/services/fileContentAnalyzer.test.js.map +1 -0
  27. package/dist/services/gitService.test.js +242 -24
  28. package/dist/services/gitService.test.js.map +1 -1
  29. package/dist/services/hookService.test.d.ts +2 -0
  30. package/dist/services/hookService.test.d.ts.map +1 -0
  31. package/dist/services/hookService.test.js +182 -0
  32. package/dist/services/hookService.test.js.map +1 -0
  33. package/dist/services/lintService.test.d.ts +2 -0
  34. package/dist/services/lintService.test.d.ts.map +1 -0
  35. package/dist/services/lintService.test.js +288 -0
  36. package/dist/services/lintService.test.js.map +1 -0
  37. package/dist/services/messageBuilder.d.ts +16 -0
  38. package/dist/services/messageBuilder.d.ts.map +1 -0
  39. package/dist/services/messageBuilder.js +135 -0
  40. package/dist/services/messageBuilder.js.map +1 -0
  41. package/dist/services/scopeDetector.d.ts +6 -0
  42. package/dist/services/scopeDetector.d.ts.map +1 -0
  43. package/dist/services/scopeDetector.js +51 -0
  44. package/dist/services/scopeDetector.js.map +1 -0
  45. package/dist/services/semanticAnalyzer.d.ts +25 -0
  46. package/dist/services/semanticAnalyzer.d.ts.map +1 -0
  47. package/dist/services/semanticAnalyzer.js +713 -0
  48. package/dist/services/semanticAnalyzer.js.map +1 -0
  49. package/dist/services/splitService.test.d.ts +2 -0
  50. package/dist/services/splitService.test.d.ts.map +1 -0
  51. package/dist/services/splitService.test.js +190 -0
  52. package/dist/services/splitService.test.js.map +1 -0
  53. package/dist/services/statsService.test.d.ts +2 -0
  54. package/dist/services/statsService.test.d.ts.map +1 -0
  55. package/dist/services/statsService.test.js +211 -0
  56. package/dist/services/statsService.test.js.map +1 -0
  57. package/package.json +1 -1
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeFileContent = analyzeFileContent;
4
+ const gitService_1 = require("./gitService");
5
+ /**
6
+ * Analyze file content changes for non-JS/TS files.
7
+ * Returns structural descriptions of what changed (keys, selectors, headings, etc.)
8
+ */
9
+ function analyzeFileContent(filePath, diff) {
10
+ const ext = filePath.split('.').pop()?.toLowerCase();
11
+ switch (ext) {
12
+ case 'json':
13
+ return analyzeJsonChanges(filePath);
14
+ case 'css':
15
+ case 'scss':
16
+ case 'sass':
17
+ case 'less':
18
+ return analyzeCssChanges(diff);
19
+ case 'md':
20
+ case 'mdx':
21
+ return analyzeMarkdownChanges(diff);
22
+ case 'yml':
23
+ case 'yaml':
24
+ return analyzeYamlChanges(diff);
25
+ default:
26
+ return null;
27
+ }
28
+ }
29
+ function analyzeJsonChanges(filePath) {
30
+ try {
31
+ const oldContent = gitService_1.GitService.getFileAtHead(filePath);
32
+ const newContent = gitService_1.GitService.getStagedFileContent(filePath);
33
+ // New file
34
+ if (!oldContent && newContent) {
35
+ const newObj = JSON.parse(newContent);
36
+ const keys = Object.keys(newObj).slice(0, 5);
37
+ return { changedElements: keys, changeKind: 'added' };
38
+ }
39
+ // Deleted file
40
+ if (oldContent && !newContent) {
41
+ const oldObj = JSON.parse(oldContent);
42
+ const keys = Object.keys(oldObj).slice(0, 5);
43
+ return { changedElements: keys, changeKind: 'removed' };
44
+ }
45
+ if (!oldContent || !newContent)
46
+ return null;
47
+ const oldObj = JSON.parse(oldContent);
48
+ const newObj = JSON.parse(newContent);
49
+ const oldKeys = new Set(Object.keys(oldObj));
50
+ const newKeys = new Set(Object.keys(newObj));
51
+ const addedKeys = [];
52
+ const removedKeys = [];
53
+ const modifiedKeys = [];
54
+ for (const key of newKeys) {
55
+ if (!oldKeys.has(key))
56
+ addedKeys.push(key);
57
+ }
58
+ for (const key of oldKeys) {
59
+ if (!newKeys.has(key))
60
+ removedKeys.push(key);
61
+ }
62
+ for (const key of newKeys) {
63
+ if (oldKeys.has(key) && JSON.stringify(oldObj[key]) !== JSON.stringify(newObj[key])) {
64
+ modifiedKeys.push(key);
65
+ }
66
+ }
67
+ const changedElements = [...addedKeys, ...removedKeys, ...modifiedKeys];
68
+ if (changedElements.length === 0)
69
+ return null;
70
+ let changeKind;
71
+ if (addedKeys.length > 0 && removedKeys.length === 0 && modifiedKeys.length === 0) {
72
+ changeKind = 'added';
73
+ }
74
+ else if (removedKeys.length > 0 && addedKeys.length === 0 && modifiedKeys.length === 0) {
75
+ changeKind = 'removed';
76
+ }
77
+ else if (modifiedKeys.length > 0 && addedKeys.length === 0 && removedKeys.length === 0) {
78
+ changeKind = 'modified';
79
+ }
80
+ else {
81
+ changeKind = 'mixed';
82
+ }
83
+ return { changedElements: changedElements.slice(0, 5), changeKind };
84
+ }
85
+ catch {
86
+ return null;
87
+ }
88
+ }
89
+ function analyzeCssChanges(diff) {
90
+ if (!diff)
91
+ return null;
92
+ const selectors = [];
93
+ const lines = diff.split('\n');
94
+ for (const line of lines) {
95
+ if (!line.startsWith('+') && !line.startsWith('-'))
96
+ continue;
97
+ if (line.startsWith('+++') || line.startsWith('---'))
98
+ continue;
99
+ const content = line.substring(1).trim();
100
+ // Match CSS selectors: .class {, #id {, element {
101
+ const selectorMatch = content.match(/^([.#@]?[a-zA-Z_-][\w-]*(?:\s*[>,+~]\s*[.#]?[\w-]+)*)\s*\{/);
102
+ if (selectorMatch) {
103
+ const selector = selectorMatch[1].trim();
104
+ if (!selectors.includes(selector)) {
105
+ selectors.push(selector);
106
+ }
107
+ continue;
108
+ }
109
+ // Match @-rules: @media (...), @keyframes name
110
+ const atRuleMatch = content.match(/^(@(?:media|keyframes|font-face|supports|layer)\b[^{]*)/);
111
+ if (atRuleMatch) {
112
+ const rule = atRuleMatch[1].trim();
113
+ const short = rule.length > 30 ? rule.substring(0, 30) + '...' : rule;
114
+ if (!selectors.includes(short)) {
115
+ selectors.push(short);
116
+ }
117
+ }
118
+ }
119
+ if (selectors.length === 0)
120
+ return null;
121
+ return { changedElements: selectors.slice(0, 5), changeKind: 'modified' };
122
+ }
123
+ function analyzeMarkdownChanges(diff) {
124
+ if (!diff)
125
+ return null;
126
+ const headings = [];
127
+ const lines = diff.split('\n');
128
+ for (const line of lines) {
129
+ if (!line.startsWith('+') && !line.startsWith('-'))
130
+ continue;
131
+ if (line.startsWith('+++') || line.startsWith('---'))
132
+ continue;
133
+ const content = line.substring(1);
134
+ const headingMatch = content.match(/^(#{1,6})\s+(.+)$/);
135
+ if (headingMatch) {
136
+ const title = headingMatch[2].trim();
137
+ if (!headings.includes(title)) {
138
+ headings.push(title);
139
+ }
140
+ }
141
+ }
142
+ if (headings.length === 0)
143
+ return null;
144
+ return { changedElements: headings.slice(0, 5), changeKind: 'modified' };
145
+ }
146
+ function analyzeYamlChanges(diff) {
147
+ if (!diff)
148
+ return null;
149
+ const keys = [];
150
+ const lines = diff.split('\n');
151
+ for (const line of lines) {
152
+ if (!line.startsWith('+') && !line.startsWith('-'))
153
+ continue;
154
+ if (line.startsWith('+++') || line.startsWith('---'))
155
+ continue;
156
+ const content = line.substring(1);
157
+ // Match top-level keys (no leading whitespace)
158
+ const topLevelMatch = content.match(/^([a-zA-Z_][\w.-]*)\s*:/);
159
+ if (topLevelMatch) {
160
+ const key = topLevelMatch[1];
161
+ if (!keys.includes(key)) {
162
+ keys.push(key);
163
+ }
164
+ }
165
+ }
166
+ if (keys.length === 0)
167
+ return null;
168
+ return { changedElements: keys.slice(0, 5), changeKind: 'modified' };
169
+ }
170
+ //# sourceMappingURL=fileContentAnalyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileContentAnalyzer.js","sourceRoot":"","sources":["../../src/services/fileContentAnalyzer.ts"],"names":[],"mappings":";;AAWA,gDAuBC;AAlCD,6CAA0C;AAO1C;;;GAGG;AACH,SAAgB,kBAAkB,CAChC,QAAgB,EAChB,IAAY;IAEZ,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,CAAC;IAErD,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,MAAM;YACT,OAAO,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QACtC,KAAK,KAAK,CAAC;QACX,KAAK,MAAM,CAAC;QACZ,KAAK,MAAM,CAAC;QACZ,KAAK,MAAM;YACT,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACjC,KAAK,IAAI,CAAC;QACV,KAAK,KAAK;YACR,OAAO,sBAAsB,CAAC,IAAI,CAAC,CAAC;QACtC,KAAK,KAAK,CAAC;QACX,KAAK,MAAM;YACT,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC;QAClC;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,QAAgB;IAC1C,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,uBAAU,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACtD,MAAM,UAAU,GAAG,uBAAU,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC;QAE7D,WAAW;QACX,IAAI,CAAC,UAAU,IAAI,UAAU,EAAE,CAAC;YAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACtC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAC7C,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;QACxD,CAAC;QAED,eAAe;QACf,IAAI,UAAU,IAAI,CAAC,UAAU,EAAE,CAAC;YAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YACtC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YAC7C,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC;QAC1D,CAAC;QAED,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU;YAAE,OAAO,IAAI,CAAC;QAE5C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACtC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QAEtC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAa,EAAE,CAAC;QAC/B,MAAM,WAAW,GAAa,EAAE,CAAC;QACjC,MAAM,YAAY,GAAa,EAAE,CAAC;QAElC,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7C,CAAC;QAED,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/C,CAAC;QAED,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;gBACpF,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACzB,CAAC;QACH,CAAC;QAED,MAAM,eAAe,GAAG,CAAC,GAAG,SAAS,EAAE,GAAG,WAAW,EAAE,GAAG,YAAY,CAAC,CAAC;QACxE,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAE9C,IAAI,UAA6C,CAAC;QAClD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAClF,UAAU,GAAG,OAAO,CAAC;QACvB,CAAC;aAAM,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzF,UAAU,GAAG,SAAS,CAAC;QACzB,CAAC;aAAM,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzF,UAAU,GAAG,UAAU,CAAC;QAC1B,CAAC;aAAM,CAAC;YACN,UAAU,GAAG,OAAO,CAAC;QACvB,CAAC;QAED,OAAO,EAAE,eAAe,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvB,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAC7D,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,SAAS;QAE/D,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEzC,kDAAkD;QAClD,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,4DAA4D,CAAC,CAAC;QAClG,IAAI,aAAa,EAAE,CAAC;YAClB,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACzC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAClC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC3B,CAAC;YACD,SAAS;QACX,CAAC;QAED,+CAA+C;QAC/C,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC7F,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACnC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;YACtE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC/B,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,EAAE,eAAe,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AAC5E,CAAC;AAED,SAAS,sBAAsB,CAAC,IAAY;IAC1C,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvB,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAC7D,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,SAAS;QAE/D,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAElC,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;QACxD,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACrC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC9B,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACvC,OAAO,EAAE,eAAe,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AAC3E,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAY;IACtC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEvB,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAE/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAC7D,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,SAAS;QAE/D,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAElC,+CAA+C;QAC/C,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC/D,IAAI,aAAa,EAAE,CAAC;YAClB,MAAM,GAAG,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;YAC7B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBACxB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,EAAE,eAAe,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC;AACvE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fileContentAnalyzer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileContentAnalyzer.test.d.ts","sourceRoot":"","sources":["../../src/services/fileContentAnalyzer.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fileContentAnalyzer_1 = require("./fileContentAnalyzer");
4
+ const gitService_1 = require("./gitService");
5
+ jest.mock('./gitService');
6
+ const mockedGitService = gitService_1.GitService;
7
+ describe('fileContentAnalyzer', () => {
8
+ beforeEach(() => {
9
+ jest.clearAllMocks();
10
+ });
11
+ describe('JSON parser', () => {
12
+ it('should detect changed top-level keys in package.json', () => {
13
+ mockedGitService.getFileAtHead.mockReturnValue(JSON.stringify({ name: 'app', version: '1.0.0', scripts: { test: 'jest' } }, null, 2));
14
+ mockedGitService.getStagedFileContent.mockReturnValue(JSON.stringify({ name: 'app', version: '1.1.0', scripts: { test: 'jest', build: 'tsc' } }, null, 2));
15
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('package.json', '');
16
+ expect(result).not.toBeNull();
17
+ expect(result.changedElements).toContain('version');
18
+ expect(result.changedElements).toContain('scripts');
19
+ });
20
+ it('should detect added top-level keys', () => {
21
+ mockedGitService.getFileAtHead.mockReturnValue(JSON.stringify({ name: 'app' }, null, 2));
22
+ mockedGitService.getStagedFileContent.mockReturnValue(JSON.stringify({ name: 'app', description: 'My app' }, null, 2));
23
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('package.json', '');
24
+ expect(result).not.toBeNull();
25
+ expect(result.changedElements).toContain('description');
26
+ expect(result.changeKind).toBe('added');
27
+ });
28
+ it('should detect removed top-level keys', () => {
29
+ mockedGitService.getFileAtHead.mockReturnValue(JSON.stringify({ name: 'app', deprecated: true }, null, 2));
30
+ mockedGitService.getStagedFileContent.mockReturnValue(JSON.stringify({ name: 'app' }, null, 2));
31
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('package.json', '');
32
+ expect(result).not.toBeNull();
33
+ expect(result.changedElements).toContain('deprecated');
34
+ expect(result.changeKind).toBe('removed');
35
+ });
36
+ it('should return null for unparseable JSON', () => {
37
+ mockedGitService.getFileAtHead.mockReturnValue('not json');
38
+ mockedGitService.getStagedFileContent.mockReturnValue('also not json');
39
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('config.json', '');
40
+ expect(result).toBeNull();
41
+ });
42
+ it('should handle new JSON files (no old content)', () => {
43
+ mockedGitService.getFileAtHead.mockReturnValue(null);
44
+ mockedGitService.getStagedFileContent.mockReturnValue(JSON.stringify({ name: 'app', version: '1.0.0' }, null, 2));
45
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('config.json', '');
46
+ expect(result).not.toBeNull();
47
+ expect(result.changeKind).toBe('added');
48
+ });
49
+ });
50
+ describe('CSS parser', () => {
51
+ it('should detect changed selectors from diff', () => {
52
+ const diff = '+.header {\n' +
53
+ '+ background: red;\n' +
54
+ '+}\n' +
55
+ '-#footer {\n' +
56
+ '- display: none;\n' +
57
+ '-}\n';
58
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('styles.css', diff);
59
+ expect(result).not.toBeNull();
60
+ expect(result.changedElements).toContain('.header');
61
+ expect(result.changedElements).toContain('#footer');
62
+ });
63
+ it('should detect @media and @keyframes', () => {
64
+ const diff = '+@media (max-width: 768px) {\n' +
65
+ '+ .container { width: 100%; }\n' +
66
+ '+}\n';
67
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('styles.css', diff);
68
+ expect(result).not.toBeNull();
69
+ expect(result.changedElements.some((e) => e.includes('@media'))).toBe(true);
70
+ });
71
+ it('should return null for empty diff', () => {
72
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('styles.css', '');
73
+ expect(result).toBeNull();
74
+ });
75
+ });
76
+ describe('Markdown parser', () => {
77
+ it('should detect changed headings', () => {
78
+ const diff = '+## Installation\n' +
79
+ '+\n' +
80
+ '+Run npm install to get started.\n' +
81
+ '-## Setup\n' +
82
+ '-\n' +
83
+ '-Follow these steps.\n';
84
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('README.md', diff);
85
+ expect(result).not.toBeNull();
86
+ expect(result.changedElements).toContain('Installation');
87
+ expect(result.changedElements).toContain('Setup');
88
+ });
89
+ it('should handle multiple heading levels', () => {
90
+ const diff = '+# Overview\n+### Quick Start\n';
91
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('docs.md', diff);
92
+ expect(result).not.toBeNull();
93
+ expect(result.changedElements).toContain('Overview');
94
+ expect(result.changedElements).toContain('Quick Start');
95
+ });
96
+ });
97
+ describe('YAML parser', () => {
98
+ it('should detect changed top-level keys', () => {
99
+ const diff = '+services:\n' +
100
+ '+ web:\n' +
101
+ '+ ports:\n' +
102
+ '+ - "8080:80"\n' +
103
+ '-volumes:\n' +
104
+ '- data:\n';
105
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('docker-compose.yml', diff);
106
+ expect(result).not.toBeNull();
107
+ expect(result.changedElements).toContain('services');
108
+ expect(result.changedElements).toContain('volumes');
109
+ });
110
+ });
111
+ describe('unsupported files', () => {
112
+ it('should return null for unknown file types', () => {
113
+ const result = (0, fileContentAnalyzer_1.analyzeFileContent)('binary.exe', '');
114
+ expect(result).toBeNull();
115
+ });
116
+ });
117
+ });
118
+ //# sourceMappingURL=fileContentAnalyzer.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileContentAnalyzer.test.js","sourceRoot":"","sources":["../../src/services/fileContentAnalyzer.test.ts"],"names":[],"mappings":"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"}
@@ -35,6 +35,21 @@ describe('GitService', () => {
35
35
  expect(status.staged[1]).toEqual({ status: 'M', path: 'src/modified.ts' });
36
36
  expect(status.staged[2]).toEqual({ status: 'D', path: 'src/deleted.ts' });
37
37
  });
38
+ it('should parse unstaged files correctly', () => {
39
+ mockedExecSync.mockReturnValueOnce(' M src/unstaged.ts\n');
40
+ const status = gitService_1.GitService.getStatus();
41
+ expect(status.unstaged).toHaveLength(1);
42
+ expect(status.unstaged[0]).toEqual({ status: 'M', path: 'src/unstaged.ts' });
43
+ expect(status.staged).toHaveLength(0);
44
+ expect(status.hasStagedChanges).toBe(false);
45
+ });
46
+ it('should not include untracked files in staged or unstaged', () => {
47
+ mockedExecSync.mockReturnValueOnce('?? untracked.ts\n');
48
+ const status = gitService_1.GitService.getStatus();
49
+ expect(status.staged).toHaveLength(0);
50
+ expect(status.unstaged).toHaveLength(0);
51
+ expect(status.hasStagedChanges).toBe(false);
52
+ });
38
53
  it('should return empty arrays when no changes', () => {
39
54
  mockedExecSync.mockReturnValueOnce('');
40
55
  const status = gitService_1.GitService.getStatus();
@@ -42,11 +57,25 @@ describe('GitService', () => {
42
57
  expect(status.staged).toHaveLength(0);
43
58
  expect(status.unstaged).toHaveLength(0);
44
59
  });
60
+ it('should handle mixed staged and unstaged changes in the same file', () => {
61
+ mockedExecSync.mockReturnValueOnce('MM src/both.ts\n');
62
+ const status = gitService_1.GitService.getStatus();
63
+ expect(status.staged).toHaveLength(1);
64
+ expect(status.staged[0]).toEqual({ status: 'M', path: 'src/both.ts' });
65
+ // Source only adds to unstaged when stagedStatus === ' '
66
+ expect(status.unstaged).toHaveLength(0);
67
+ });
45
68
  it('should handle renamed files', () => {
46
69
  mockedExecSync.mockReturnValueOnce('R old.ts -> new.ts\n');
47
70
  const status = gitService_1.GitService.getStatus();
48
71
  expect(status.staged[0].status).toBe('R');
49
72
  });
73
+ it('should throw on git error', () => {
74
+ mockedExecSync.mockImplementationOnce(() => {
75
+ throw new Error('fatal: not a git repository');
76
+ });
77
+ expect(() => gitService_1.GitService.getStatus()).toThrow('Failed to get git status');
78
+ });
50
79
  });
51
80
  describe('getDiffStats', () => {
52
81
  it('should parse diff stats correctly', () => {
@@ -63,13 +92,26 @@ describe('GitService', () => {
63
92
  expect(stats.insertions).toBe(0);
64
93
  expect(stats.deletions).toBe(0);
65
94
  });
66
- it('should handle single file change', () => {
95
+ it('should handle single file with insertions only', () => {
67
96
  mockedExecSync.mockReturnValueOnce(' 1 file changed, 5 insertions(+)\n');
68
97
  const stats = gitService_1.GitService.getDiffStats();
69
98
  expect(stats.filesChanged).toBe(1);
70
99
  expect(stats.insertions).toBe(5);
71
100
  expect(stats.deletions).toBe(0);
72
101
  });
102
+ it('should handle single file with deletions only', () => {
103
+ mockedExecSync.mockReturnValueOnce(' 1 file changed, 3 deletions(-)\n');
104
+ const stats = gitService_1.GitService.getDiffStats();
105
+ expect(stats.filesChanged).toBe(1);
106
+ expect(stats.insertions).toBe(0);
107
+ expect(stats.deletions).toBe(3);
108
+ });
109
+ it('should throw on git error', () => {
110
+ mockedExecSync.mockImplementationOnce(() => {
111
+ throw new Error('fatal: not a git repository');
112
+ });
113
+ expect(() => gitService_1.GitService.getDiffStats()).toThrow('Failed to get diff stats');
114
+ });
73
115
  });
74
116
  describe('getDiff', () => {
75
117
  it('should return diff content', () => {
@@ -80,9 +122,24 @@ describe('GitService', () => {
80
122
  const diff = gitService_1.GitService.getDiff();
81
123
  expect(diff).toBe(mockDiff);
82
124
  });
125
+ it('should throw on git error', () => {
126
+ mockedExecSync.mockImplementationOnce(() => {
127
+ throw new Error('fatal: not a git repository');
128
+ });
129
+ expect(() => gitService_1.GitService.getDiff()).toThrow('Failed to get diff');
130
+ });
131
+ });
132
+ describe('getStagedFiles', () => {
133
+ it('should return staged files from getStatus', () => {
134
+ mockedExecSync.mockReturnValueOnce('M src/modified.ts\nA src/new.ts\n');
135
+ const files = gitService_1.GitService.getStagedFiles();
136
+ expect(files).toHaveLength(2);
137
+ expect(files[0]).toEqual({ status: 'M', path: 'src/modified.ts' });
138
+ expect(files[1]).toEqual({ status: 'A', path: 'src/new.ts' });
139
+ });
83
140
  });
84
141
  describe('commit', () => {
85
- it('should execute git commit with message', () => {
142
+ it('should execute git commit with message via stdin', () => {
86
143
  mockedSpawnSync.mockReturnValueOnce({
87
144
  status: 0,
88
145
  stdout: '',
@@ -111,7 +168,7 @@ describe('GitService', () => {
111
168
  input: multiLineMessage,
112
169
  }));
113
170
  });
114
- it('should throw error when commit fails', () => {
171
+ it('should throw error with stderr when commit fails', () => {
115
172
  mockedSpawnSync.mockReturnValueOnce({
116
173
  status: 1,
117
174
  stdout: '',
@@ -122,6 +179,186 @@ describe('GitService', () => {
122
179
  });
123
180
  expect(() => gitService_1.GitService.commit('test')).toThrow('Failed to commit');
124
181
  });
182
+ it('should use stdout when stderr is empty on failure', () => {
183
+ mockedSpawnSync.mockReturnValueOnce({
184
+ status: 1,
185
+ stdout: 'stdout error message',
186
+ stderr: '',
187
+ pid: 123,
188
+ output: [],
189
+ signal: null,
190
+ });
191
+ expect(() => gitService_1.GitService.commit('test')).toThrow('Failed to commit: stdout error message');
192
+ });
193
+ });
194
+ describe('amendCommit', () => {
195
+ it('should execute git commit --amend with message via stdin', () => {
196
+ mockedSpawnSync.mockReturnValueOnce({
197
+ status: 0,
198
+ stdout: '',
199
+ stderr: '',
200
+ pid: 123,
201
+ output: [],
202
+ signal: null,
203
+ });
204
+ expect(() => gitService_1.GitService.amendCommit('fix: correct typo')).not.toThrow();
205
+ expect(mockedSpawnSync).toHaveBeenCalledWith('git', ['commit', '--amend', '-F', '-'], expect.objectContaining({
206
+ input: 'fix: correct typo',
207
+ }));
208
+ });
209
+ it('should throw error when amend fails', () => {
210
+ mockedSpawnSync.mockReturnValueOnce({
211
+ status: 1,
212
+ stdout: '',
213
+ stderr: 'cannot amend',
214
+ pid: 123,
215
+ output: [],
216
+ signal: null,
217
+ });
218
+ expect(() => gitService_1.GitService.amendCommit('test')).toThrow('Failed to amend commit');
219
+ });
220
+ });
221
+ describe('getLastCommitMessage', () => {
222
+ it('should return the last commit message trimmed', () => {
223
+ mockedExecSync.mockReturnValueOnce('feat: initial commit\n');
224
+ const msg = gitService_1.GitService.getLastCommitMessage();
225
+ expect(msg).toBe('feat: initial commit');
226
+ });
227
+ it('should return null when output is empty', () => {
228
+ mockedExecSync.mockReturnValueOnce(' \n');
229
+ const msg = gitService_1.GitService.getLastCommitMessage();
230
+ expect(msg).toBeNull();
231
+ });
232
+ it('should return null on error', () => {
233
+ mockedExecSync.mockImplementationOnce(() => {
234
+ throw new Error('no commits yet');
235
+ });
236
+ expect(gitService_1.GitService.getLastCommitMessage()).toBeNull();
237
+ });
238
+ });
239
+ describe('getLastCommitFiles', () => {
240
+ it('should parse tab-separated file change lines', () => {
241
+ mockedExecSync.mockReturnValueOnce('M\tsrc/index.ts\nA\tsrc/new.ts\nD\tsrc/old.ts\n');
242
+ const files = gitService_1.GitService.getLastCommitFiles();
243
+ expect(files).toHaveLength(3);
244
+ expect(files[0]).toEqual({ status: 'M', path: 'src/index.ts' });
245
+ expect(files[1]).toEqual({ status: 'A', path: 'src/new.ts' });
246
+ expect(files[2]).toEqual({ status: 'D', path: 'src/old.ts' });
247
+ });
248
+ it('should return empty array on error', () => {
249
+ mockedExecSync.mockImplementationOnce(() => {
250
+ throw new Error('no commits');
251
+ });
252
+ expect(gitService_1.GitService.getLastCommitFiles()).toEqual([]);
253
+ });
254
+ });
255
+ describe('getLastCommitDiff', () => {
256
+ it('should return diff string', () => {
257
+ const mockDiff = 'diff --git a/f.ts b/f.ts\n+new line\n';
258
+ mockedExecSync.mockReturnValueOnce(mockDiff);
259
+ expect(gitService_1.GitService.getLastCommitDiff()).toBe(mockDiff);
260
+ });
261
+ it('should return empty string on error', () => {
262
+ mockedExecSync.mockImplementationOnce(() => {
263
+ throw new Error('no commits');
264
+ });
265
+ expect(gitService_1.GitService.getLastCommitDiff()).toBe('');
266
+ });
267
+ });
268
+ describe('hasCommits', () => {
269
+ it('should return true when there are commits', () => {
270
+ mockedExecSync.mockReturnValueOnce(Buffer.from('abc123'));
271
+ expect(gitService_1.GitService.hasCommits()).toBe(true);
272
+ });
273
+ it('should return false when there are no commits', () => {
274
+ mockedExecSync.mockImplementationOnce(() => {
275
+ throw new Error('unknown revision HEAD');
276
+ });
277
+ expect(gitService_1.GitService.hasCommits()).toBe(false);
278
+ });
279
+ });
280
+ describe('getGitRoot', () => {
281
+ it('should return the trimmed root path', () => {
282
+ mockedExecSync.mockReturnValueOnce('/home/user/project\n');
283
+ expect(gitService_1.GitService.getGitRoot()).toBe('/home/user/project');
284
+ });
285
+ it('should throw on error', () => {
286
+ mockedExecSync.mockImplementationOnce(() => {
287
+ throw new Error('not a git repository');
288
+ });
289
+ expect(() => gitService_1.GitService.getGitRoot()).toThrow('Failed to get git root directory');
290
+ });
291
+ });
292
+ describe('getGitDir', () => {
293
+ it('should resolve relative .git path to absolute', () => {
294
+ mockedExecSync.mockReturnValueOnce('.git\n');
295
+ const result = gitService_1.GitService.getGitDir();
296
+ // Should resolve to an absolute path ending with .git
297
+ expect(result.endsWith('.git')).toBe(true);
298
+ expect(result.length).toBeGreaterThan(4); // more than just ".git"
299
+ });
300
+ it('should return absolute Unix paths as-is', () => {
301
+ mockedExecSync.mockReturnValueOnce('/home/user/project/.git\n');
302
+ const result = gitService_1.GitService.getGitDir();
303
+ expect(result).toBe('/home/user/project/.git');
304
+ });
305
+ it('should throw on error', () => {
306
+ mockedExecSync.mockImplementationOnce(() => {
307
+ throw new Error('not a git repository');
308
+ });
309
+ expect(() => gitService_1.GitService.getGitDir()).toThrow('Failed to get git directory');
310
+ });
311
+ });
312
+ describe('getCurrentBranch', () => {
313
+ it('should return the current branch name', () => {
314
+ mockedExecSync.mockReturnValueOnce('main\n');
315
+ expect(gitService_1.GitService.getCurrentBranch()).toBe('main');
316
+ });
317
+ it('should return null for detached HEAD', () => {
318
+ mockedExecSync.mockReturnValueOnce('HEAD\n');
319
+ expect(gitService_1.GitService.getCurrentBranch()).toBeNull();
320
+ });
321
+ it('should return null on error', () => {
322
+ mockedExecSync.mockImplementationOnce(() => {
323
+ throw new Error('not a git repository');
324
+ });
325
+ expect(gitService_1.GitService.getCurrentBranch()).toBeNull();
326
+ });
327
+ });
328
+ describe('getCommitHistory', () => {
329
+ it('should parse null-delimited commit entries', () => {
330
+ // Format: hash\x00subject\x00body\x00 per commit, separated by \x00\x00
331
+ const output = [
332
+ 'abc123\x00feat: add feature\x00Some body text\x00',
333
+ 'def456\x00fix: bug fix\x00\x00',
334
+ ].join('\x00');
335
+ mockedExecSync.mockReturnValueOnce(output);
336
+ const commits = gitService_1.GitService.getCommitHistory(10);
337
+ expect(commits).toHaveLength(2);
338
+ expect(commits[0]).toEqual({
339
+ hash: 'abc123',
340
+ subject: 'feat: add feature',
341
+ message: 'feat: add feature\n\nSome body text',
342
+ body: 'Some body text',
343
+ });
344
+ expect(commits[1]).toEqual({
345
+ hash: 'def456',
346
+ subject: 'fix: bug fix',
347
+ message: 'fix: bug fix',
348
+ body: undefined,
349
+ });
350
+ });
351
+ it('should return empty array on error', () => {
352
+ mockedExecSync.mockImplementationOnce(() => {
353
+ throw new Error('no commits');
354
+ });
355
+ expect(gitService_1.GitService.getCommitHistory()).toEqual([]);
356
+ });
357
+ it('should use default count of 50', () => {
358
+ mockedExecSync.mockReturnValueOnce('');
359
+ gitService_1.GitService.getCommitHistory();
360
+ expect(mockedExecSync).toHaveBeenCalledWith(expect.stringContaining('-50'), expect.any(Object));
361
+ });
125
362
  });
126
363
  describe('getFileAtHead', () => {
127
364
  it('should return file contents from HEAD', () => {
@@ -130,18 +367,12 @@ describe('GitService', () => {
130
367
  expect(result).toBe('export function foo() {}');
131
368
  expect(mockedExecSync).toHaveBeenCalledWith('git show HEAD:src/foo.ts', expect.objectContaining({ encoding: 'utf-8' }));
132
369
  });
133
- it('should return null for new files (not in HEAD)', () => {
370
+ it('should return null for files not in HEAD', () => {
134
371
  mockedExecSync.mockImplementationOnce(() => {
135
372
  throw new Error('path does not exist');
136
373
  });
137
374
  expect(gitService_1.GitService.getFileAtHead('src/new.ts')).toBeNull();
138
375
  });
139
- it('should return null when no commits exist', () => {
140
- mockedExecSync.mockImplementationOnce(() => {
141
- throw new Error('unknown revision');
142
- });
143
- expect(gitService_1.GitService.getFileAtHead('src/any.ts')).toBeNull();
144
- });
145
376
  });
146
377
  describe('getStagedFileContent', () => {
147
378
  it('should return staged file contents from index', () => {
@@ -150,25 +381,12 @@ describe('GitService', () => {
150
381
  expect(result).toBe('export const X = 1;');
151
382
  expect(mockedExecSync).toHaveBeenCalledWith('git show :src/x.ts', expect.objectContaining({ encoding: 'utf-8' }));
152
383
  });
153
- it('should return null for deleted files', () => {
384
+ it('should return null for deleted files in index', () => {
154
385
  mockedExecSync.mockImplementationOnce(() => {
155
386
  throw new Error('path does not exist in index');
156
387
  });
157
388
  expect(gitService_1.GitService.getStagedFileContent('src/deleted.ts')).toBeNull();
158
389
  });
159
390
  });
160
- describe('getGitDir', () => {
161
- it('should resolve relative .git path', () => {
162
- mockedExecSync.mockReturnValueOnce('.git\n');
163
- const result = gitService_1.GitService.getGitDir();
164
- // Should resolve to absolute path ending with .git
165
- expect(result.endsWith('.git')).toBe(true);
166
- });
167
- it('should handle absolute paths', () => {
168
- mockedExecSync.mockReturnValueOnce('/home/user/project/.git\n');
169
- const result = gitService_1.GitService.getGitDir();
170
- expect(result).toBe('/home/user/project/.git');
171
- });
172
- });
173
391
  });
174
392
  //# sourceMappingURL=gitService.test.js.map