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.
- package/.abapGitAgent.example +11 -0
- package/README.md +7 -7
- package/abap/.github/copilot-instructions.md +254 -0
- package/abap/CLAUDE.md +432 -0
- package/abap/guidelines/00_index.md +8 -0
- package/abap/guidelines/01_sql.md +8 -0
- package/abap/guidelines/02_exceptions.md +8 -0
- package/abap/guidelines/03_testing.md +8 -0
- package/abap/guidelines/04_cds.md +8 -0
- package/abap/guidelines/05_classes.md +8 -0
- package/abap/guidelines/06_objects.md +8 -0
- package/abap/guidelines/07_json.md +8 -0
- package/abap/guidelines/08_abapgit.md +8 -0
- package/abap/guidelines/09_unit_testable_code.md +8 -0
- package/bin/abapgit-agent +61 -2789
- package/package.json +25 -5
- package/src/agent.js +213 -20
- package/src/commands/create.js +102 -0
- package/src/commands/delete.js +72 -0
- package/src/commands/health.js +24 -0
- package/src/commands/help.js +111 -0
- package/src/commands/import.js +99 -0
- package/src/commands/init.js +321 -0
- package/src/commands/inspect.js +184 -0
- package/src/commands/list.js +143 -0
- package/src/commands/preview.js +277 -0
- package/src/commands/pull.js +278 -0
- package/src/commands/ref.js +96 -0
- package/src/commands/status.js +52 -0
- package/src/commands/syntax.js +290 -0
- package/src/commands/tree.js +209 -0
- package/src/commands/unit.js +133 -0
- package/src/commands/view.js +215 -0
- package/src/commands/where.js +138 -0
- package/src/config.js +11 -1
- package/src/utils/abap-http.js +347 -0
- package/src/{ref-search.js → utils/abap-reference.js} +119 -1
- package/src/utils/git-utils.js +58 -0
- package/src/utils/validators.js +72 -0
- package/src/utils/version-check.js +80 -0
- 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
|
+
};
|