abapgit-agent 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.abapGitAgent.example +11 -0
  2. package/README.md +7 -7
  3. package/abap/.github/copilot-instructions.md +254 -0
  4. package/abap/CLAUDE.md +432 -0
  5. package/abap/guidelines/00_index.md +8 -0
  6. package/abap/guidelines/01_sql.md +8 -0
  7. package/abap/guidelines/02_exceptions.md +8 -0
  8. package/abap/guidelines/03_testing.md +8 -0
  9. package/abap/guidelines/04_cds.md +8 -0
  10. package/abap/guidelines/05_classes.md +8 -0
  11. package/abap/guidelines/06_objects.md +8 -0
  12. package/abap/guidelines/07_json.md +8 -0
  13. package/abap/guidelines/08_abapgit.md +8 -0
  14. package/abap/guidelines/09_unit_testable_code.md +8 -0
  15. package/bin/abapgit-agent +61 -2789
  16. package/package.json +25 -5
  17. package/src/agent.js +213 -20
  18. package/src/commands/create.js +102 -0
  19. package/src/commands/delete.js +72 -0
  20. package/src/commands/health.js +24 -0
  21. package/src/commands/help.js +111 -0
  22. package/src/commands/import.js +99 -0
  23. package/src/commands/init.js +321 -0
  24. package/src/commands/inspect.js +184 -0
  25. package/src/commands/list.js +143 -0
  26. package/src/commands/preview.js +277 -0
  27. package/src/commands/pull.js +278 -0
  28. package/src/commands/ref.js +96 -0
  29. package/src/commands/status.js +52 -0
  30. package/src/commands/syntax.js +290 -0
  31. package/src/commands/tree.js +209 -0
  32. package/src/commands/unit.js +133 -0
  33. package/src/commands/view.js +215 -0
  34. package/src/commands/where.js +138 -0
  35. package/src/config.js +11 -1
  36. package/src/utils/abap-http.js +347 -0
  37. package/src/{ref-search.js → utils/abap-reference.js} +119 -1
  38. package/src/utils/git-utils.js +58 -0
  39. package/src/utils/validators.js +72 -0
  40. package/src/utils/version-check.js +80 -0
  41. package/src/abap-client.js +0 -523
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Pull command - Pull and activate ABAP objects from git repository
3
+ */
4
+
5
+ module.exports = {
6
+ name: 'pull',
7
+ description: 'Pull and activate ABAP objects from git repository',
8
+ requiresAbapConfig: true,
9
+ requiresVersionCheck: true,
10
+
11
+ async execute(args, context) {
12
+ const { loadConfig, AbapHttp, gitUtils, getTransport } = context;
13
+
14
+ const urlArgIndex = args.indexOf('--url');
15
+ const branchArgIndex = args.indexOf('--branch');
16
+ const filesArgIndex = args.indexOf('--files');
17
+ const transportArgIndex = args.indexOf('--transport');
18
+
19
+ // Auto-detect git remote URL if not provided
20
+ let gitUrl = urlArgIndex !== -1 ? args[urlArgIndex + 1] : null;
21
+ let branch = branchArgIndex !== -1 ? args[branchArgIndex + 1] : gitUtils.getBranch();
22
+ let files = null;
23
+
24
+ // Transport: CLI arg takes priority, then config/environment, then null
25
+ let transportRequest = null;
26
+ if (transportArgIndex !== -1 && transportArgIndex + 1 < args.length) {
27
+ // Explicit --transport argument
28
+ transportRequest = args[transportArgIndex + 1];
29
+ } else {
30
+ // Fall back to config or environment variable
31
+ transportRequest = getTransport();
32
+ }
33
+
34
+ if (filesArgIndex !== -1 && filesArgIndex + 1 < args.length) {
35
+ files = args[filesArgIndex + 1].split(',').map(f => f.trim());
36
+ }
37
+
38
+ if (!gitUrl) {
39
+ gitUrl = gitUtils.getRemoteUrl();
40
+ if (!gitUrl) {
41
+ console.error('Error: Not in a git repository or no remote configured.');
42
+ console.error('Either run from a git repo, or specify --url <git-url>');
43
+ process.exit(1);
44
+ }
45
+ console.log(`šŸ“Œ Auto-detected git remote: ${gitUrl}`);
46
+ }
47
+
48
+ await this.pull(gitUrl, branch, files, transportRequest, loadConfig, AbapHttp);
49
+ },
50
+
51
+ async pull(gitUrl, branch = 'main', files = null, transportRequest = null, loadConfig, AbapHttp) {
52
+ const TERM_WIDTH = process.stdout.columns || 80;
53
+
54
+ console.log(`\nšŸš€ Starting pull for: ${gitUrl}`);
55
+ console.log(` Branch: ${branch}`);
56
+ if (files && files.length > 0) {
57
+ console.log(` Files: ${files.join(', ')}`);
58
+ }
59
+ if (transportRequest) {
60
+ console.log(` Transport Request: ${transportRequest}`);
61
+ }
62
+
63
+ try {
64
+ const config = loadConfig();
65
+
66
+ // Fetch CSRF token first
67
+ const http = new AbapHttp(config);
68
+ const csrfToken = await http.fetchCsrfToken();
69
+
70
+ // Prepare request data with git credentials
71
+ const data = {
72
+ url: gitUrl,
73
+ branch: branch,
74
+ username: config.gitUsername,
75
+ password: config.gitPassword
76
+ };
77
+
78
+ // Add files array if specified
79
+ if (files && files.length > 0) {
80
+ data.files = files;
81
+ }
82
+
83
+ // Add transport request if specified
84
+ if (transportRequest) {
85
+ data.transport_request = transportRequest;
86
+ }
87
+
88
+ const result = await http.post('/sap/bc/z_abapgit_agent/pull', data, { csrfToken });
89
+
90
+ console.log('\n');
91
+
92
+ // Display raw result for debugging
93
+ if (process.env.DEBUG) {
94
+ console.log('Raw result:', JSON.stringify(result, null, 2));
95
+ }
96
+
97
+ // Handle uppercase keys from ABAP
98
+ const success = result.SUCCESS || result.success;
99
+ const jobId = result.JOB_ID || result.job_id;
100
+ const message = result.MESSAGE || result.message;
101
+ const errorDetail = result.ERROR_DETAIL || result.error_detail;
102
+ const transportRequestUsed = result.TRANSPORT_REQUEST || result.transport_request;
103
+ const activatedCount = result.ACTIVATED_COUNT || result.activated_count || 0;
104
+ const failedCount = result.FAILED_COUNT || result.failed_count || 0;
105
+ const logMessages = result.LOG_MESSAGES || result.log_messages || [];
106
+ const activatedObjects = result.ACTIVATED_OBJECTS || result.activated_objects || [];
107
+ const failedObjects = result.FAILED_OBJECTS || result.failed_objects || [];
108
+
109
+ // Icon mapping for message types
110
+ const getIcon = (type) => {
111
+ const icons = {
112
+ 'S': 'āœ…', // Success
113
+ 'E': 'āŒ', // Error
114
+ 'W': 'āš ļø', // Warning
115
+ 'A': 'šŸ›‘' // Abort
116
+ };
117
+ return icons[type] || '';
118
+ };
119
+
120
+ // Calculate display width accounting for emoji (2 cells) vs ASCII (1 cell)
121
+ const calcWidth = (str) => {
122
+ if (!str) return 0;
123
+ let width = 0;
124
+ let i = 0;
125
+ while (i < str.length) {
126
+ const code = str.codePointAt(i);
127
+ if (!code) break;
128
+ // Variation selectors (FE00-FE0F) and ZWJ (200D) take 0 width
129
+ if (code >= 0xFE00 && code <= 0xFE0F) {
130
+ i += 1;
131
+ continue;
132
+ }
133
+ if (code === 0x200D) { // Zero width joiner
134
+ i += 1;
135
+ continue;
136
+ }
137
+ // Emoji and wide characters take 2 cells
138
+ if (code > 0xFFFF) {
139
+ width += 2;
140
+ i += 2; // Skip surrogate pair
141
+ } else if (code > 127) {
142
+ width += 2;
143
+ i += 1;
144
+ } else {
145
+ width += 1;
146
+ i += 1;
147
+ }
148
+ }
149
+ return width;
150
+ };
151
+
152
+ // Pad string to display width
153
+ const padToWidth = (str, width) => {
154
+ const s = str || '';
155
+ const currentWidth = calcWidth(s);
156
+ return s + ' '.repeat(Math.max(0, width - currentWidth));
157
+ };
158
+
159
+ if (success === 'X' || success === true) {
160
+ console.log(`āœ… Pull completed successfully!`);
161
+ console.log(` Job ID: ${jobId || 'N/A'}`);
162
+ console.log(` Message: ${message || 'N/A'}`);
163
+ } else {
164
+ console.log(`āŒ Pull completed with errors!`);
165
+ console.log(` Job ID: ${jobId || 'N/A'}`);
166
+ console.log(` Message: ${message || 'N/A'}`);
167
+ }
168
+
169
+ // Display error detail if available
170
+ if (errorDetail && errorDetail.trim()) {
171
+ console.log(`\nšŸ“‹ Error Details:`);
172
+ console.log('─'.repeat(TERM_WIDTH));
173
+ // Handle escaped newlines from ABAP JSON
174
+ const formattedDetail = errorDetail.replace(/\\n/g, '\n');
175
+ console.log(formattedDetail);
176
+ }
177
+
178
+ // Display all messages as table (from log_messages)
179
+ if (logMessages && logMessages.length > 0) {
180
+ console.log(`\nšŸ“‹ Pull Log (${logMessages.length} messages):`);
181
+
182
+ // Calculate column widths based on terminal width
183
+ const tableWidth = Math.min(TERM_WIDTH, 120);
184
+ const iconCol = 4; // Fixed width for icon column
185
+ const objCol = 28;
186
+ const msgCol = tableWidth - iconCol - objCol - 6; // Account for vertical lines (3 chars)
187
+
188
+ const headerLine = '─'.repeat(iconCol) + '┼' + '─'.repeat(objCol) + '┼' + '─'.repeat(msgCol);
189
+ const headerText = padToWidth('Icon', iconCol) + '│' + padToWidth('Object', objCol) + '│' + 'Message'.substring(0, msgCol);
190
+ const borderLine = '─'.repeat(tableWidth);
191
+
192
+ console.log(borderLine);
193
+ console.log(headerText);
194
+ console.log(headerLine);
195
+
196
+ for (const msg of logMessages) {
197
+ const icon = getIcon(msg.TYPE);
198
+ const objType = msg.OBJ_TYPE || '';
199
+ const objName = msg.OBJ_NAME || '';
200
+ const obj = objType && objName ? `${objType} ${objName}` : '';
201
+ const text = msg.TEXT || '';
202
+
203
+ // Truncate long text
204
+ const displayText = text.length > msgCol ? text.substring(0, msgCol - 3) + '...' : text;
205
+
206
+ console.log(padToWidth(icon, iconCol) + '│' + padToWidth(obj.substring(0, objCol), objCol) + '│' + displayText);
207
+ }
208
+ }
209
+
210
+ // Extract objects with errors from log messages (type 'E' = Error)
211
+ const failedObjectsFromLog = [];
212
+ const objectsWithErrors = new Set();
213
+
214
+ for (const msg of logMessages) {
215
+ if (msg.TYPE === 'E' && msg.OBJ_TYPE && msg.OBJ_NAME) {
216
+ const objKey = `${msg.OBJ_TYPE} ${msg.OBJ_NAME}`;
217
+ objectsWithErrors.add(objKey);
218
+ failedObjectsFromLog.push({
219
+ OBJ_TYPE: msg.OBJ_TYPE,
220
+ OBJ_NAME: msg.OBJ_NAME,
221
+ TEXT: msg.TEXT || 'Activation failed',
222
+ EXCEPTION: msg.EXCEPTION || ''
223
+ });
224
+ }
225
+ }
226
+
227
+ // Filter activated objects - only include objects without errors
228
+ const filteredActivatedObjects = activatedObjects.filter(obj => {
229
+ const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
230
+ return objKey && !objectsWithErrors.has(objKey);
231
+ });
232
+
233
+ // Display unique activated objects (excluding objects with errors)
234
+ if (filteredActivatedObjects && filteredActivatedObjects.length > 0) {
235
+ console.log(`\nšŸ“¦ Activated Objects (${filteredActivatedObjects.length}):`);
236
+ console.log('─'.repeat(TERM_WIDTH));
237
+ for (const obj of filteredActivatedObjects) {
238
+ console.log(`āœ… ${obj.OBJ_TYPE} ${obj.OBJ_NAME}`);
239
+ }
240
+ }
241
+
242
+ // Display failed objects log (all error messages, duplicates allowed)
243
+ if (failedObjectsFromLog.length > 0) {
244
+ console.log(`\nāŒ Failed Objects Log (${failedObjectsFromLog.length} entries):`);
245
+ console.log('─'.repeat(TERM_WIDTH));
246
+ for (const obj of failedObjectsFromLog) {
247
+ const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
248
+ let text = obj.TEXT || 'Activation failed';
249
+ // Include exception text if available
250
+ if (obj.EXCEPTION && obj.EXCEPTION.trim()) {
251
+ text = `${text}\nException: ${obj.EXCEPTION}`;
252
+ }
253
+ console.log(`āŒ ${objKey}: ${text}`);
254
+ }
255
+ } else if (failedObjects && failedObjects.length > 0) {
256
+ // Fallback to API failed_objects if no errors in log
257
+ console.log(`\nāŒ Failed Objects Log (${failedObjects.length}):`);
258
+ console.log('─'.repeat(TERM_WIDTH));
259
+ for (const obj of failedObjects) {
260
+ const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
261
+ let text = obj.TEXT || 'Activation failed';
262
+ // Include exception text if available
263
+ if (obj.EXCEPTION && obj.EXCEPTION.trim()) {
264
+ text = `${text}\nException: ${obj.EXCEPTION}`;
265
+ }
266
+ console.log(`āŒ ${objKey}: ${text}`);
267
+ }
268
+ } else if (failedCount > 0) {
269
+ console.log(`\nāŒ Failed Objects Log (${failedCount})`);
270
+ }
271
+
272
+ return result;
273
+ } catch (error) {
274
+ console.error(`\nāŒ Error: ${error.message}`);
275
+ process.exit(1);
276
+ }
277
+ }
278
+ };
@@ -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
+ };