abapgit-agent 1.7.2 → 1.8.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 (40) hide show
  1. package/README.md +26 -8
  2. package/abap/CLAUDE.md +146 -26
  3. package/abap/guidelines/00_index.md +8 -0
  4. package/abap/guidelines/01_sql.md +28 -0
  5. package/abap/guidelines/02_exceptions.md +8 -0
  6. package/abap/guidelines/03_testing.md +8 -0
  7. package/abap/guidelines/04_cds.md +151 -36
  8. package/abap/guidelines/05_classes.md +8 -0
  9. package/abap/guidelines/06_objects.md +8 -0
  10. package/abap/guidelines/07_json.md +8 -0
  11. package/abap/guidelines/08_abapgit.md +52 -3
  12. package/abap/guidelines/09_unit_testable_code.md +8 -0
  13. package/abap/guidelines/10_common_errors.md +95 -0
  14. package/bin/abapgit-agent +61 -2852
  15. package/package.json +21 -5
  16. package/src/agent.js +205 -16
  17. package/src/commands/create.js +102 -0
  18. package/src/commands/delete.js +72 -0
  19. package/src/commands/health.js +24 -0
  20. package/src/commands/help.js +111 -0
  21. package/src/commands/import.js +99 -0
  22. package/src/commands/init.js +321 -0
  23. package/src/commands/inspect.js +184 -0
  24. package/src/commands/list.js +143 -0
  25. package/src/commands/preview.js +277 -0
  26. package/src/commands/pull.js +278 -0
  27. package/src/commands/ref.js +96 -0
  28. package/src/commands/status.js +52 -0
  29. package/src/commands/syntax.js +340 -0
  30. package/src/commands/tree.js +209 -0
  31. package/src/commands/unit.js +133 -0
  32. package/src/commands/view.js +215 -0
  33. package/src/commands/where.js +138 -0
  34. package/src/config.js +11 -1
  35. package/src/utils/abap-http.js +347 -0
  36. package/src/utils/git-utils.js +58 -0
  37. package/src/utils/validators.js +72 -0
  38. package/src/utils/version-check.js +80 -0
  39. package/src/abap-client.js +0 -526
  40. /package/src/{ref-search.js → utils/abap-reference.js} +0 -0
