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,96 @@
1
+ /**
2
+ * Ref command - Search ABAP reference materials (cheat sheets and guidelines)
3
+ */
4
+
5
+ module.exports = {
6
+ name: 'ref',
7
+ description: 'Search ABAP reference materials',
8
+ requiresAbapConfig: false,
9
+ requiresVersionCheck: false,
10
+
11
+ async execute(args, context) {
12
+ const refSearch = require('../utils/abap-reference');
13
+
14
+ const topicIndex = args.indexOf('--topic');
15
+ const cloneIndex = args.indexOf('--clone');
16
+ const nameIndex = args.indexOf('--name');
17
+ const listTopics = args.includes('--list-topics') || args.includes('-l');
18
+ const listRepos = args.includes('--list-repos') || args.includes('-r');
19
+ const jsonOutput = args.includes('--json');
20
+
21
+ // Handle --clone option
22
+ if (cloneIndex !== -1 && cloneIndex + 1 < args.length) {
23
+ const repoUrl = args[cloneIndex + 1];
24
+ const name = nameIndex !== -1 && nameIndex + 1 < args.length ? args[nameIndex + 1] : null;
25
+ const result = refSearch.cloneRepository(repoUrl, name);
26
+ if (jsonOutput) {
27
+ console.log(JSON.stringify(result, null, 2));
28
+ } else {
29
+ refSearch.displayCloneResult(result);
30
+ }
31
+ return;
32
+ }
33
+
34
+ if (listRepos) {
35
+ const result = await refSearch.listRepositories();
36
+ if (jsonOutput) {
37
+ console.log(JSON.stringify(result, null, 2));
38
+ } else {
39
+ refSearch.displayRepositories(result);
40
+ }
41
+ return;
42
+ }
43
+
44
+ if (listTopics) {
45
+ const result = await refSearch.listTopics();
46
+ if (jsonOutput) {
47
+ console.log(JSON.stringify(result, null, 2));
48
+ } else {
49
+ refSearch.displayTopics(result);
50
+ }
51
+ return;
52
+ }
53
+
54
+ if (topicIndex !== -1 && topicIndex + 1 < args.length) {
55
+ const topic = args[topicIndex + 1];
56
+ const result = await refSearch.getTopic(topic);
57
+ if (jsonOutput) {
58
+ console.log(JSON.stringify(result, null, 2));
59
+ } else {
60
+ refSearch.displayTopic(result);
61
+ }
62
+ return;
63
+ }
64
+
65
+ // Pattern search (default)
66
+ const patternIndex = args.findIndex((arg, idx) => idx > 0 && !arg.startsWith('--'));
67
+ if (patternIndex === -1) {
68
+ console.error('Error: No pattern specified');
69
+ console.error('');
70
+ console.error('Usage:');
71
+ console.error(' abapgit-agent ref <pattern> Search for pattern');
72
+ console.error(' abapgit-agent ref --topic <name> View specific topic');
73
+ console.error(' abapgit-agent ref --list-topics List available topics');
74
+ console.error(' abapgit-agent ref --list-repos List reference repositories');
75
+ console.error(' abapgit-agent ref --clone <repo> Clone a repository');
76
+ console.error('');
77
+ console.error('Examples:');
78
+ console.error(' abapgit-agent ref "CORRESPONDING"');
79
+ console.error(' abapgit-agent ref --topic exceptions');
80
+ console.error(' abapgit-agent ref --list-topics');
81
+ console.error(' abapgit-agent ref --list-repos');
82
+ console.error(' abapgit-agent ref --clone SAP-samples/abap-cheat-sheets');
83
+ console.error(' abapgit-agent ref --clone https://github.com/abapGit/abapGit.git');
84
+ process.exit(1);
85
+ }
86
+
87
+ const pattern = args[patternIndex];
88
+ const result = await refSearch.searchPattern(pattern);
89
+
90
+ if (jsonOutput) {
91
+ console.log(JSON.stringify(result, null, 2));
92
+ } else {
93
+ refSearch.displaySearchResults(result);
94
+ }
95
+ }
96
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Status command - Check if ABAP integration is configured
3
+ */
4
+
5
+ const pathModule = require('path');
6
+
7
+ module.exports = {
8
+ name: 'status',
9
+ description: 'Check if ABAP integration is configured for this repo',
10
+ requiresAbapConfig: false, // We check manually in execute
11
+ requiresVersionCheck: false,
12
+
13
+ async execute(args, context) {
14
+ const { gitUtils, isAbapIntegrationEnabled, AbapHttp, loadConfig } = context;
15
+
16
+ if (isAbapIntegrationEnabled()) {
17
+ console.log('✅ ABAP Git Agent is ENABLED');
18
+ console.log(' Config location:', pathModule.join(process.cwd(), '.abapGitAgent'));
19
+
20
+ // Check if repo exists in ABAP
21
+ const config = loadConfig();
22
+ const repoUrl = gitUtils.getRemoteUrl();
23
+
24
+ if (repoUrl) {
25
+ try {
26
+ const http = new AbapHttp(config);
27
+ const csrfToken = await http.fetchCsrfToken();
28
+ const result = await http.post('/sap/bc/z_abapgit_agent/status', { url: repoUrl }, { csrfToken });
29
+
30
+ const status = result.status || result.STATUS || result.SUCCESS;
31
+ if (status === 'Found' || status === 'X' || status === true) {
32
+ console.log(' Repository: ✅ Created');
33
+ const pkg = result.package || result.PACKAGE || 'N/A';
34
+ const key = result.key || result.KEY || result.REPO_KEY || result.repo_key || 'N/A';
35
+ console.log(` Package: ${pkg}`);
36
+ console.log(` URL: ${repoUrl}`);
37
+ console.log(` Key: ${key}`);
38
+ } else {
39
+ console.log(' Repository: ❌ Not created');
40
+ console.log(' Run "abapgit-agent create" to create it');
41
+ }
42
+ } catch (error) {
43
+ console.log(' Repository: ❓ Unknown (could not check)');
44
+ console.log(` Error: ${error.message}`);
45
+ }
46
+ }
47
+ } else {
48
+ console.log('❌ ABAP Git Agent is NOT configured');
49
+ console.log(' Run "abapgit-agent init" to set up configuration');
50
+ }
51
+ }
52
+ };
@@ -0,0 +1,340 @@
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
+ } else if (baseName.includes('.ddls.asddls')) {
83
+ objType = 'DDLS';
84
+ objName = baseName.split('.')[0].toUpperCase();
85
+ fileKind = 'main';
86
+ }
87
+
88
+ // Read source from file
89
+ const filePath = pathModule.resolve(file);
90
+ if (!fs.existsSync(filePath)) {
91
+ console.error(` Error: File not found: ${file}`);
92
+ continue;
93
+ }
94
+
95
+ const source = fs.readFileSync(filePath, 'utf8');
96
+
97
+ // For class files, group them together
98
+ if (objType === 'CLAS') {
99
+ if (!classFilesMap.has(objName)) {
100
+ classFilesMap.set(objName, { main: null, locals_def: null, locals_imp: null, testclasses: null });
101
+ }
102
+ const classFiles = classFilesMap.get(objName);
103
+ // For testclasses, store in testclasses field (not locals_imp)
104
+ if (fileKind === 'locals_imp' && baseName.includes('.testclasses.')) {
105
+ classFiles.testclasses = source;
106
+ } else {
107
+ classFiles[fileKind] = source;
108
+ }
109
+ } else {
110
+ const obj = {
111
+ type: objType,
112
+ name: objName,
113
+ source: source
114
+ };
115
+
116
+ // Read FIXPT from XML metadata for INTF and PROG
117
+ if (objType === 'INTF' || objType === 'PROG') {
118
+ const dir = pathModule.dirname(filePath);
119
+ let xmlFile;
120
+ if (objType === 'INTF') {
121
+ xmlFile = pathModule.join(dir, `${objName.toLowerCase()}.intf.xml`);
122
+ } else if (objType === 'PROG') {
123
+ xmlFile = pathModule.join(dir, `${objName.toLowerCase()}.prog.xml`);
124
+ }
125
+ if (xmlFile && fs.existsSync(xmlFile)) {
126
+ const xmlContent = fs.readFileSync(xmlFile, 'utf8');
127
+ // Simple regex to extract FIXPT value
128
+ const fixptMatch = xmlContent.match(/<FIXPT>([^<]+)<\/FIXPT>/);
129
+ if (fixptMatch && fixptMatch[1] === 'X') {
130
+ obj.fixpt = 'X';
131
+ } else {
132
+ // No FIXPT tag means FIXPT=false (blank)
133
+ obj.fixpt = '';
134
+ }
135
+ }
136
+ }
137
+
138
+ objects.push(obj);
139
+ }
140
+ }
141
+
142
+ // Add class objects with their local files
143
+ for (const [className, files] of classFilesMap) {
144
+ // Try to auto-detect companion files if only one type is provided
145
+ if (files.main && !files.locals_def && !files.locals_imp && !files.testclasses) {
146
+ // Main file provided - look for companion local files
147
+ const mainFile = syntaxFiles.find(f => {
148
+ const bn = pathModule.basename(f).toUpperCase();
149
+ return bn.startsWith(className) && bn.includes('.CLAS.ABAP') && !bn.includes('LOCALS') && !bn.includes('TESTCLASSES');
150
+ });
151
+ if (mainFile) {
152
+ const dir = pathModule.dirname(mainFile);
153
+ const defFile = pathModule.join(dir, `${className.toLowerCase()}.clas.locals_def.abap`);
154
+ const impFile = pathModule.join(dir, `${className.toLowerCase()}.clas.locals_imp.abap`);
155
+ const testFile = pathModule.join(dir, `${className.toLowerCase()}.clas.testclasses.abap`);
156
+ if (fs.existsSync(defFile)) {
157
+ files.locals_def = fs.readFileSync(defFile, 'utf8');
158
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(defFile)}`);
159
+ }
160
+ if (fs.existsSync(impFile)) {
161
+ files.locals_imp = fs.readFileSync(impFile, 'utf8');
162
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(impFile)}`);
163
+ }
164
+ if (fs.existsSync(testFile)) {
165
+ files.testclasses = fs.readFileSync(testFile, 'utf8');
166
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(testFile)}`);
167
+ }
168
+ }
169
+ } else if (!files.main && (files.locals_def || files.locals_imp || files.testclasses)) {
170
+ // Any local file provided - look for main class file and other companions
171
+ const localFile = syntaxFiles.find(f => {
172
+ const bn = pathModule.basename(f).toUpperCase();
173
+ return bn.startsWith(className) && (bn.includes('.LOCALS_') || bn.includes('.TESTCLASSES.'));
174
+ });
175
+ if (localFile) {
176
+ const dir = pathModule.dirname(localFile);
177
+ const mainFile = pathModule.join(dir, `${className.toLowerCase()}.clas.abap`);
178
+ const defFile = pathModule.join(dir, `${className.toLowerCase()}.clas.locals_def.abap`);
179
+ const impFile = pathModule.join(dir, `${className.toLowerCase()}.clas.locals_imp.abap`);
180
+ const testFile = pathModule.join(dir, `${className.toLowerCase()}.clas.testclasses.abap`);
181
+
182
+ if (fs.existsSync(mainFile)) {
183
+ files.main = fs.readFileSync(mainFile, 'utf8');
184
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(mainFile)}`);
185
+ }
186
+ // Also auto-detect other companion files
187
+ if (!files.locals_def && fs.existsSync(defFile)) {
188
+ files.locals_def = fs.readFileSync(defFile, 'utf8');
189
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(defFile)}`);
190
+ }
191
+ if (!files.locals_imp && fs.existsSync(impFile)) {
192
+ files.locals_imp = fs.readFileSync(impFile, 'utf8');
193
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(impFile)}`);
194
+ }
195
+ if (!files.testclasses && fs.existsSync(testFile)) {
196
+ files.testclasses = fs.readFileSync(testFile, 'utf8');
197
+ if (!jsonOutput) console.log(` Auto-detected: ${pathModule.basename(testFile)}`);
198
+ }
199
+ }
200
+ }
201
+
202
+ if (files.main) {
203
+ const obj = {
204
+ type: 'CLAS',
205
+ name: className,
206
+ source: files.main
207
+ };
208
+ if (files.locals_def) obj.locals_def = files.locals_def;
209
+ if (files.locals_imp) obj.locals_imp = files.locals_imp;
210
+ if (files.testclasses) obj.testclasses = files.testclasses;
211
+
212
+ // Read FIXPT from XML metadata
213
+ const mainFile = syntaxFiles.find(f => {
214
+ const bn = pathModule.basename(f).toUpperCase();
215
+ return bn.startsWith(className) && bn.includes('.CLAS.ABAP') && !bn.includes('LOCALS') && !bn.includes('TESTCLASSES');
216
+ });
217
+ if (mainFile) {
218
+ const dir = pathModule.dirname(mainFile);
219
+ const xmlFile = pathModule.join(dir, `${className.toLowerCase()}.clas.xml`);
220
+ if (fs.existsSync(xmlFile)) {
221
+ const xmlContent = fs.readFileSync(xmlFile, 'utf8');
222
+ // Simple regex to extract FIXPT value
223
+ const fixptMatch = xmlContent.match(/<FIXPT>([^<]+)<\/FIXPT>/);
224
+ if (fixptMatch && fixptMatch[1] === 'X') {
225
+ obj.fixpt = 'X';
226
+ } else {
227
+ // No FIXPT tag means FIXPT=false (blank)
228
+ obj.fixpt = '';
229
+ }
230
+ }
231
+ }
232
+
233
+ objects.push(obj);
234
+ } else {
235
+ console.error(` Warning: No main class file for ${className}, skipping local files`);
236
+ }
237
+ }
238
+
239
+ if (objects.length === 0) {
240
+ console.error(' No valid files to check');
241
+ process.exit(1);
242
+ }
243
+
244
+ // Send request
245
+ const data = {
246
+ objects: objects,
247
+ uccheck: cloudMode ? '5' : 'X'
248
+ };
249
+
250
+ const result = await http.post('/sap/bc/z_abapgit_agent/syntax', data, { csrfToken });
251
+
252
+ // Handle response
253
+ const success = result.SUCCESS !== undefined ? result.SUCCESS : result.success;
254
+ const results = result.RESULTS || result.results || [];
255
+ const message = result.MESSAGE || result.message || '';
256
+
257
+ if (jsonOutput) {
258
+ console.log(JSON.stringify(result, null, 2));
259
+ } else {
260
+ // Display results for each object
261
+ for (const res of results) {
262
+ const objSuccess = res.SUCCESS !== undefined ? res.SUCCESS : res.success;
263
+ const objType = res.OBJECT_TYPE || res.object_type || 'UNKNOWN';
264
+ const objName = res.OBJECT_NAME || res.object_name || 'UNKNOWN';
265
+ const errorCount = res.ERROR_COUNT || res.error_count || 0;
266
+ const errors = res.ERRORS || res.errors || [];
267
+ const warnings = res.WARNINGS || res.warnings || [];
268
+ const objMessage = res.MESSAGE || res.message || '';
269
+
270
+ if (objSuccess) {
271
+ console.log(`✅ ${objType} ${objName} - Syntax check passed`);
272
+ if (warnings.length > 0) {
273
+ console.log(` (${warnings.length} warning(s))`);
274
+ }
275
+ } else {
276
+ console.log(`❌ ${objType} ${objName} - Syntax check failed (${errorCount} error(s))`);
277
+ console.log('');
278
+ console.log('Errors:');
279
+ console.log('─'.repeat(60));
280
+
281
+ for (const err of errors) {
282
+ const line = err.LINE || err.line || '?';
283
+ const column = err.COLUMN || err.column || '';
284
+ const text = err.TEXT || err.text || 'Unknown error';
285
+ const methodName = err.METHOD_NAME || err.method_name || '';
286
+ const include = err.INCLUDE || err.include || '';
287
+
288
+ // Display which file/include the error is in
289
+ if (include) {
290
+ const includeMap = {
291
+ 'main': { display: 'Main class', suffix: '.clas.abap' },
292
+ 'locals_def': { display: 'Local definitions', suffix: '.clas.locals_def.abap' },
293
+ 'locals_imp': { display: 'Local implementations', suffix: '.clas.locals_imp.abap' },
294
+ 'testclasses': { display: 'Test classes', suffix: '.clas.testclasses.abap' }
295
+ };
296
+ const includeInfo = includeMap[include] || { display: include, suffix: '' };
297
+
298
+ // Show both display name and filename
299
+ if (includeInfo.suffix) {
300
+ console.log(` In: ${includeInfo.display} (${objName.toLowerCase()}${includeInfo.suffix})`);
301
+ } else {
302
+ console.log(` In: ${includeInfo.display}`);
303
+ }
304
+ }
305
+ if (methodName) {
306
+ console.log(` Method: ${methodName}`);
307
+ }
308
+ if (column) {
309
+ console.log(` Line ${line}, Column ${column}:`);
310
+ } else {
311
+ console.log(` Line ${line}:`);
312
+ }
313
+ console.log(` ${text}`);
314
+ console.log('');
315
+ }
316
+ }
317
+
318
+ // Show warnings if any
319
+ if (warnings.length > 0) {
320
+ console.log('');
321
+ console.log('Warnings:');
322
+ console.log('─'.repeat(60));
323
+ for (const warn of warnings) {
324
+ const line = warn.LINE || warn.line || '?';
325
+ const text = warn.TEXT || warn.text || warn.MESSAGE || warn.message || '';
326
+ console.log(` Line ${line}: ${text}`);
327
+ }
328
+ }
329
+ console.log('');
330
+ }
331
+
332
+ // Overall summary
333
+ if (success) {
334
+ console.log(`✅ ${message}`);
335
+ } else {
336
+ console.log(`❌ ${message}`);
337
+ }
338
+ }
339
+ }
340
+ };
@@ -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
+ };