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.
- package/README.md +26 -8
- package/abap/CLAUDE.md +146 -26
- package/abap/guidelines/00_index.md +8 -0
- package/abap/guidelines/01_sql.md +28 -0
- package/abap/guidelines/02_exceptions.md +8 -0
- package/abap/guidelines/03_testing.md +8 -0
- package/abap/guidelines/04_cds.md +151 -36
- 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 +52 -3
- package/abap/guidelines/09_unit_testable_code.md +8 -0
- package/abap/guidelines/10_common_errors.md +95 -0
- package/bin/abapgit-agent +61 -2852
- package/package.json +21 -5
- package/src/agent.js +205 -16
- 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 +340 -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/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 -526
- /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
|
+
};
|