abapgit-agent 1.0.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 (53) hide show
  1. package/.abapGitAgent.example +11 -0
  2. package/API.md +271 -0
  3. package/CLAUDE.md +445 -0
  4. package/CLAUDE_MEM.md +88 -0
  5. package/ERROR_HANDLING.md +30 -0
  6. package/INSTALL.md +160 -0
  7. package/README.md +127 -0
  8. package/abap/CLAUDE.md +492 -0
  9. package/abap/package.devc.xml +10 -0
  10. package/abap/zcl_abgagt_agent.clas.abap +769 -0
  11. package/abap/zcl_abgagt_agent.clas.xml +15 -0
  12. package/abap/zcl_abgagt_cmd_factory.clas.abap +43 -0
  13. package/abap/zcl_abgagt_cmd_factory.clas.xml +15 -0
  14. package/abap/zcl_abgagt_command_inspect.clas.abap +192 -0
  15. package/abap/zcl_abgagt_command_inspect.clas.testclasses.abap +121 -0
  16. package/abap/zcl_abgagt_command_inspect.clas.xml +16 -0
  17. package/abap/zcl_abgagt_command_pull.clas.abap +80 -0
  18. package/abap/zcl_abgagt_command_pull.clas.testclasses.abap +87 -0
  19. package/abap/zcl_abgagt_command_pull.clas.xml +16 -0
  20. package/abap/zcl_abgagt_command_unit.clas.abap +297 -0
  21. package/abap/zcl_abgagt_command_unit.clas.xml +15 -0
  22. package/abap/zcl_abgagt_resource_health.clas.abap +25 -0
  23. package/abap/zcl_abgagt_resource_health.clas.xml +15 -0
  24. package/abap/zcl_abgagt_resource_inspect.clas.abap +62 -0
  25. package/abap/zcl_abgagt_resource_inspect.clas.xml +15 -0
  26. package/abap/zcl_abgagt_resource_pull.clas.abap +71 -0
  27. package/abap/zcl_abgagt_resource_pull.clas.xml +15 -0
  28. package/abap/zcl_abgagt_resource_unit.clas.abap +64 -0
  29. package/abap/zcl_abgagt_resource_unit.clas.xml +15 -0
  30. package/abap/zcl_abgagt_rest_handler.clas.abap +27 -0
  31. package/abap/zcl_abgagt_rest_handler.clas.xml +15 -0
  32. package/abap/zcl_abgagt_util.clas.abap +93 -0
  33. package/abap/zcl_abgagt_util.clas.testclasses.abap +84 -0
  34. package/abap/zcl_abgagt_util.clas.xml +16 -0
  35. package/abap/zif_abgagt_agent.intf.abap +134 -0
  36. package/abap/zif_abgagt_agent.intf.xml +15 -0
  37. package/abap/zif_abgagt_cmd_factory.intf.abap +7 -0
  38. package/abap/zif_abgagt_cmd_factory.intf.xml +15 -0
  39. package/abap/zif_abgagt_command.intf.abap +21 -0
  40. package/abap/zif_abgagt_command.intf.xml +15 -0
  41. package/abap/zif_abgagt_util.intf.abap +28 -0
  42. package/abap/zif_abgagt_util.intf.xml +15 -0
  43. package/bin/abapgit-agent +902 -0
  44. package/img/claude.png +0 -0
  45. package/package.json +31 -0
  46. package/scripts/claude-integration.js +351 -0
  47. package/scripts/test-integration.js +139 -0
  48. package/src/abap-client.js +314 -0
  49. package/src/agent.js +119 -0
  50. package/src/config.js +66 -0
  51. package/src/index.js +48 -0
  52. package/src/logger.js +39 -0
  53. package/src/server.js +116 -0
