abapgit-agent 1.7.1 → 1.8.0

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 (41) hide show
  1. package/.abapGitAgent.example +11 -0
  2. package/README.md +7 -7
  3. package/abap/.github/copilot-instructions.md +254 -0
  4. package/abap/CLAUDE.md +432 -0
  5. package/abap/guidelines/00_index.md +8 -0
  6. package/abap/guidelines/01_sql.md +8 -0
  7. package/abap/guidelines/02_exceptions.md +8 -0
  8. package/abap/guidelines/03_testing.md +8 -0
  9. package/abap/guidelines/04_cds.md +8 -0
  10. package/abap/guidelines/05_classes.md +8 -0
  11. package/abap/guidelines/06_objects.md +8 -0
  12. package/abap/guidelines/07_json.md +8 -0
  13. package/abap/guidelines/08_abapgit.md +8 -0
  14. package/abap/guidelines/09_unit_testable_code.md +8 -0
  15. package/bin/abapgit-agent +61 -2789
  16. package/package.json +25 -5
  17. package/src/agent.js +213 -20
  18. package/src/commands/create.js +102 -0
  19. package/src/commands/delete.js +72 -0
  20. package/src/commands/health.js +24 -0
  21. package/src/commands/help.js +111 -0
  22. package/src/commands/import.js +99 -0
  23. package/src/commands/init.js +321 -0
  24. package/src/commands/inspect.js +184 -0
  25. package/src/commands/list.js +143 -0
  26. package/src/commands/preview.js +277 -0
  27. package/src/commands/pull.js +278 -0
  28. package/src/commands/ref.js +96 -0
  29. package/src/commands/status.js +52 -0
  30. package/src/commands/syntax.js +290 -0
  31. package/src/commands/tree.js +209 -0
  32. package/src/commands/unit.js +133 -0
  33. package/src/commands/view.js +215 -0
  34. package/src/commands/where.js +138 -0
  35. package/src/config.js +11 -1
  36. package/src/utils/abap-http.js +347 -0
  37. package/src/{ref-search.js → utils/abap-reference.js} +119 -1
  38. package/src/utils/git-utils.js +58 -0
  39. package/src/utils/validators.js +72 -0
  40. package/src/utils/version-check.js +80 -0
  41. package/src/abap-client.js +0 -523
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Syntax command - Check syntax of ABAP source files directly (without pull/activation)
3
+ */
4
+
5
+ const pathModule = require('path');
6
+ const fs = require('fs');
7
+
8
+ module.exports = {
9
+ name: 'syntax',
10
+ description: 'Check syntax of ABAP source files without pull/activation',
11
+ requiresAbapConfig: true,
12
+ requiresVersionCheck: true,
13
+
14
+ async execute(args, context) {
15
+ const { loadConfig, AbapHttp } = context;
16
+
17
+ const filesArgIndex = args.indexOf('--files');
18
+ if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
19
+ console.error('Error: --files parameter required');
20
+ console.error('Usage: abapgit-agent syntax --files <file1>,<file2>,... [--cloud] [--json]');
21
+ console.error('');
22
+ console.error('Options:');
23
+ console.error(' --cloud Use ABAP Cloud syntax check (stricter)');
24
+ console.error(' --json Output raw JSON');
25
+ console.error('');
26
+ console.error('Examples:');
27
+ console.error(' abapgit-agent syntax --files src/zcl_my_class.clas.abap');
28
+ console.error(' abapgit-agent syntax --files src/zcl_my_class.clas.abap --cloud');
29
+ process.exit(1);
30
+ }
31
+
32
+ const syntaxFiles = args[filesArgIndex + 1].split(',').map(f => f.trim());
33
+ const cloudMode = args.includes('--cloud');
34
+ const jsonOutput = args.includes('--json');
35
+
36
+ if (!jsonOutput) {
37
+ console.log(`\n Syntax check for ${syntaxFiles.length} file(s)`);
38
+ if (cloudMode) {
39
+ console.log(' Mode: ABAP Cloud');
40
+ }
41
+ console.log('');
42
+ }
43
+
44
+ const config = loadConfig();
45
+ const http = new AbapHttp(config);
46
+ const csrfToken = await http.fetchCsrfToken();
47
+
48
+ // Build objects array from files
49
+ // Group class files together (main + locals)
50
+ const classFilesMap = new Map(); // className -> { main, locals_def, locals_imp }
51
+ const objects = [];
52
+
53
+ for (const file of syntaxFiles) {
54
+ const baseName = pathModule.basename(file);
55
+ let objType = 'PROG';
56
+ let objName = baseName.toUpperCase();
57
+ let fileKind = 'main'; // main, locals_def, locals_imp
58
+
59
+ // Parse file type from extension
60
+ if (baseName.includes('.clas.locals_def.')) {
61
+ objType = 'CLAS';
62
+ objName = baseName.split('.')[0].toUpperCase();
63
+ fileKind = 'locals_def';
64
+ } else if (baseName.includes('.clas.locals_imp.')) {
65
+ objType = 'CLAS';
66
+ objName = baseName.split('.')[0].toUpperCase();
67
+ fileKind = 'locals_imp';
68
+ } else if (baseName.includes('.clas.testclasses.')) {
69
+ objType = 'CLAS';
70
+ objName = baseName.split('.')[0].toUpperCase();
71
+ fileKind = 'locals_imp'; // Test classes are implementations
72
+ } else if (baseName.includes('.clas.')) {
73
+ objType = 'CLAS';
74
+ objName = baseName.split('.')[0].toUpperCase();
75
+ fileKind = 'main';
76
+ } else if (baseName.includes('.intf.')) {
77
+ objType = 'INTF';
78
+ objName = baseName.split('.')[0].toUpperCase();
79
+ } else if (baseName.includes('.prog.')) {
80
+ objType = 'PROG';
81
+ objName = baseName.split('.')[0].toUpperCase();
82
+ }
83
+
84
+ // Read source from file
85
+ const filePath = pathModule.resolve(file);
86
+ if (!fs.existsSync(filePath)) {
87
+ console.error(` Error: File not found: ${file}`);
88
+ continue;
89
+ }
90
+
91
+ const source = fs.readFileSync(filePath, 'utf8');
92
+
93
+ // For class files, group them together
94
+ if (objType === 'CLAS') {
95
+ if (!classFilesMap.has(objName)) {
96
+ classFilesMap.set(objName, { main: null, locals_def: null, locals_imp: null, testclasses: null });
97
+ }
98
+ const classFiles = classFilesMap.get(objName);
99
+ // For testclasses, store in testclasses field (not locals_imp)
100
+ if (fileKind === 'locals_imp' && baseName.includes('.testclasses.')) {
101
+ classFiles.testclasses = source;
102
+ } else {
103
+ classFiles[fileKind] = source;
104
+ }
105
+ } else {
106
+ objects.push({
107
+ type: objType,
108
+ name: objName,
109
+ source: source
110
+ });
111
+ }
112
+ }
113
+
114
+ // Add class objects with their local files
115
+ for (const [className, files] of classFilesMap) {
116
+ // Try to auto-detect companion files if only one type is provided
117
+ if (files.main && !files.locals_def && !files.locals_imp && !files.testclasses) {
118
+ // Main file provided - look for companion local files
119
+ const mainFile = syntaxFiles.find(f => {
120
+ const bn = pathModule.basename(f).toUpperCase();
121
+ return bn.startsWith(className) && bn.includes('.CLAS.ABAP') && !bn.includes('LOCALS') && !bn.includes('TESTCLASSES');
122
+ });
123
+ if (mainFile) {
124
+ const dir = pathModule.dirname(mainFile);
125
+ const defFile = pathModule.join(dir, `${className.toLowerCase()}.clas.locals_def.abap`);
126
+ const impFile = pathModule.join(dir, `${className.toLowerCase()}.clas.locals_imp.abap`);
127
+ const testFile = pathModule.join(dir, `${className.toLowerCase()}.clas.testclasses.abap`);
128
+ if (fs.existsSync(defFile)) {
129
+ files.locals_def = fs.readFileSync(defFile, 'utf8');
130
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(defFile)}`);
131
+ }
132
+ if (fs.existsSync(impFile)) {
133
+ files.locals_imp = fs.readFileSync(impFile, 'utf8');
134
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(impFile)}`);
135
+ }
136
+ if (fs.existsSync(testFile)) {
137
+ files.testclasses = fs.readFileSync(testFile, 'utf8');
138
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(testFile)}`);
139
+ }
140
+ }
141
+ } else if (!files.main && (files.locals_def || files.locals_imp || files.testclasses)) {
142
+ // Any local file provided - look for main class file and other companions
143
+ const localFile = syntaxFiles.find(f => {
144
+ const bn = pathModule.basename(f).toUpperCase();
145
+ return bn.startsWith(className) && (bn.includes('.LOCALS_') || bn.includes('.TESTCLASSES.'));
146
+ });
147
+ if (localFile) {
148
+ const dir = pathModule.dirname(localFile);
149
+ const mainFile = pathModule.join(dir, `${className.toLowerCase()}.clas.abap`);
150
+ const defFile = pathModule.join(dir, `${className.toLowerCase()}.clas.locals_def.abap`);
151
+ const impFile = pathModule.join(dir, `${className.toLowerCase()}.clas.locals_imp.abap`);
152
+ const testFile = pathModule.join(dir, `${className.toLowerCase()}.clas.testclasses.abap`);
153
+
154
+ if (fs.existsSync(mainFile)) {
155
+ files.main = fs.readFileSync(mainFile, 'utf8');
156
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(mainFile)}`);
157
+ }
158
+ // Also auto-detect other companion files
159
+ if (!files.locals_def && fs.existsSync(defFile)) {
160
+ files.locals_def = fs.readFileSync(defFile, 'utf8');
161
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(defFile)}`);
162
+ }
163
+ if (!files.locals_imp && fs.existsSync(impFile)) {
164
+ files.locals_imp = fs.readFileSync(impFile, 'utf8');
165
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(impFile)}`);
166
+ }
167
+ if (!files.testclasses && fs.existsSync(testFile)) {
168
+ files.testclasses = fs.readFileSync(testFile, 'utf8');
169
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(testFile)}`);
170
+ }
171
+ }
172
+ }
173
+
174
+ if (files.main) {
175
+ const obj = {
176
+ type: 'CLAS',
177
+ name: className,
178
+ source: files.main
179
+ };
180
+ if (files.locals_def) obj.locals_def = files.locals_def;
181
+ if (files.locals_imp) obj.locals_imp = files.locals_imp;
182
+ if (files.testclasses) obj.testclasses = files.testclasses;
183
+ objects.push(obj);
184
+ } else {
185
+ console.error(` Warning: No main class file for ${className}, skipping local files`);
186
+ }
187
+ }
188
+
189
+ if (objects.length === 0) {
190
+ console.error(' No valid files to check');
191
+ process.exit(1);
192
+ }
193
+
194
+ // Send request
195
+ const data = {
196
+ objects: objects,
197
+ uccheck: cloudMode ? '5' : 'X'
198
+ };
199
+
200
+ const result = await http.post('/sap/bc/z_abapgit_agent/syntax', data, { csrfToken });
201
+
202
+ // Handle response
203
+ const success = result.SUCCESS !== undefined ? result.SUCCESS : result.success;
204
+ const results = result.RESULTS || result.results || [];
205
+ const message = result.MESSAGE || result.message || '';
206
+
207
+ if (jsonOutput) {
208
+ console.log(JSON.stringify(result, null, 2));
209
+ } else {
210
+ // Display results for each object
211
+ for (const res of results) {
212
+ const objSuccess = res.SUCCESS !== undefined ? res.SUCCESS : res.success;
213
+ const objType = res.OBJECT_TYPE || res.object_type || 'UNKNOWN';
214
+ const objName = res.OBJECT_NAME || res.object_name || 'UNKNOWN';
215
+ const errorCount = res.ERROR_COUNT || res.error_count || 0;
216
+ const errors = res.ERRORS || res.errors || [];
217
+ const warnings = res.WARNINGS || res.warnings || [];
218
+ const objMessage = res.MESSAGE || res.message || '';
219
+
220
+ if (objSuccess) {
221
+ console.log(`✅ ${objType} ${objName} - Syntax check passed`);
222
+ if (warnings.length > 0) {
223
+ console.log(` (${warnings.length} warning(s))`);
224
+ }
225
+ } else {
226
+ console.log(`❌ ${objType} ${objName} - Syntax check failed (${errorCount} error(s))`);
227
+ console.log('');
228
+ console.log('Errors:');
229
+ console.log('─'.repeat(60));
230
+
231
+ for (const err of errors) {
232
+ const line = err.LINE || err.line || '?';
233
+ const column = err.COLUMN || err.column || '';
234
+ const text = err.TEXT || err.text || 'Unknown error';
235
+ const methodName = err.METHOD_NAME || err.method_name || '';
236
+ const include = err.INCLUDE || err.include || '';
237
+
238
+ // Display which file/include the error is in
239
+ if (include) {
240
+ const includeMap = {
241
+ 'main': { display: 'Main class', suffix: '.clas.abap' },
242
+ 'locals_def': { display: 'Local definitions', suffix: '.clas.locals_def.abap' },
243
+ 'locals_imp': { display: 'Local implementations', suffix: '.clas.locals_imp.abap' },
244
+ 'testclasses': { display: 'Test classes', suffix: '.clas.testclasses.abap' }
245
+ };
246
+ const includeInfo = includeMap[include] || { display: include, suffix: '' };
247
+
248
+ // Show both display name and filename
249
+ if (includeInfo.suffix) {
250
+ console.log(` In: ${includeInfo.display} (${objName.toLowerCase()}${includeInfo.suffix})`);
251
+ } else {
252
+ console.log(` In: ${includeInfo.display}`);
253
+ }
254
+ }
255
+ if (methodName) {
256
+ console.log(` Method: ${methodName}`);
257
+ }
258
+ if (column) {
259
+ console.log(` Line ${line}, Column ${column}:`);
260
+ } else {
261
+ console.log(` Line ${line}:`);
262
+ }
263
+ console.log(` ${text}`);
264
+ console.log('');
265
+ }
266
+ }
267
+
268
+ // Show warnings if any
269
+ if (warnings.length > 0) {
270
+ console.log('');
271
+ console.log('Warnings:');
272
+ console.log('─'.repeat(60));
273
+ for (const warn of warnings) {
274
+ const line = warn.LINE || warn.line || '?';
275
+ const text = warn.TEXT || warn.text || warn.MESSAGE || warn.message || '';
276
+ console.log(` Line ${line}: ${text}`);
277
+ }
278
+ }
279
+ console.log('');
280
+ }
281
+
282
+ // Overall summary
283
+ if (success) {
284
+ console.log(`✅ ${message}`);
285
+ } else {
286
+ console.log(`❌ ${message}`);
287
+ }
288
+ }
289
+ }
290
+ };
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Tree command - Display package hierarchy tree
3
+ */
4
+
5
+ /**
6
+ * Run tree command and return raw result
7
+ */
8
+ async function runTreeCommand(packageName, depth, includeTypes, csrfToken, http) {
9
+ const data = {
10
+ package: packageName,
11
+ depth: depth,
12
+ include_objects: includeTypes
13
+ };
14
+
15
+ return await http.post('/sap/bc/z_abapgit_agent/tree', data, { csrfToken });
16
+ }
17
+
18
+ /**
19
+ * Build tree display lines from flat nodes list
20
+ */
21
+ function buildTreeLinesFromNodes(nodes, prefix, isLast) {
22
+ const lines = [];
23
+
24
+ if (nodes.length === 0) {
25
+ return lines;
26
+ }
27
+
28
+ // First node is the root
29
+ const root = nodes[0];
30
+ const icon = '📦';
31
+ lines.push(`${prefix}${isLast ? '└─ ' : '├─ '} ${icon} ${root.PACKAGE || root.package}`);
32
+
33
+ // Get children (nodes with depth > 0, grouped by depth)
34
+ const children = nodes.filter(n => (n.DEPTH || n.depth) > 0);
35
+
36
+ // Group children by depth
37
+ const byDepth = {};
38
+ children.forEach(n => {
39
+ const d = n.DEPTH || n.depth;
40
+ if (!byDepth[d]) byDepth[d] = [];
41
+ byDepth[d].push(n);
42
+ });
43
+
44
+ // Process depth 1 children
45
+ const depth1 = byDepth[1] || [];
46
+ const newPrefix = prefix + (isLast ? ' ' : '│ ');
47
+
48
+ depth1.forEach((child, idx) => {
49
+ const childIsLast = idx === depth1.length - 1;
50
+ const childLines = buildChildLines(child, newPrefix, childIsLast, byDepth);
51
+ lines.push(...childLines);
52
+ });
53
+
54
+ return lines;
55
+ }
56
+
57
+ /**
58
+ * Build lines for child nodes recursively
59
+ */
60
+ function buildChildLines(node, prefix, isLast, byDepth) {
61
+ const lines = [];
62
+ const icon = '📦';
63
+ const pkg = node.PACKAGE || node.package;
64
+
65
+ lines.push(`${prefix}${isLast ? '└─ ' : '├─ '} ${icon} ${pkg}`);
66
+
67
+ // Get children of this node
68
+ const nodeDepth = node.DEPTH || node.depth;
69
+ const children = (byDepth[nodeDepth + 1] || []).filter(n => {
70
+ const parent = n.PARENT || n.parent;
71
+ return parent === pkg;
72
+ });
73
+
74
+ const newPrefix = prefix + (isLast ? ' ' : '│ ');
75
+
76
+ children.forEach((child, idx) => {
77
+ const childIsLast = idx === children.length - 1;
78
+ const childLines = buildChildLines(child, newPrefix, childIsLast, byDepth);
79
+ lines.push(...childLines);
80
+ });
81
+
82
+ return lines;
83
+ }
84
+
85
+ /**
86
+ * Display tree output
87
+ */
88
+ async function displayTreeOutput(packageName, depth, includeTypes, loadConfig, AbapHttp) {
89
+ const config = loadConfig();
90
+ const http = new AbapHttp(config);
91
+
92
+ const csrfToken = await http.fetchCsrfToken();
93
+
94
+ console.log(`\n Getting package tree for: ${packageName}`);
95
+
96
+ const result = await runTreeCommand(packageName, depth, includeTypes, csrfToken, http);
97
+
98
+ // Handle uppercase keys from ABAP
99
+ const success = result.SUCCESS || result.success;
100
+ const error = result.ERROR || result.error;
101
+
102
+ if (!success || error) {
103
+ console.error(`\n ❌ Error: ${error || 'Failed to get tree'}`);
104
+ return;
105
+ }
106
+
107
+ // Parse hierarchy structure (ABAP returns flat nodes with parent refs)
108
+ const nodes = result.NODES || result.nodes || [];
109
+ const rootPackage = result.PACKAGE || result.package || packageName;
110
+ const parentPackage = result.PARENT_PACKAGE || result.parent_package;
111
+ const totalPackages = result.TOTAL_PACKAGES || result.total_packages || 0;
112
+ const totalObjects = result.TOTAL_OBJECTS || result.total_objects || 0;
113
+ const objectTypes = result.OBJECTS || result.objects || [];
114
+
115
+ console.log(`\n Package Tree: ${rootPackage}`);
116
+
117
+ // Display parent info if available
118
+ if (parentPackage && parentPackage !== rootPackage) {
119
+ console.log(` ⬆️ Parent: ${parentPackage}`);
120
+ }
121
+
122
+ console.log('');
123
+
124
+ // Build and display tree from flat nodes list
125
+ const lines = buildTreeLinesFromNodes(nodes, '', true);
126
+ for (const line of lines) {
127
+ console.log(` ${line}`);
128
+ }
129
+
130
+ console.log('');
131
+ console.log(' Summary');
132
+ console.log(` PACKAGES: ${totalPackages}`);
133
+ console.log(` OBJECTS: ${totalObjects}`);
134
+
135
+ // Display object types if available
136
+ if (includeTypes && objectTypes.length > 0) {
137
+ const typeStr = objectTypes.map(t => `${t.OBJECT || t.object}=${t.COUNT || t.count}`).join(' ');
138
+ console.log(` TYPES: ${typeStr}`);
139
+ }
140
+ }
141
+
142
+ module.exports = {
143
+ name: 'tree',
144
+ description: 'Display package hierarchy tree from ABAP system',
145
+ requiresAbapConfig: true,
146
+ requiresVersionCheck: true,
147
+
148
+ async execute(args, context) {
149
+ const { loadConfig, AbapHttp } = context;
150
+
151
+ const packageArgIndex = args.indexOf('--package');
152
+ if (packageArgIndex === -1) {
153
+ console.error('Error: --package parameter required');
154
+ console.error('Usage: abapgit-agent tree --package <package> [--depth <n>] [--include-types] [--json]');
155
+ console.error('Example: abapgit-agent tree --package ZMY_PACKAGE');
156
+ process.exit(1);
157
+ }
158
+
159
+ // Check if package value is missing (happens when shell variable expands to empty)
160
+ if (packageArgIndex + 1 >= args.length) {
161
+ console.error('Error: --package parameter value is missing');
162
+ console.error('');
163
+ console.error('Tip: If you are using a shell variable, make sure to quote it:');
164
+ console.error(' abapgit-agent tree --package \'$ZMY_PACKAGE\'');
165
+ console.error(' or escape the $ character:');
166
+ console.error(' abapgit-agent tree --package \\$ZMY_PACKAGE');
167
+ console.error('');
168
+ console.error('Usage: abapgit-agent tree --package <package> [--depth <n>] [--include-types] [--json]');
169
+ console.error('Example: abapgit-agent tree --package ZMY_PACKAGE');
170
+ process.exit(1);
171
+ }
172
+
173
+ const packageName = args[packageArgIndex + 1];
174
+
175
+ // Check for empty/whitespace-only package name
176
+ if (!packageName || packageName.trim() === '') {
177
+ console.error('Error: --package parameter cannot be empty');
178
+ console.error('Usage: abapgit-agent tree --package <package> [--depth <n>] [--include-types] [--json]');
179
+ console.error('Example: abapgit-agent tree --package ZMY_PACKAGE');
180
+ process.exit(1);
181
+ }
182
+
183
+ // Optional depth parameter
184
+ const depthArgIndex = args.indexOf('--depth');
185
+ let depth = 3;
186
+ if (depthArgIndex !== -1 && depthArgIndex + 1 < args.length) {
187
+ depth = parseInt(args[depthArgIndex + 1], 10);
188
+ if (isNaN(depth) || depth < 1) {
189
+ console.error('Error: --depth must be a positive number');
190
+ process.exit(1);
191
+ }
192
+ }
193
+
194
+ // Optional include-types parameter (--include-objects is deprecated alias)
195
+ const includeTypes = args.includes('--include-types') || args.includes('--include-objects');
196
+
197
+ // Optional json parameter
198
+ const jsonOutput = args.includes('--json');
199
+
200
+ if (jsonOutput) {
201
+ const config = loadConfig();
202
+ const csrfToken = await fetchCsrfToken(config);
203
+ const result = await runTreeCommand(packageName, depth, includeTypes, csrfToken, request);
204
+ console.log(JSON.stringify(result, null, 2));
205
+ } else {
206
+ await displayTreeOutput(packageName, depth, includeTypes, loadConfig, AbapHttp);
207
+ }
208
+ }
209
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Unit command - Run AUnit tests for ABAP test class files
3
+ */
4
+
5
+ const pathModule = require('path');
6
+ const fs = require('fs');
7
+
8
+ /**
9
+ * Run unit test for a single file
10
+ */
11
+ async function runUnitTestForFile(sourceFile, csrfToken, config, coverage, http) {
12
+ console.log(` Running unit test for: ${sourceFile}`);
13
+
14
+ try {
15
+ // Read file content
16
+ const absolutePath = pathModule.isAbsolute(sourceFile)
17
+ ? sourceFile
18
+ : pathModule.join(process.cwd(), sourceFile);
19
+
20
+ if (!fs.existsSync(absolutePath)) {
21
+ console.error(` ❌ File not found: ${absolutePath}`);
22
+ return;
23
+ }
24
+
25
+ // Extract object type and name from file path
26
+ // e.g., "zcl_my_test.clas.abap" -> CLAS, ZCL_MY_TEST
27
+ const fileName = pathModule.basename(sourceFile).toUpperCase();
28
+ const parts = fileName.split('.');
29
+ if (parts.length < 3) {
30
+ console.error(` ❌ Invalid file format: ${sourceFile}`);
31
+ return;
32
+ }
33
+
34
+ // obj_name is first part (may contain path), obj_type is second part
35
+ const objType = parts[1] === 'CLASS' ? 'CLAS' : parts[1];
36
+ let objName = parts[0];
37
+
38
+ // Handle subdirectory paths
39
+ const lastSlash = objName.lastIndexOf('/');
40
+ if (lastSlash >= 0) {
41
+ objName = objName.substring(lastSlash + 1);
42
+ }
43
+
44
+ // Send files array to unit endpoint (ABAP expects string_table of file names)
45
+ const data = {
46
+ files: [sourceFile],
47
+ coverage: coverage
48
+ };
49
+
50
+ const result = await http.post('/sap/bc/z_abapgit_agent/unit', data, { csrfToken });
51
+
52
+ // Handle uppercase keys from ABAP
53
+ const success = result.SUCCESS || result.success;
54
+ const testCount = result.TEST_COUNT || result.test_count || 0;
55
+ const passedCount = result.PASSED_COUNT || result.passed_count || 0;
56
+ const failedCount = result.FAILED_COUNT || result.failed_count || 0;
57
+ const message = result.MESSAGE || result.message || '';
58
+ const errors = result.ERRORS || result.errors || [];
59
+
60
+ // Handle coverage data
61
+ const coverageStats = result.COVERAGE_STATS || result.coverage_stats;
62
+
63
+ if (testCount === 0) {
64
+ console.log(` ➖ ${objName} - No unit tests`);
65
+ } else if (success === 'X' || success === true) {
66
+ console.log(` ✅ ${objName} - All tests passed`);
67
+ } else {
68
+ console.log(` ❌ ${objName} - Tests failed`);
69
+ }
70
+
71
+ console.log(` Tests: ${testCount} | Passed: ${passedCount} | Failed: ${failedCount}`);
72
+
73
+ // Display coverage if available
74
+ if (coverage && coverageStats) {
75
+ const totalLines = coverageStats.TOTAL_LINES || coverageStats.total_lines || 0;
76
+ const coveredLines = coverageStats.COVERED_LINES || coverageStats.covered_lines || 0;
77
+ const coverageRate = coverageStats.COVERAGE_RATE || coverageStats.coverage_rate || 0;
78
+
79
+ console.log(` 📊 Coverage: ${coverageRate}%`);
80
+ console.log(` Total Lines: ${totalLines}`);
81
+ console.log(` Covered Lines: ${coveredLines}`);
82
+ }
83
+
84
+ if (failedCount > 0 && errors.length > 0) {
85
+ for (const err of errors) {
86
+ const className = err.CLASS_NAME || err.class_name || '?';
87
+ const methodName = err.METHOD_NAME || err.method_name || '?';
88
+ const errorText = err.ERROR_TEXT || err.error_text || 'Unknown error';
89
+ console.log(` ✗ ${className}=>${methodName}: ${errorText}`);
90
+ }
91
+ }
92
+
93
+ return result;
94
+ } catch (error) {
95
+ console.error(`\n Error: ${error.message}`);
96
+ }
97
+ }
98
+
99
+ module.exports = {
100
+ name: 'unit',
101
+ description: 'Run AUnit tests for ABAP test class files',
102
+ requiresAbapConfig: true,
103
+ requiresVersionCheck: true,
104
+
105
+ async execute(args, context) {
106
+ const { loadConfig, AbapHttp } = context;
107
+
108
+ const filesArgIndex = args.indexOf('--files');
109
+ if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
110
+ console.error('Error: --files parameter required');
111
+ console.error('Usage: abapgit-agent unit --files <file1>,<file2>,... [--coverage]');
112
+ console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.abap');
113
+ console.error('Example: abapgit-agent unit --files src/zcl_my_test.clas.abap --coverage');
114
+ process.exit(1);
115
+ }
116
+
117
+ const files = args[filesArgIndex + 1].split(',').map(f => f.trim());
118
+
119
+ // Check for coverage option
120
+ const coverage = args.includes('--coverage');
121
+
122
+ console.log(`\n Running unit tests for ${files.length} file(s)${coverage ? ' (with coverage)' : ''}`);
123
+ console.log('');
124
+
125
+ const config = loadConfig();
126
+ const http = new AbapHttp(config);
127
+ const csrfToken = await http.fetchCsrfToken();
128
+
129
+ for (const sourceFile of files) {
130
+ await runUnitTestForFile(sourceFile, csrfToken, config, coverage, http);
131
+ }
132
+ }
133
+ };