@@ -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
+ };
@@ -0,0 +1,215 @@
1
+ /**
2
+ * View command - View ABAP object definitions
3
+ */
4
+
5
+ module.exports = {
6
+ name: 'view',
7
+ description: 'View ABAP object definitions from ABAP system',
8
+ requiresAbapConfig: true,
9
+ requiresVersionCheck: true,
10
+
11
+ async execute(args, context) {
12
+ const { loadConfig, AbapHttp } = context;
13
+
14
+ const objectsArgIndex = args.indexOf('--objects');
15
+ if (objectsArgIndex === -1 || objectsArgIndex + 1 >= args.length) {
16
+ console.error('Error: --objects parameter required');
17
+ console.error('Usage: abapgit-agent view --objects <obj1>,<obj2>,... [--type <type>] [--json]');
18
+ console.error('Example: abapgit-agent view --objects ZCL_MY_CLASS');
19
+ console.error('Example: abapgit-agent view --objects ZCL_CLASS1,ZCL_CLASS2 --type CLAS');
20
+ process.exit(1);
21
+ }
22
+
23
+ const objects = args[objectsArgIndex + 1].split(',').map(o => o.trim().toUpperCase());
24
+ const typeArg = args.indexOf('--type');
25
+ const type = typeArg !== -1 ? args[typeArg + 1].toUpperCase() : null;
26
+ const jsonOutput = args.includes('--json');
27
+
28
+ console.log(`\n Viewing ${objects.length} object(s)`);
29
+
30
+ const config = loadConfig();
31
+ const http = new AbapHttp(config);
32
+ const csrfToken = await http.fetchCsrfToken();
33
+
34
+ const data = {
35
+ objects: objects
36
+ };
37
+
38
+ if (type) {
39
+ data.type = type;
40
+ }
41
+
42
+ const result = await http.post('/sap/bc/z_abapgit_agent/view', data, { csrfToken });
43
+
44
+ // Handle uppercase keys from ABAP
45
+ const success = result.SUCCESS || result.success;
46
+ const viewObjects = result.OBJECTS || result.objects || [];
47
+ const message = result.MESSAGE || result.message || '';
48
+ const error = result.ERROR || result.error;
49
+
50
+ if (!success || error) {
51
+ console.error(`\n Error: ${error || 'Failed to view objects'}`);
52
+ return;
53
+ }
54
+
55
+ if (jsonOutput) {
56
+ console.log(JSON.stringify(result, null, 2));
57
+ } else {
58
+ console.log(`\n ${message}`);
59
+ console.log('');
60
+
61
+ for (let i = 0; i < viewObjects.length; i++) {
62
+ const obj = viewObjects[i];
63
+ const objName = obj.NAME || obj.name || `Object ${i + 1}`;
64
+ const objType = obj.TYPE || obj.type || '';
65
+ const objTypeText = obj.TYPE_TEXT || obj.type_text || '';
66
+ const description = obj.DESCRIPTION || obj.description || '';
67
+ const methods = obj.METHODS || obj.methods || [];
68
+ const components = obj.COMPONENTS || obj.components || [];
69
+ const notFound = obj.NOT_FOUND || obj.not_found || false;
70
+
71
+ // Check if object was not found
72
+ if (notFound) {
73
+ console.log(` ❌ ${objName} (${objTypeText})`);
74
+ console.log(` Object not found: ${objName}`);
75
+ continue;
76
+ }
77
+
78
+ console.log(` 📖 ${objName} (${objTypeText})`);
79
+ if (description) {
80
+ console.log(` ${description}`);
81
+ }
82
+
83
+ // Display source code for classes, interfaces, CDS views, and programs/source includes
84
+ const source = obj.SOURCE || obj.source || '';
85
+ if (source && (objType === 'INTF' || objType === 'Interface' || objType === 'CLAS' || objType === 'Class' || objType === 'DDLS' || objType === 'CDS View' || objType === 'PROG' || objType === 'Program')) {
86
+ console.log('');
87
+ // Replace escaped newlines with actual newlines and display
88
+ const displaySource = source.replace(/\\n/g, '\n');
89
+ const lines = displaySource.split('\n');
90
+ for (const line of lines) {
91
+ console.log(` ${line}`);
92
+ }
93
+ }
94
+
95
+ if (methods.length > 0) {
96
+ console.log(` Methods: ${methods.length}`);
97
+ for (const method of methods.slice(0, 5)) {
98
+ const name = method.NAME || method.name || '';
99
+ const visibility = method.VISIBILITY || method.visibility || '';
100
+ console.log(` - ${visibility} ${name}`);
101
+ }
102
+ if (methods.length > 5) {
103
+ console.log(` ... and ${methods.length - 5} more`);
104
+ }
105
+ }
106
+
107
+ if (components.length > 0) {
108
+ // Check if this is a data element (DTEL) - show domain info in property format
109
+ if (objType === 'DTEL' || objType === 'Data Element') {
110
+ const propWidth = 18;
111
+ const valueWidth = 40;
112
+
113
+ // Build separator with corners
114
+ const sep = '┌' + '─'.repeat(propWidth + 2) + '┬' + '─'.repeat(valueWidth + 2) + '┐';
115
+ const mid = '├' + '─'.repeat(propWidth + 2) + '┼' + '─'.repeat(valueWidth + 2) + '┤';
116
+ const end = '└' + '─'.repeat(propWidth + 2) + '┴' + '─'.repeat(valueWidth + 2) + '┘';
117
+
118
+ // Helper to build row
119
+ const buildPropRow = (property, value) => {
120
+ return '│ ' + String(property || '').padEnd(propWidth) + ' │ ' +
121
+ String(value || '').substring(0, valueWidth).padEnd(valueWidth) + ' │';
122
+ };
123
+
124
+ console.log(` DATA ELEMENT ${objName}:`);
125
+ console.log(sep);
126
+ console.log(buildPropRow('Property', 'Value'));
127
+ console.log(mid);
128
+
129
+ // Collect properties from top-level fields and components
130
+ const domain = obj.DOMAIN || obj.domain || '';
131
+ const domainType = obj.DOMAIN_TYPE || obj.domain_type || '';
132
+ const domainLength = obj.DOMAIN_LENGTH || obj.domain_length || 0;
133
+ const domainDecimals = obj.DOMAIN_DECIMALS || obj.domain_decimals || 0;
134
+ const description = obj.DESCRIPTION || obj.description || '';
135
+
136
+ if (domainType) {
137
+ console.log(buildPropRow('Data Type', domainType));
138
+ }
139
+ if (domainLength) {
140
+ console.log(buildPropRow('Length', String(domainLength)));
141
+ }
142
+ if (domainDecimals) {
143
+ console.log(buildPropRow('Decimals', String(domainDecimals)));
144
+ }
145
+ if (description) {
146
+ console.log(buildPropRow('Description', description));
147
+ }
148
+ if (domain) {
149
+ console.log(buildPropRow('Domain', domain));
150
+ }
151
+
152
+ console.log(end);
153
+ } else if (objType === 'TTYP' || objType === 'Table Type') {
154
+ // Show TTYP details as simple text lines
155
+ console.log('');
156
+ for (const comp of components) {
157
+ const desc = comp.DESCRIPTION || comp.description || '';
158
+ if (desc) {
159
+ console.log(` ${desc}`);
160
+ }
161
+ }
162
+ } else {
163
+ // Build table display for TABL/STRU with Data Element and Description
164
+ const colWidths = {
165
+ field: 16, // Max field name length
166
+ key: 3,
167
+ type: 8,
168
+ length: 8,
169
+ dataelement: 30, // Max data element name length
170
+ description: 60, // Max field description length
171
+ };
172
+
173
+ // Helper to truncate with ellipsis if needed
174
+ const truncate = (str, maxLen) => {
175
+ const s = String(str || '');
176
+ if (s.length <= maxLen) return s;
177
+ return s.substring(0, maxLen - 1) + '…';
178
+ };
179
+
180
+ // Helper to build row
181
+ const buildRow = (field, key, type, length, dataelement, description) => {
182
+ return ' | ' + truncate(field, colWidths.field).padEnd(colWidths.field) + ' | ' + String(key || '').padEnd(colWidths.key) + ' | ' + truncate(type, colWidths.type).padEnd(colWidths.type) + ' | ' + String(length || '').padStart(colWidths.length) + ' | ' + truncate(dataelement, colWidths.dataelement).padEnd(colWidths.dataelement) + ' | ' + truncate(description, colWidths.description).padEnd(colWidths.description) + ' |';
183
+ };
184
+
185
+ // Build separator line (matches row structure with | at ends and + between columns)
186
+ const sep = ' |' + '-'.repeat(colWidths.field + 2) + '+' +
187
+ '-'.repeat(colWidths.key + 2) + '+' +
188
+ '-'.repeat(colWidths.type + 2) + '+' +
189
+ '-'.repeat(colWidths.length + 2) + '+' +
190
+ '-'.repeat(colWidths.dataelement + 2) + '+' +
191
+ '-'.repeat(colWidths.description + 2) + '|';
192
+
193
+ // Header
194
+ console.log(` TABLE ${objName}:`);
195
+ console.log(sep);
196
+ console.log(buildRow('Field', 'Key', 'Type', 'Length', 'Data Elem', 'Description'));
197
+ console.log(sep);
198
+
199
+ // Rows
200
+ for (const comp of components) {
201
+ const key = comp.KEY || comp.key || false ? 'X' : '';
202
+ const dataelement = comp.DATAELEMENT || comp.dataelement || '';
203
+ const description = comp.DESCRIPTION || comp.description || '';
204
+ console.log(buildRow(comp.FIELD || comp.field, key, comp.TYPE || comp.type, comp.LENGTH || comp.length, dataelement, description));
205
+ }
206
+
207
+ console.log(sep);
208
+ }
209
+ }
210
+
211
+ console.log('');
212
+ }
213
+ }
214
+ }
215
+ };
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Where command - Find where-used references for ABAP objects
3
+ */
4
+
5
+ module.exports = {
6
+ name: 'where',
7
+ description: 'Find where-used references for ABAP objects',
8
+ requiresAbapConfig: true,
9
+ requiresVersionCheck: true,
10
+
11
+ async execute(args, context) {
12
+ const { loadConfig, AbapHttp } = context;
13
+
14
+ const objectsArgIndex = args.indexOf('--objects');
15
+ if (objectsArgIndex === -1 || objectsArgIndex + 1 >= args.length) {
16
+ console.error('Error: --objects parameter required');
17
+ console.error('Usage: abapgit-agent where --objects <obj1>,<obj2>,... [--type <type>] [--limit <n>] [--offset <n>] [--json]');
18
+ console.error('Example: abapgit-agent where --objects ZCL_MY_CLASS');
19
+ console.error('Example: abapgit-agent where --objects ZIF_MY_INTERFACE');
20
+ console.error('Example: abapgit-agent where --objects CL_SUT_AUNIT_RUNNER --limit 20');
21
+ console.error('Example: abapgit-agent where --objects CL_SUT_AUNIT_RUNNER --offset 100');
22
+ process.exit(1);
23
+ }
24
+
25
+ const objects = args[objectsArgIndex + 1].split(',').map(o => o.trim().toUpperCase());
26
+ const typeArg = args.indexOf('--type');
27
+ const type = typeArg !== -1 ? args[typeArg + 1].toUpperCase() : null;
28
+ const limitArg = args.indexOf('--limit');
29
+ const limitRaw = limitArg !== -1 ? args[limitArg + 1] : null;
30
+ const limitParsed = parseInt(limitRaw, 10);
31
+ const limit = limitRaw && !limitRaw.startsWith('--') && !isNaN(limitParsed) ? Math.max(1, limitParsed) : 100;
32
+ const offsetArg = args.indexOf('--offset');
33
+ const offsetRaw = offsetArg !== -1 ? args[offsetArg + 1] : null;
34
+ const offsetParsed = parseInt(offsetRaw, 10);
35
+ const offset = offsetRaw && !offsetRaw.startsWith('--') && !isNaN(offsetParsed) ? Math.max(0, offsetParsed) : 0;
36
+ const jsonOutput = args.includes('--json');
37
+
38
+ console.log(`\n Where-used list for ${objects.length} object(s)`);
39
+
40
+ const config = loadConfig();
41
+ const http = new AbapHttp(config);
42
+ const csrfToken = await http.fetchCsrfToken();
43
+
44
+ const data = {
45
+ objects: objects,
46
+ limit: Math.min(Math.max(1, limit), 500),
47
+ offset: Math.max(0, offset)
48
+ };
49
+
50
+ if (type) {
51
+ data.type = type;
52
+ }
53
+
54
+ const result = await http.post('/sap/bc/z_abapgit_agent/where', data, { csrfToken });
55
+
56
+ // Handle uppercase keys from ABAP
57
+ const success = result.SUCCESS || result.success;
58
+ const whereObjects = result.OBJECTS || result.objects || [];
59
+ const message = result.MESSAGE || result.message || '';
60
+ const error = result.ERROR || result.error;
61
+ const pagination = result.PAGINATION || result.pagination || null;
62
+
63
+ if (!success || error) {
64
+ console.error(`\n Error: ${error || 'Failed to get where-used list'}`);
65
+ return;
66
+ }
67
+
68
+ if (jsonOutput) {
69
+ console.log(JSON.stringify(result, null, 2));
70
+ } else {
71
+ // Build pagination message
72
+ let paginationMsg = '';
73
+ const paginationTotal = pagination.TOTAL || pagination.total || 0;
74
+ const paginationHasMore = pagination.HAS_MORE || pagination.has_more || false;
75
+ if (pagination && paginationTotal > 0) {
76
+ // Handle case where offset exceeds total
77
+ if (offset >= paginationTotal) {
78
+ paginationMsg = ` (Offset ${offset} exceeds total ${paginationTotal})`;
79
+ } else {
80
+ const start = offset + 1;
81
+ const end = Math.min(offset + limit, paginationTotal);
82
+ paginationMsg = ` (Showing ${start}-${end} of ${paginationTotal})`;
83
+ if (paginationHasMore) {
84
+ paginationMsg += ` — Use --offset ${offset + limit} to see more`;
85
+ }
86
+ }
87
+ }
88
+
89
+ console.log(`\n ${message}${paginationMsg}`);
90
+ console.log('');
91
+
92
+ for (let i = 0; i < whereObjects.length; i++) {
93
+ const obj = whereObjects[i];
94
+ const objName = obj.NAME || obj.name || `Object ${i + 1}`;
95
+ const objType = obj.TYPE || obj.type || '';
96
+ const error = obj.ERROR || obj.error || '';
97
+ const references = obj.REFERENCES || obj.references || [];
98
+ const count = obj.COUNT || obj.count || 0;
99
+
100
+ // Handle object not found error
101
+ if (error) {
102
+ console.log(` ❌ ${objName} (${objType})`);
103
+ console.log(` ${error}`);
104
+ console.log('');
105
+ continue;
106
+ }
107
+
108
+ if (count === 0) {
109
+ console.log(` ❌ ${objName} (${objType})`);
110
+ console.log(` No references found`);
111
+ console.log('');
112
+ continue;
113
+ }
114
+
115
+ console.log(` 🔍 ${objName} (${objType})`);
116
+ console.log(` Found ${count} reference(s):`);
117
+ console.log('');
118
+
119
+ // Display references - one line format: include → method (type) or include (type)
120
+ for (let j = 0; j < references.length; j++) {
121
+ const ref = references[j];
122
+ const includeName = ref.INCLUDE_NAME || ref.include_name || '';
123
+ const includeType = ref.INCLUDE_TYPE || ref.include_type || '';
124
+ const methodName = ref.METHOD_NAME || ref.method_name || '';
125
+
126
+ let line;
127
+ if (methodName) {
128
+ line = ` ${j + 1}. ${includeName} → ${methodName} (${includeType})`;
129
+ } else {
130
+ line = ` ${j + 1}. ${includeName} (${includeType})`;
131
+ }
132
+ console.log(line);
133
+ }
134
+ console.log('');
135
+ }
136
+ }
137
+ }
138
+ };
package/src/config.js CHANGED
@@ -65,9 +65,19 @@ function getTransport() {
65
65
  return cfg.transport;
66
66
  }
67
67
 
68
+ /**
69
+ * Check if ABAP integration is configured for this repo
70
+ * @returns {boolean} True if .abapGitAgent file exists
71
+ */
72
+ function isAbapIntegrationEnabled() {
73
+ const repoConfigPath = path.join(process.cwd(), '.abapGitAgent');
74
+ return fs.existsSync(repoConfigPath);
75
+ }
76
+
68
77
  module.exports = {
69
78
  loadConfig,
70
79
  getAbapConfig,
71
80
  getAgentConfig,
72
- getTransport
81
+ getTransport,
82
+ isAbapIntegrationEnabled
73
83
  };