@@ -0,0 +1,902 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ABAP Git Agent - CLI Tool
4
+ *
5
+ * Usage:
6
+ * abapgit-agent pull [--branch <branch>]
7
+ * abapgit-agent pull --url <git-url> [--branch <branch>]
8
+ * abapgit-agent health
9
+ * abapgit-agent status
10
+ *
11
+ * Auto-detects git remote URL and branch from current directory.
12
+ */
13
+
14
+ const pathModule = require('path');
15
+ const fs = require('fs');
16
+
17
+ // Get terminal width for responsive table
18
+ const getTermWidth = () => process.stdout.columns || 80;
19
+ const TERM_WIDTH = getTermWidth();
20
+
21
+ const COOKIE_FILE = '.abapgit_agent_cookies.txt';
22
+
23
+ /**
24
+ * Load configuration from .abapGitAgent in current working directory
25
+ */
26
+ function loadConfig() {
27
+ const repoConfigPath = pathModule.join(process.cwd(), '.abapGitAgent');
28
+
29
+ if (fs.existsSync(repoConfigPath)) {
30
+ return JSON.parse(fs.readFileSync(repoConfigPath, 'utf8'));
31
+ }
32
+
33
+ // Fallback to environment variables
34
+ return {
35
+ host: process.env.ABAP_HOST,
36
+ sapport: parseInt(process.env.ABAP_PORT, 10) || 443,
37
+ client: process.env.ABAP_CLIENT || '100',
38
+ user: process.env.ABAP_USER,
39
+ password: process.env.ABAP_PASSWORD,
40
+ language: process.env.ABAP_LANGUAGE || 'EN',
41
+ gitUsername: process.env.GIT_USERNAME,
42
+ gitPassword: process.env.GIT_PASSWORD
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Check if ABAP integration is configured for this repo
48
+ */
49
+ function isAbapIntegrationEnabled() {
50
+ const repoConfigPath = pathModule.join(process.cwd(), '.abapGitAgent');
51
+ return fs.existsSync(repoConfigPath);
52
+ }
53
+
54
+ /**
55
+ * Get git remote URL from .git/config
56
+ */
57
+ function getGitRemoteUrl() {
58
+ const gitConfigPath = pathModule.join(process.cwd(), '.git', 'config');
59
+
60
+ if (!fs.existsSync(gitConfigPath)) {
61
+ return null;
62
+ }
63
+
64
+ const content = fs.readFileSync(gitConfigPath, 'utf8');
65
+ const remoteMatch = content.match(/\[remote "origin"\]/);
66
+
67
+ if (!remoteMatch) {
68
+ return null;
69
+ }
70
+
71
+ const urlMatch = content.match(/ url = (.+)/);
72
+ return urlMatch ? urlMatch[1].trim() : null;
73
+ }
74
+
75
+ /**
76
+ * Get current git branch
77
+ */
78
+ function getGitBranch() {
79
+ const headPath = pathModule.join(process.cwd(), '.git', 'HEAD');
80
+
81
+ if (!fs.existsSync(headPath)) {
82
+ return 'main';
83
+ }
84
+
85
+ const content = fs.readFileSync(headPath, 'utf8').trim();
86
+ const match = content.match(/ref: refs\/heads\/(.+)/);
87
+ return match ? match[1] : 'main';
88
+ }
89
+
90
+ /**
91
+ * Read cookies from file
92
+ * Supports both Netscape format and simple cookie string format
93
+ */
94
+ function readNetscapeCookies() {
95
+ const cookiePath = pathModule.join(process.cwd(), COOKIE_FILE);
96
+ if (!fs.existsSync(cookiePath)) return '';
97
+
98
+ const content = fs.readFileSync(cookiePath, 'utf8').trim();
99
+ if (!content) return '';
100
+
101
+ // Check if it's Netscape format (has tabs)
102
+ if (content.includes('\t')) {
103
+ const lines = content.split('\n');
104
+ const cookies = [];
105
+
106
+ for (const line of lines) {
107
+ const trimmed = line.trim();
108
+ if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#HttpOnly'))) continue;
109
+
110
+ const parts = trimmed.split('\t');
111
+ if (parts.length >= 7) {
112
+ cookies.push(`${parts[5]}=${parts[6]}`);
113
+ }
114
+ }
115
+
116
+ return cookies.join('; ');
117
+ }
118
+
119
+ // Simple format - just return as-is
120
+ return content;
121
+ }
122
+
123
+ /**
124
+ * Save cookies to Netscape format cookie file
125
+ */
126
+ function saveCookies(cookies, host) {
127
+ const cookiePath = pathModule.join(process.cwd(), COOKIE_FILE);
128
+
129
+ // Parse cookies and convert to Netscape format
130
+ const cookieList = cookies.split(';').map(c => c.trim()).filter(Boolean);
131
+ const lines = ['# Netscape HTTP Cookie File', '# https://curl.se/docs/http-cookies.html', '# This file was generated by libcurl! Edit at your own risk.'];
132
+
133
+ for (const cookie of cookieList) {
134
+ const [name, ...valueParts] = cookie.split('=');
135
+ const value = valueParts.join('=');
136
+
137
+ // HttpOnly cookies get #HttpOnly_ prefix
138
+ const domain = name.includes('SAP') || name.includes('MYSAP') || name.includes('SAPLOGON')
139
+ ? `#HttpOnly_${host}`
140
+ : host;
141
+
142
+ // Format: domain, flag, path, secure, expiration, name, value
143
+ lines.push(`${domain}\tFALSE\t/\tFALSE\t0\t${name.trim()}\t${value}`);
144
+ }
145
+
146
+ fs.writeFileSync(cookiePath, lines.join('\n'));
147
+ }
148
+
149
+ /**
150
+ * Fetch CSRF token using GET /pull with X-CSRF-Token: fetch
151
+ */
152
+ async function fetchCsrfToken(config) {
153
+ const https = require('https');
154
+ const url = new URL(`/sap/bc/z_abapgit_agent/health`, `https://${config.host}:${config.sapport}`);
155
+
156
+ return new Promise((resolve, reject) => {
157
+ // Clear stale cookies before fetching new token
158
+ const cookiePath = pathModule.join(process.cwd(), COOKIE_FILE);
159
+ if (fs.existsSync(cookiePath)) {
160
+ fs.unlinkSync(cookiePath);
161
+ }
162
+
163
+ const cookieHeader = readNetscapeCookies();
164
+
165
+ const options = {
166
+ hostname: url.hostname,
167
+ port: url.port,
168
+ path: url.pathname,
169
+ method: 'GET',
170
+ headers: {
171
+ 'Authorization': `Basic ${Buffer.from(`${config.user}:${config.password}`).toString('base64')}`,
172
+ 'sap-client': config.client,
173
+ 'sap-language': config.language || 'EN',
174
+ 'X-CSRF-Token': 'fetch',
175
+ 'Content-Type': 'application/json',
176
+ ...(cookieHeader && { 'Cookie': cookieHeader })
177
+ },
178
+ agent: new https.Agent({ rejectUnauthorized: false })
179
+ };
180
+
181
+ const req = https.request(options, (res) => {
182
+ const csrfToken = res.headers['x-csrf-token'];
183
+
184
+ // Save new cookies from response
185
+ const setCookie = res.headers['set-cookie'];
186
+ if (setCookie) {
187
+ const cookies = Array.isArray(setCookie)
188
+ ? setCookie.map(c => c.split(';')[0]).join('; ')
189
+ : setCookie.split(';')[0];
190
+ saveCookies(cookies, config.host);
191
+ }
192
+
193
+ let body = '';
194
+ res.on('data', chunk => body += chunk);
195
+ res.on('end', () => {
196
+ resolve(csrfToken);
197
+ });
198
+ });
199
+
200
+ req.on('error', reject);
201
+ req.end();
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Make HTTP request to ABAP REST endpoint
207
+ */
208
+ function request(method, urlPath, data = null, options = {}) {
209
+ return new Promise((resolve, reject) => {
210
+ const https = require('https');
211
+ const http = require('http');
212
+ const config = loadConfig();
213
+ const url = new URL(urlPath, `https://${config.host}:${config.sapport}`);
214
+
215
+ const headers = {
216
+ 'Content-Type': 'application/json',
217
+ 'sap-client': config.client,
218
+ 'sap-language': config.language || 'EN',
219
+ ...options.headers
220
+ };
221
+
222
+ // Add authorization
223
+ headers['Authorization'] = `Basic ${Buffer.from(`${config.user}:${config.password}`).toString('base64')}`;
224
+
225
+ // Add CSRF token for POST
226
+ if (method === 'POST' && options.csrfToken) {
227
+ headers['X-CSRF-Token'] = options.csrfToken;
228
+ }
229
+
230
+ // Add cookies if available
231
+ const cookieHeader = readNetscapeCookies();
232
+ if (cookieHeader) {
233
+ headers['Cookie'] = cookieHeader;
234
+ }
235
+
236
+ const reqOptions = {
237
+ hostname: url.hostname,
238
+ port: url.port,
239
+ path: url.pathname,
240
+ method,
241
+ headers,
242
+ agent: new https.Agent({ rejectUnauthorized: false })
243
+ };
244
+
245
+ const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (res) => {
246
+ let body = '';
247
+ res.on('data', chunk => body += chunk);
248
+ res.on('end', () => {
249
+ try {
250
+ // Handle unescaped newlines from ABAP - replace actual newlines with \n
251
+ const cleanedBody = body.replace(/\n/g, '\\n');
252
+ resolve(JSON.parse(cleanedBody));
253
+ } catch (e) {
254
+ // Fallback: try to extract JSON from response
255
+ const jsonMatch = body.match(/\{[\s\S]*\}/);
256
+ if (jsonMatch) {
257
+ try {
258
+ resolve(JSON.parse(jsonMatch[0].replace(/\n/g, '\\n')));
259
+ } catch (e2) {
260
+ resolve({ raw: body, error: e2.message });
261
+ }
262
+ } else {
263
+ resolve({ raw: body, error: e.message });
264
+ }
265
+ }
266
+ });
267
+ });
268
+
269
+ req.on('error', reject);
270
+
271
+ if (data) {
272
+ req.write(JSON.stringify(data));
273
+ }
274
+ req.end();
275
+ });
276
+ }
277
+
278
+ /**
279
+ * Inspect ABAP source file
280
+ * Reads file content and sends to /inspect
281
+ */
282
+ async function syntaxCheckSource(sourceFile, csrfToken, config) {
283
+ console.log(` Syntax check for file: ${sourceFile}`);
284
+
285
+ try {
286
+ // Read file content
287
+ const absolutePath = pathModule.isAbsolute(sourceFile)
288
+ ? sourceFile
289
+ : pathModule.join(process.cwd(), sourceFile);
290
+
291
+ if (!fs.existsSync(absolutePath)) {
292
+ console.error(` ❌ File not found: ${absolutePath}`);
293
+ return;
294
+ }
295
+
296
+ const fileContent = fs.readFileSync(absolutePath, 'utf8');
297
+
298
+ // Extract source name from file path (basename with extension)
299
+ // e.g., "zcl_my_class.clas.abap" -> "ZCL_MY_CLASS.CLASS.ABAP"
300
+ const sourceName = pathModule.basename(sourceFile).toUpperCase();
301
+
302
+ // Send files array to syntax-check endpoint
303
+ const data = {
304
+ files: [sourceName]
305
+ };
306
+
307
+ const result = await request('POST', '/sap/bc/z_abapgit_agent/inspect', data, { csrfToken: csrfToken });
308
+
309
+ // Handle uppercase keys from ABAP - handle both 'X'/true (success) and false
310
+ const success = result.SUCCESS !== undefined ? result.SUCCESS : (result.success !== undefined ? result.success : null);
311
+ const errorCount = result.ERROR_COUNT || result.error_count || 0;
312
+ const errors = result.ERRORS || result.errors || [];
313
+
314
+ console.log('\n');
315
+
316
+ if (errorCount > 0) {
317
+ console.log(`❌ Syntax check failed (${errorCount} error(s)):`);
318
+ console.log('\nErrors:');
319
+ console.log('─'.repeat(60));
320
+
321
+ for (const err of errors) {
322
+ const line = err.LINE || err.line || '?';
323
+ const column = err.COLUMN || err.column || '?';
324
+ const text = err.TEXT || err.text || 'Unknown error';
325
+
326
+ console.log(` Line ${line}, Column ${column}:`);
327
+ console.log(` ${text}`);
328
+ console.log('');
329
+ }
330
+ } else if (success === 'X' || success === true) {
331
+ console.log(`✅ ${pathModule.basename(sourceFile)} - Syntax check passed (0 errors)`);
332
+ } else if (success === false) {
333
+ console.log(`❌ ${pathModule.basename(sourceFile)} - Syntax check failed`);
334
+ } else {
335
+ console.log(`⚠️ Syntax check returned unexpected status: ${success}`);
336
+ }
337
+
338
+ return result;
339
+ } catch (error) {
340
+ console.error(`\n Error: ${error.message}`);
341
+ process.exit(1);
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Run unit tests for package or objects
347
+ */
348
+ async function runUnitTests(options) {
349
+ console.log('\n Running unit tests');
350
+
351
+ try {
352
+ const config = loadConfig();
353
+
354
+ // Fetch CSRF token first
355
+ const csrfToken = await fetchCsrfToken(config);
356
+
357
+ const data = {};
358
+
359
+ if (options.package) {
360
+ data.package = options.package;
361
+ }
362
+
363
+ if (options.objects && options.objects.length > 0) {
364
+ data.objects = options.objects;
365
+ }
366
+
367
+ const result = await request('POST', '/sap/bc/z_abapgit_agent/unit', data, { csrfToken });
368
+
369
+ // Display raw result for debugging
370
+ if (process.env.DEBUG) {
371
+ console.log('Raw result:', JSON.stringify(result, null, 2));
372
+ }
373
+
374
+ // Handle uppercase keys from ABAP
375
+ const success = result.SUCCESS || result.success;
376
+ const testCount = result.TEST_COUNT || result.test_count || 0;
377
+ const passedCount = result.PASSED_COUNT || result.passed_count || 0;
378
+ const failedCount = result.FAILED_COUNT || result.failed_count || 0;
379
+ const message = result.MESSAGE || result.message || '';
380
+ const errors = result.ERRORS || result.errors || [];
381
+
382
+ console.log('\n');
383
+
384
+ if (success === 'X' || success === true) {
385
+ console.log(`✅ All tests passed!`);
386
+ } else {
387
+ console.log(`❌ Some tests failed`);
388
+ }
389
+
390
+ console.log(` ${message}`);
391
+ console.log(` Tests: ${testCount} | Passed: ${passedCount} | Failed: ${failedCount}`);
392
+
393
+ if (failedCount > 0 && errors.length > 0) {
394
+ console.log('\nFailed Tests:');
395
+ console.log('─'.repeat(60));
396
+
397
+ for (const err of errors) {
398
+ const className = err.CLASS_NAME || err.class_name || '?';
399
+ const methodName = err.METHOD_NAME || err.method_name || '?';
400
+ const errorKind = err.ERROR_KIND || err.error_kind || '';
401
+ const errorText = err.ERROR_TEXT || err.error_text || 'Unknown error';
402
+
403
+ console.log(` ✗ ${className}=>${methodName}`);
404
+ if (errorKind) {
405
+ console.log(` Kind: ${errorKind}`);
406
+ }
407
+ console.log(` Error: ${errorText}`);
408
+ console.log('');
409
+ }
410
+ }
411
+
412
+ return result;
413
+ } catch (error) {
414
+ console.error(`\n Error: ${error.message}`);
415
+ process.exit(1);
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Run unit test for a single file
421
+ */
422
+ async function runUnitTestForFile(sourceFile, csrfToken, config) {
423
+ console.log(` Running unit test for: ${sourceFile}`);
424
+
425
+ try {
426
+ // Read file content
427
+ const absolutePath = pathModule.isAbsolute(sourceFile)
428
+ ? sourceFile
429
+ : pathModule.join(process.cwd(), sourceFile);
430
+
431
+ if (!fs.existsSync(absolutePath)) {
432
+ console.error(` ❌ File not found: ${absolutePath}`);
433
+ return;
434
+ }
435
+
436
+ // Extract object type and name from file path
437
+ // e.g., "zcl_my_test.clas.abap" -> CLAS, ZCL_MY_TEST
438
+ const fileName = pathModule.basename(sourceFile).toUpperCase();
439
+ const parts = fileName.split('.');
440
+ if (parts.length < 3) {
441
+ console.error(` ❌ Invalid file format: ${sourceFile}`);
442
+ return;
443
+ }
444
+
445
+ // obj_name is first part (may contain path), obj_type is second part
446
+ const objType = parts[1] === 'CLASS' ? 'CLAS' : parts[1];
447
+ let objName = parts[0];
448
+
449
+ // Handle subdirectory paths
450
+ const lastSlash = objName.lastIndexOf('/');
451
+ if (lastSlash >= 0) {
452
+ objName = objName.substring(lastSlash + 1);
453
+ }
454
+
455
+ // Send files array to unit endpoint (ABAP expects string_table of file names)
456
+ const data = {
457
+ files: [sourceFile]
458
+ };
459
+
460
+ const result = await request('POST', '/sap/bc/z_abapgit_agent/unit', data, { csrfToken });
461
+
462
+ // Handle uppercase keys from ABAP
463
+ const success = result.SUCCESS || result.success;
464
+ const testCount = result.TEST_COUNT || result.test_count || 0;
465
+ const passedCount = result.PASSED_COUNT || result.passed_count || 0;
466
+ const failedCount = result.FAILED_COUNT || result.failed_count || 0;
467
+ const message = result.MESSAGE || result.message || '';
468
+ const errors = result.ERRORS || result.errors || [];
469
+
470
+ if (testCount === 0) {
471
+ console.log(` ➖ ${objName} - No unit tests`);
472
+ } else if (success === 'X' || success === true) {
473
+ console.log(` ✅ ${objName} - All tests passed`);
474
+ } else {
475
+ console.log(` ❌ ${objName} - Tests failed`);
476
+ }
477
+
478
+ console.log(` Tests: ${testCount} | Passed: ${passedCount} | Failed: ${failedCount}`);
479
+
480
+ if (failedCount > 0 && errors.length > 0) {
481
+ for (const err of errors) {
482
+ const className = err.CLASS_NAME || err.class_name || '?';
483
+ const methodName = err.METHOD_NAME || err.method_name || '?';
484
+ const errorText = err.ERROR_TEXT || err.error_text || 'Unknown error';
485
+ console.log(` ✗ ${className}=>${methodName}: ${errorText}`);
486
+ }
487
+ }
488
+
489
+ return result;
490
+ } catch (error) {
491
+ console.error(`\n Error: ${error.message}`);
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Pull and activate repository
497
+ */
498
+ async function pull(gitUrl, branch = 'main', files = null, transportRequest = null) {
499
+ console.log(`\n🚀 Starting pull for: ${gitUrl}`);
500
+ console.log(` Branch: ${branch}`);
501
+ if (files && files.length > 0) {
502
+ console.log(` Files: ${files.join(', ')}`);
503
+ }
504
+ if (transportRequest) {
505
+ console.log(` Transport Request: ${transportRequest}`);
506
+ }
507
+
508
+ try {
509
+ const config = loadConfig();
510
+
511
+ // Fetch CSRF token first
512
+ const csrfToken = await fetchCsrfToken(config);
513
+
514
+ // Prepare request data with git credentials
515
+ const data = {
516
+ url: gitUrl,
517
+ branch: branch,
518
+ username: config.gitUsername,
519
+ password: config.gitPassword
520
+ };
521
+
522
+ // Add files array if specified
523
+ if (files && files.length > 0) {
524
+ data.files = files;
525
+ }
526
+
527
+ // Add transport request if specified
528
+ if (transportRequest) {
529
+ data.transport_request = transportRequest;
530
+ }
531
+
532
+ const result = await request('POST', '/sap/bc/z_abapgit_agent/pull', data, { csrfToken });
533
+
534
+ console.log('\n');
535
+
536
+ // Display raw result for debugging
537
+ if (process.env.DEBUG) {
538
+ console.log('Raw result:', JSON.stringify(result, null, 2));
539
+ }
540
+
541
+ // Handle uppercase keys from ABAP
542
+ const success = result.SUCCESS || result.success;
543
+ const jobId = result.JOB_ID || result.job_id;
544
+ const message = result.MESSAGE || result.message;
545
+ const errorDetail = result.ERROR_DETAIL || result.error_detail;
546
+ const transportRequestUsed = result.TRANSPORT_REQUEST || result.transport_request;
547
+ const activatedCount = result.ACTIVATED_COUNT || result.activated_count || 0;
548
+ const failedCount = result.FAILED_COUNT || result.failed_count || 0;
549
+ const logMessages = result.LOG_MESSAGES || result.log_messages || [];
550
+ const activatedObjects = result.ACTIVATED_OBJECTS || result.activated_objects || [];
551
+ const failedObjects = result.FAILED_OBJECTS || result.failed_objects || [];
552
+
553
+ // Icon mapping for message types
554
+ const getIcon = (type) => {
555
+ const icons = {
556
+ 'S': '✅', // Success
557
+ 'E': '❌', // Error
558
+ 'W': '⚠️', // Warning
559
+ 'A': '🛑' // Abort
560
+ };
561
+ return icons[type] || ' ';
562
+ };
563
+
564
+ if (success === 'X' || success === true) {
565
+ console.log(`✅ Pull completed successfully!`);
566
+ console.log(` Job ID: ${jobId || 'N/A'}`);
567
+ console.log(` Message: ${message || 'N/A'}`);
568
+ } else {
569
+ console.log(`❌ Pull completed with errors!`);
570
+ console.log(` Job ID: ${jobId || 'N/A'}`);
571
+ console.log(` Message: ${message || 'N/A'}`);
572
+ }
573
+
574
+ // Display error detail if available
575
+ if (errorDetail && errorDetail.trim()) {
576
+ console.log(`\n📋 Error Details:`);
577
+ console.log('─'.repeat(TERM_WIDTH));
578
+ // Handle escaped newlines from ABAP JSON
579
+ const formattedDetail = errorDetail.replace(/\\n/g, '\n');
580
+ console.log(formattedDetail);
581
+ }
582
+
583
+ // Display all messages as table (from log_messages)
584
+ if (logMessages && logMessages.length > 0) {
585
+ console.log(`\n📋 Pull Log (${logMessages.length} messages):`);
586
+
587
+ // Calculate column widths based on terminal width
588
+ const tableWidth = Math.min(TERM_WIDTH, 120);
589
+ const iconCol = 5; // Width for emoji column (emoji takes 2 visual cells)
590
+ const objCol = 28;
591
+ const msgCol = tableWidth - iconCol - objCol - 6; // Account for vertical lines (3 chars)
592
+
593
+ // Helper to get visible width (emoji takes 2 cells)
594
+ const getVisibleWidth = (str) => {
595
+ let width = 0;
596
+ for (const char of str) {
597
+ width += (char.charCodeAt(0) > 127) ? 2 : 1; // Emoji/wide chars = 2
598
+ }
599
+ return width;
600
+ };
601
+
602
+ // Helper to pad to visible width
603
+ const padToWidth = (str, width) => {
604
+ const currentWidth = getVisibleWidth(str);
605
+ return str + ' '.repeat(width - currentWidth);
606
+ };
607
+
608
+ const headerLine = '─'.repeat(iconCol) + '┼' + '─'.repeat(objCol) + '┼' + '─'.repeat(msgCol);
609
+ const headerText = padToWidth('Icon', iconCol) + '│' + padToWidth('Object', objCol) + '│' + 'Message'.substring(0, msgCol);
610
+ const borderLine = '─'.repeat(tableWidth);
611
+
612
+ console.log(borderLine);
613
+ console.log(headerText);
614
+ console.log(headerLine);
615
+
616
+ for (const msg of logMessages) {
617
+ const icon = getIcon(msg.TYPE);
618
+ const objType = msg.OBJ_TYPE || '';
619
+ const objName = msg.OBJ_NAME || '';
620
+ const obj = objType && objName ? `${objType} ${objName}` : '';
621
+ const text = msg.TEXT || '';
622
+
623
+ // Truncate long text
624
+ const displayText = text.length > msgCol ? text.substring(0, msgCol - 3) + '...' : text;
625
+
626
+ console.log(padToWidth(icon, iconCol) + '│' + padToWidth(obj.substring(0, objCol), objCol) + '│' + displayText);
627
+ }
628
+ }
629
+
630
+ // Extract objects with errors from log messages (type 'E' = Error)
631
+ const failedObjectsFromLog = [];
632
+ const objectsWithErrors = new Set();
633
+
634
+ for (const msg of logMessages) {
635
+ if (msg.TYPE === 'E' && msg.OBJ_TYPE && msg.OBJ_NAME) {
636
+ const objKey = `${msg.OBJ_TYPE} ${msg.OBJ_NAME}`;
637
+ objectsWithErrors.add(objKey);
638
+ failedObjectsFromLog.push({
639
+ OBJ_TYPE: msg.OBJ_TYPE,
640
+ OBJ_NAME: msg.OBJ_NAME,
641
+ TEXT: msg.TEXT || 'Activation failed',
642
+ EXCEPTION: msg.EXCEPTION || ''
643
+ });
644
+ }
645
+ }
646
+
647
+ // Filter activated objects - only include objects without errors
648
+ const filteredActivatedObjects = activatedObjects.filter(obj => {
649
+ const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
650
+ return objKey && !objectsWithErrors.has(objKey);
651
+ });
652
+
653
+ // Display unique activated objects (excluding objects with errors)
654
+ if (filteredActivatedObjects && filteredActivatedObjects.length > 0) {
655
+ console.log(`\n📦 Activated Objects (${filteredActivatedObjects.length}):`);
656
+ console.log('─'.repeat(TERM_WIDTH));
657
+ for (const obj of filteredActivatedObjects) {
658
+ console.log(`✅ ${obj.OBJ_TYPE} ${obj.OBJ_NAME}`);
659
+ }
660
+ }
661
+
662
+ // Display failed objects log (all error messages, duplicates allowed)
663
+ if (failedObjectsFromLog.length > 0) {
664
+ console.log(`\n❌ Failed Objects Log (${failedObjectsFromLog.length} entries):`);
665
+ console.log('─'.repeat(TERM_WIDTH));
666
+ for (const obj of failedObjectsFromLog) {
667
+ const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
668
+ let text = obj.TEXT || 'Activation failed';
669
+ // Include exception text if available
670
+ if (obj.EXCEPTION && obj.EXCEPTION.trim()) {
671
+ text = `${text}\nException: ${obj.EXCEPTION}`;
672
+ }
673
+ console.log(`❌ ${objKey}: ${text}`);
674
+ }
675
+ } else if (failedObjects && failedObjects.length > 0) {
676
+ // Fallback to API failed_objects if no errors in log
677
+ console.log(`\n❌ Failed Objects Log (${failedObjects.length}):`);
678
+ console.log('─'.repeat(TERM_WIDTH));
679
+ for (const obj of failedObjects) {
680
+ const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
681
+ let text = obj.TEXT || 'Activation failed';
682
+ // Include exception text if available
683
+ if (obj.EXCEPTION && obj.EXCEPTION.trim()) {
684
+ text = `${text}\nException: ${obj.EXCEPTION}`;
685
+ }
686
+ console.log(`❌ ${objKey}: ${text}`);
687
+ }
688
+ } else if (failedCount > 0) {
689
+ console.log(`\n❌ Failed Objects Log (${failedCount})`);
690
+ }
691
+
692
+ return result;
693
+ } catch (error) {
694
+ console.error(`\n❌ Error: ${error.message}`);
695
+ process.exit(1);
696
+ }
697
+ }
698
+
699
+ /**
700
+ * Check agent health
701
+ */
702
+ async function healthCheck() {
703
+ try {
704
+ const result = await request('GET', '/sap/bc/z_abapgit_agent/health');
705
+ console.log(JSON.stringify(result, null, 2));
706
+ return result;
707
+ } catch (error) {
708
+ console.error(`Health check failed: ${error.message}`);
709
+ process.exit(1);
710
+ }
711
+ }
712
+
713
+ /**
714
+ * Main CLI
715
+ */
716
+ async function main() {
717
+ const args = process.argv.slice(2);
718
+ const command = args[0];
719
+
720
+ // Check if ABAP integration is enabled for this repo
721
+ if (!isAbapIntegrationEnabled()) {
722
+ console.log(`
723
+ ⚠️ ABAP Git Agent not configured for this repository.
724
+
725
+ To enable integration:
726
+ 1. Create a .abapGitAgent file in the repo root with ABAP connection details:
727
+ {
728
+ "host": "your-sap-system.com",
729
+ "sapport": 443,
730
+ "client": "100",
731
+ "user": "TECH_USER",
732
+ "password": "your-password",
733
+ "language": "EN"
734
+ }
735
+
736
+ 2. Or set environment variables:
737
+ - ABAP_HOST, ABAP_PORT, ABAP_CLIENT, ABAP_USER, ABAP_PASSWORD
738
+ `);
739
+ if (command !== 'help' && command !== '--help' && command !== '-h') {
740
+ process.exit(1);
741
+ }
742
+ }
743
+
744
+ try {
745
+ switch (command) {
746
+ case 'pull':
747
+ const urlArgIndex = args.indexOf('--url');
748
+ const branchArgIndex = args.indexOf('--branch');
749
+ const filesArgIndex = args.indexOf('--files');
750
+ const transportArgIndex = args.indexOf('--transport');
751
+
752
+ // Auto-detect git remote URL if not provided
753
+ let gitUrl = urlArgIndex !== -1 ? args[urlArgIndex + 1] : null;
754
+ let branch = branchArgIndex !== -1 ? args[branchArgIndex + 1] : getGitBranch();
755
+ let files = null;
756
+ let transportRequest = null;
757
+
758
+ if (filesArgIndex !== -1 && filesArgIndex + 1 < args.length) {
759
+ files = args[filesArgIndex + 1].split(',').map(f => f.trim());
760
+ }
761
+
762
+ if (transportArgIndex !== -1 && transportArgIndex + 1 < args.length) {
763
+ transportRequest = args[transportArgIndex + 1];
764
+ }
765
+
766
+ if (!gitUrl) {
767
+ gitUrl = getGitRemoteUrl();
768
+ if (!gitUrl) {
769
+ console.error('Error: Not in a git repository or no remote configured.');
770
+ console.error('Either run from a git repo, or specify --url <git-url>');
771
+ process.exit(1);
772
+ }
773
+ console.log(`📌 Auto-detected git remote: ${gitUrl}`);
774
+ }
775
+
776
+ await pull(gitUrl, branch, files, transportRequest);
777
+ break;
778
+
779
+ case 'health':
780
+ await healthCheck();
781
+ break;
782
+
783
+ case 'status':
784
+ if (isAbapIntegrationEnabled()) {
785
+ console.log('✅ ABAP Git Agent is ENABLED');
786
+ console.log(' Config location:', pathModule.join(process.cwd(), '.abapGitAgent'));
787
+ } else {
788
+ console.log('❌ ABAP Git Agent is NOT configured');
789
+ }
790
+ break;
791
+
792
+ case 'inspect': {
793
+ // TODO: Implement full inspect feature with:
794
+ // - Syntax check (currently implemented via /inspect)
795
+ // - Code Inspector checks (SE51, SCI)
796
+ // - ATC checks (SATC)
797
+ // - Custom rule checks
798
+ // Add --check-type parameter to specify which check to run
799
+
800
+ const filesArgIndex = args.indexOf('--files');
801
+ if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
802
+ console.error('Error: --files parameter required');
803
+ console.error('Usage: abapgit-agent inspect --files <file1>,<file2>,...');
804
+ console.error('Example: abapgit-agent inspect --files zcl_my_class.clas.abap');
805
+ process.exit(1);
806
+ }
807
+
808
+ const filesSyntaxCheck = args[filesArgIndex + 1].split(',').map(f => f.trim());
809
+
810
+ console.log(`\n Inspect for ${filesSyntaxCheck.length} file(s)`);
811
+ console.log('');
812
+
813
+ const config = loadConfig();
814
+ const csrfToken = await fetchCsrfToken(config);
815
+
816
+ for (const sourceFile of filesSyntaxCheck) {
817
+ await syntaxCheckSource(sourceFile, csrfToken, config);
818
+ }
819
+ break;
820
+ }
821
+
822
+ case 'unit': {
823
+ const filesArgIndex = args.indexOf('--files');
824
+ if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
825
+ console.error('Error: --files parameter required');
826
+ console.error('Usage: abapgit-agent unit --files <file1>,<file2>,...');
827
+ console.error('Example: abapgit-agent unit --files zcl_my_test.clas.abap');
828
+ process.exit(1);
829
+ }
830
+
831
+ const files = args[filesArgIndex + 1].split(',').map(f => f.trim());
832
+
833
+ console.log(`\n Running unit tests for ${files.length} file(s)`);
834
+ console.log('');
835
+
836
+ const config = loadConfig();
837
+ const csrfToken = await fetchCsrfToken(config);
838
+
839
+ for (const sourceFile of files) {
840
+ await runUnitTestForFile(sourceFile, csrfToken, config);
841
+ }
842
+ break;
843
+ }
844
+
845
+ case 'help':
846
+ case '--help':
847
+ case '-h':
848
+ console.log(`
849
+ ABAP Git Agent
850
+
851
+ Usage:
852
+ abapgit-agent <command> [options]
853
+
854
+ Commands:
855
+ pull [--url <git-url>] [--branch <branch>] [--files <file1,file2,...>] [--transport <request>]
856
+ Pull and activate repository in ABAP system.
857
+ Auto-detects git remote and branch from current directory.
858
+ Use --files to pull only specific files.
859
+ Use --transport to specify a transport request.
860
+
861
+ inspect --files <file1>,<file2>,...
862
+ Inspect ABAP source file(s) for issues. Currently runs syntax check.
863
+
864
+ unit --files <file1>,<file2>,...
865
+ Run AUnit tests for ABAP test class files (.testclasses.abap)
866
+
867
+ unit --object <type> <name>
868
+ Run unit tests for a specific object.
869
+
870
+ health
871
+ Check if ABAP REST API is healthy
872
+
873
+ status
874
+ Check if ABAP integration is configured for this repo
875
+
876
+ Examples:
877
+ abapgit-agent pull # Auto-detect from git repo
878
+ abapgit-agent pull --branch develop # Use specific branch
879
+ abapgit-agent pull --files zcl_my_class.clas.abap # Pull specific file
880
+ abapgit-agent pull --transport DEVK900001 # Use specific transport request
881
+ abapgit-agent pull --files zcl_my_class.clas.abap --transport DEVK900001
882
+ abapgit-agent inspect --files zcl_my_class.clas.abap # Inspect syntax
883
+ abapgit-agent inspect --files zcl_my_class.clas.abap,zcl_other.clas.abap
884
+ abapgit-agent unit --files zcl_my_test.clas.testclasses.abap
885
+ abapgit-agent unit --files zcl_my_test.clas.testclasses.abap,zcl_other.clas.testclasses.abap
886
+ abapgit-agent health
887
+ abapgit-agent status
888
+ `);
889
+ break;
890
+
891
+ default:
892
+ console.error(`Unknown command: ${command}`);
893
+ console.error('Use: abapgit-agent help');
894
+ process.exit(1);
895
+ }
896
+ } catch (error) {
897
+ console.error(`Error: ${error.message}`);
898
+ process.exit(1);
899
+ }
900
+ }
901
+
902
+ main();