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
package/bin/abapgit-agent CHANGED
@@ -19,1478 +19,48 @@
19
19
  const pathModule = require('path');
20
20
  const fs = require('fs');
21
21
 
22
- // Get terminal width for responsive table
23
- const getTermWidth = () => process.stdout.columns || 80;
24
- const TERM_WIDTH = getTermWidth();
25
-
26
- const COOKIE_FILE = '.abapgit_agent_cookies.txt';
27
-
28
- /**
29
- * Convert ISO date formats (YYYY-MM-DD) to ABAP DATS format (YYYYMMDD) in WHERE clause
30
- * This allows users to use familiar ISO date formats while ensuring compatibility with ABAP SQL
31
- * @param {string} whereClause - SQL WHERE clause
32
- * @returns {string} - WHERE clause with dates converted to YYYYMMDD format
33
- */
34
- function convertDatesInWhereClause(whereClause) {
35
- if (!whereClause) return whereClause;
36
-
37
- // Pattern to match ISO date format: 'YYYY-MM-DD'
38
- const isoDatePattern = /'\d{4}-\d{2}-\d{2}'/g;
39
-
40
- return whereClause.replace(isoDatePattern, (match) => {
41
- // Extract YYYY, MM, DD from 'YYYY-MM-DD'
42
- const dateContent = match.slice(1, -1); // Remove quotes: YYYY-MM-DD
43
- const [year, month, day] = dateContent.split('-');
44
- // Return in ABAP format: 'YYYYMMDD'
45
- return `'${year}${month}${day}'`;
46
- });
47
- }
48
-
49
- /**
50
- * Get CLI version from package.json
51
- */
52
- function getCliVersion() {
53
- const packageJsonPath = pathModule.join(__dirname, '..', 'package.json');
54
- if (fs.existsSync(packageJsonPath)) {
55
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
56
- return pkg.version || '1.0.0';
57
- }
58
- return '1.0.0';
59
- }
60
-
61
- /**
62
- * Check version compatibility between CLI and ABAP API
63
- */
64
- async function checkVersionCompatibility() {
65
- const cliVersion = getCliVersion();
66
-
67
- try {
68
- const config = loadConfig();
69
- const https = require('https');
70
- const url = new URL(`/sap/bc/z_abapgit_agent/health`, `https://${config.host}:${config.sapport}`);
71
-
72
- return new Promise((resolve) => {
73
- const options = {
74
- hostname: url.hostname,
75
- port: url.port,
76
- path: url.pathname,
77
- method: 'GET',
78
- headers: {
79
- 'Authorization': `Basic ${Buffer.from(`${config.user}:${config.password}`).toString('base64')}`,
80
- 'sap-client': config.client,
81
- 'sap-language': config.language || 'EN',
82
- 'Content-Type': 'application/json'
83
- },
84
- agent: new https.Agent({ rejectUnauthorized: false })
85
- };
86
-
87
- const req = https.request(options, (res) => {
88
- let body = '';
89
- res.on('data', chunk => body += chunk);
90
- res.on('end', () => {
91
- try {
92
- const result = JSON.parse(body);
93
- const apiVersion = result.version || '1.0.0';
94
-
95
- if (cliVersion !== apiVersion) {
96
- console.log(`\n⚠️ Version mismatch: CLI ${cliVersion}, ABAP API ${apiVersion}`);
97
- console.log(' Some commands may not work correctly.');
98
- console.log(' Update ABAP code: abapgit-agent pull\n');
99
- }
100
- resolve({ cliVersion, apiVersion, compatible: cliVersion === apiVersion });
101
- } catch (e) {
102
- resolve({ cliVersion, apiVersion: null, compatible: false, error: e.message });
103
- }
104
- });
105
- });
106
-
107
- req.on('error', (e) => {
108
- resolve({ cliVersion, apiVersion: null, compatible: false, error: e.message });
109
- });
110
- req.end();
111
- });
112
- } catch (error) {
113
- return { cliVersion, apiVersion: null, compatible: false, error: error.message };
114
- }
115
- }
116
-
117
- /**
118
- * Load configuration from .abapGitAgent in current working directory
119
- */
120
- function loadConfig() {
121
- const repoConfigPath = pathModule.join(process.cwd(), '.abapGitAgent');
122
-
123
- if (fs.existsSync(repoConfigPath)) {
124
- return JSON.parse(fs.readFileSync(repoConfigPath, 'utf8'));
125
- }
126
-
127
- // Fallback to environment variables
128
- return {
129
- host: process.env.ABAP_HOST,
130
- sapport: parseInt(process.env.ABAP_PORT, 10) || 443,
131
- client: process.env.ABAP_CLIENT || '100',
132
- user: process.env.ABAP_USER,
133
- password: process.env.ABAP_PASSWORD,
134
- language: process.env.ABAP_LANGUAGE || 'EN',
135
- gitUsername: process.env.GIT_USERNAME,
136
- gitPassword: process.env.GIT_PASSWORD,
137
- transport: process.env.ABAP_TRANSPORT
138
- };
139
- }
140
-
141
- /**
142
- * Get transport request from config or environment
143
- */
144
- function getTransport() {
145
- const config = loadConfig();
146
- return config.transport;
147
- }
148
-
149
- /**
150
- * Check if ABAP integration is configured for this repo
151
- */
152
- function isAbapIntegrationEnabled() {
153
- const repoConfigPath = pathModule.join(process.cwd(), '.abapGitAgent');
154
- return fs.existsSync(repoConfigPath);
155
- }
156
-
157
- /**
158
- * Get git remote URL from .git/config
159
- */
160
- function getGitRemoteUrl() {
161
- const gitConfigPath = pathModule.join(process.cwd(), '.git', 'config');
162
-
163
- if (!fs.existsSync(gitConfigPath)) {
164
- return null;
165
- }
166
-
167
- const content = fs.readFileSync(gitConfigPath, 'utf8');
168
- const remoteMatch = content.match(/\[remote "origin"\]/);
169
-
170
- if (!remoteMatch) {
171
- return null;
172
- }
173
-
174
- const urlMatch = content.match(/ url = (.+)/);
175
- return urlMatch ? urlMatch[1].trim() : null;
176
- }
177
-
178
- /**
179
- * Get current git branch
180
- */
181
- function getGitBranch() {
182
- const headPath = pathModule.join(process.cwd(), '.git', 'HEAD');
183
-
184
- if (!fs.existsSync(headPath)) {
185
- return 'main';
186
- }
187
-
188
- const content = fs.readFileSync(headPath, 'utf8').trim();
189
- const match = content.match(/ref: refs\/heads\/(.+)/);
190
- return match ? match[1] : 'main';
191
- }
192
-
193
- /**
194
- * Read cookies from file
195
- * Supports both Netscape format and simple cookie string format
196
- */
197
- function readNetscapeCookies() {
198
- const cookiePath = pathModule.join(process.cwd(), COOKIE_FILE);
199
- if (!fs.existsSync(cookiePath)) return '';
200
-
201
- const content = fs.readFileSync(cookiePath, 'utf8').trim();
202
- if (!content) return '';
203
-
204
- // Check if it's Netscape format (has tabs)
205
- if (content.includes('\t')) {
206
- const lines = content.split('\n');
207
- const cookies = [];
208
-
209
- for (const line of lines) {
210
- const trimmed = line.trim();
211
- if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#HttpOnly'))) continue;
212
-
213
- const parts = trimmed.split('\t');
214
- if (parts.length >= 7) {
215
- cookies.push(`${parts[5]}=${parts[6]}`);
216
- }
217
- }
218
-
219
- return cookies.join('; ');
220
- }
221
-
222
- // Simple format - just return as-is
223
- return content;
224
- }
225
-
226
- /**
227
- * Save cookies to Netscape format cookie file
228
- */
229
- function saveCookies(cookies, host) {
230
- const cookiePath = pathModule.join(process.cwd(), COOKIE_FILE);
231
-
232
- // Parse cookies and convert to Netscape format
233
- const cookieList = cookies.split(';').map(c => c.trim()).filter(Boolean);
234
- const lines = ['# Netscape HTTP Cookie File', '# https://curl.se/docs/http-cookies.html', '# This file was generated by libcurl! Edit at your own risk.'];
235
-
236
- for (const cookie of cookieList) {
237
- const [name, ...valueParts] = cookie.split('=');
238
- const value = valueParts.join('=');
239
-
240
- // HttpOnly cookies get #HttpOnly_ prefix
241
- const domain = name.includes('SAP') || name.includes('MYSAP') || name.includes('SAPLOGON')
242
- ? `#HttpOnly_${host}`
243
- : host;
244
-
245
- // Format: domain, flag, path, secure, expiration, name, value
246
- lines.push(`${domain}\tFALSE\t/\tFALSE\t0\t${name.trim()}\t${value}`);
247
- }
248
-
249
- fs.writeFileSync(cookiePath, lines.join('\n'));
250
- }
251
-
252
- /**
253
- * Fetch CSRF token using GET /health with X-CSRF-Token: fetch
254
- */
255
- async function fetchCsrfToken(config) {
256
- const https = require('https');
257
- const url = new URL(`/sap/bc/z_abapgit_agent/health`, `https://${config.host}:${config.sapport}`);
258
-
259
- return new Promise((resolve, reject) => {
260
- // Clear stale cookies before fetching new token
261
- const cookiePath = pathModule.join(process.cwd(), COOKIE_FILE);
262
- if (fs.existsSync(cookiePath)) {
263
- fs.unlinkSync(cookiePath);
264
- }
265
-
266
- const cookieHeader = readNetscapeCookies();
267
-
268
- const options = {
269
- hostname: url.hostname,
270
- port: url.port,
271
- path: url.pathname,
272
- method: 'GET',
273
- headers: {
274
- 'Authorization': `Basic ${Buffer.from(`${config.user}:${config.password}`).toString('base64')}`,
275
- 'sap-client': config.client,
276
- 'sap-language': config.language || 'EN',
277
- 'X-CSRF-Token': 'fetch',
278
- 'Content-Type': 'application/json',
279
- ...(cookieHeader && { 'Cookie': cookieHeader })
280
- },
281
- agent: new https.Agent({ rejectUnauthorized: false })
282
- };
283
-
284
- const req = https.request(options, (res) => {
285
- const csrfToken = res.headers['x-csrf-token'];
286
-
287
- // Save new cookies from response
288
- const setCookie = res.headers['set-cookie'];
289
- if (setCookie) {
290
- const cookies = Array.isArray(setCookie)
291
- ? setCookie.map(c => c.split(';')[0]).join('; ')
292
- : setCookie.split(';')[0];
293
- saveCookies(cookies, config.host);
294
- }
295
-
296
- let body = '';
297
- res.on('data', chunk => body += chunk);
298
- res.on('end', () => {
299
- resolve(csrfToken);
300
- });
301
- });
302
-
303
- req.on('error', reject);
304
- req.end();
305
- });
306
- }
307
-
308
- /**
309
- * Make HTTP request to ABAP REST endpoint
310
- */
311
- function request(method, urlPath, data = null, options = {}) {
312
- return new Promise((resolve, reject) => {
313
- const https = require('https');
314
- const http = require('http');
315
- const config = loadConfig();
316
- const url = new URL(urlPath, `https://${config.host}:${config.sapport}`);
317
-
318
- const headers = {
319
- 'Content-Type': 'application/json',
320
- 'sap-client': config.client,
321
- 'sap-language': config.language || 'EN',
322
- ...options.headers
323
- };
324
-
325
- // Add authorization
326
- headers['Authorization'] = `Basic ${Buffer.from(`${config.user}:${config.password}`).toString('base64')}`;
327
-
328
- // Add CSRF token for POST
329
- if (method === 'POST' && options.csrfToken) {
330
- headers['X-CSRF-Token'] = options.csrfToken;
331
- }
332
-
333
- // Add cookies if available
334
- const cookieHeader = readNetscapeCookies();
335
- if (cookieHeader) {
336
- headers['Cookie'] = cookieHeader;
337
- }
338
-
339
- const reqOptions = {
340
- hostname: url.hostname,
341
- port: url.port,
342
- path: url.pathname,
343
- method,
344
- headers,
345
- agent: new https.Agent({ rejectUnauthorized: false })
346
- };
347
-
348
- const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (res) => {
349
- let body = '';
350
- res.on('data', chunk => body += chunk);
351
- res.on('end', () => {
352
- try {
353
- // Handle unescaped newlines from ABAP - replace actual newlines with \n
354
- const cleanedBody = body.replace(/\n/g, '\\n');
355
- resolve(JSON.parse(cleanedBody));
356
- } catch (e) {
357
- // Fallback: try to extract JSON from response
358
- const jsonMatch = body.match(/\{[\s\S]*\}/);
359
- if (jsonMatch) {
360
- try {
361
- resolve(JSON.parse(jsonMatch[0].replace(/\n/g, '\\n')));
362
- } catch (e2) {
363
- resolve({ raw: body, error: e2.message });
364
- }
365
- } else {
366
- resolve({ raw: body, error: e.message });
367
- }
368
- }
369
- });
370
- });
371
-
372
- req.on('error', reject);
373
-
374
- if (data) {
375
- req.write(JSON.stringify(data));
376
- }
377
- req.end();
378
- });
379
- }
380
-
381
- /**
382
- * Inspect ABAP source file
383
- * Reads file content and sends to /inspect
384
- */
385
- async function syntaxCheckSource(sourceFile, csrfToken, config) {
386
- console.log(` Syntax check for file: ${sourceFile}`);
387
-
388
- try {
389
- // Read file content
390
- const absolutePath = pathModule.isAbsolute(sourceFile)
391
- ? sourceFile
392
- : pathModule.join(process.cwd(), sourceFile);
393
-
394
- if (!fs.existsSync(absolutePath)) {
395
- console.error(` ❌ File not found: ${absolutePath}`);
396
- return;
397
- }
398
-
399
- const fileContent = fs.readFileSync(absolutePath, 'utf8');
400
-
401
- // Extract source name from file path (basename with extension)
402
- // e.g., "zcl_my_class.clas.abap" -> "ZCL_MY_CLASS.CLASS.ABAP"
403
- const sourceName = pathModule.basename(sourceFile).toUpperCase();
404
-
405
- // Send files array to syntax-check endpoint
406
- const data = {
407
- files: [sourceName]
408
- };
409
-
410
- const result = await request('POST', '/sap/bc/z_abapgit_agent/inspect', data, { csrfToken: csrfToken });
411
-
412
- // Handle new table-based result format (array) or old single result
413
- let results = [];
414
- if (Array.isArray(result)) {
415
- results = result;
416
- } else {
417
- // Convert old single result to array format for compatibility
418
- results = [{
419
- OBJECT_TYPE: 'UNKNOWN',
420
- OBJECT_NAME: pathModule.basename(sourceFile),
421
- SUCCESS: result.SUCCESS !== undefined ? result.SUCCESS === 'X' || result.SUCCESS === true : result.success,
422
- ERROR_COUNT: result.ERROR_COUNT || result.error_count || 0,
423
- ERRORS: result.ERRORS || result.errors || [],
424
- WARNINGS: result.warnings || []
425
- }];
426
- }
427
-
428
- // Process each result
429
- let hasErrors = false;
430
- for (const res of results) {
431
- // Handle both uppercase and lowercase keys
432
- const success = res.SUCCESS !== undefined ? res.SUCCESS : res.success;
433
- const objectType = res.OBJECT_TYPE !== undefined ? res.OBJECT_TYPE : res.object_type;
434
- const objectName = res.OBJECT_NAME !== undefined ? res.OBJECT_NAME : res.object_name;
435
- const errorCount = res.ERROR_COUNT !== undefined ? res.ERROR_COUNT : (res.error_count || 0);
436
- const errors = res.ERRORS !== undefined ? res.ERRORS : (res.errors || []);
437
- const warnings = res.WARNINGS !== undefined ? res.WARNINGS : (res.warnings || []);
438
-
439
- console.log('\n');
440
-
441
- if (errorCount > 0 || warnings.length > 0) {
442
- if (errorCount > 0) {
443
- console.log(`❌ ${objectType} ${objectName} - Syntax check failed (${errorCount} error(s)):`);
444
- } else {
445
- console.log(`⚠️ ${objectType} ${objectName} - Syntax check passed with warnings (${warnings.length}):`);
446
- }
447
- console.log('\nErrors:');
448
- console.log('─'.repeat(60));
449
-
450
- for (const err of errors) {
451
- const line = err.LINE || err.line || '?';
452
- const column = err.COLUMN || err.column || '?';
453
- const text = err.TEXT || err.text || 'Unknown error';
454
-
455
- console.log(` Line ${line}, Column ${column}:`);
456
- console.log(` ${text}`);
457
- console.log('');
458
- }
459
-
460
- // Show warnings if any
461
- if (warnings.length > 0) {
462
- console.log('Warnings:');
463
- console.log('─'.repeat(60));
464
- for (const warn of warnings) {
465
- const line = warn.LINE || warn.line || '?';
466
- const text = warn.MESSAGE || warn.message || 'Unknown warning';
467
- console.log(` Line ${line}: ${text}`);
468
- }
469
- }
470
- hasErrors = true;
471
- } else if (success === true || success === 'X') {
472
- console.log(`✅ ${objectType} ${objectName} - Syntax check passed`);
473
- } else {
474
- console.log(`⚠️ ${objectType} ${objectName} - Syntax check returned unexpected status`);
475
- }
476
- }
477
-
478
- return result;
479
- } catch (error) {
480
- console.error(`\n Error: ${error.message}`);
481
- process.exit(1);
482
- }
483
- }
484
-
485
- /**
486
- * Inspect all files in one request
487
- */
488
- async function inspectAllFiles(files, csrfToken, config, variant = null) {
489
- // Convert files to uppercase names (same as syntaxCheckSource does)
490
- const fileNames = files.map(f => {
491
- const baseName = pathModule.basename(f).toUpperCase();
492
- return baseName;
493
- });
494
-
495
- try {
496
- // Send all files in one request
497
- const data = {
498
- files: fileNames
499
- };
500
-
501
- // Add variant if specified
502
- if (variant) {
503
- data.variant = variant;
504
- }
505
-
506
- const result = await request('POST', '/sap/bc/z_abapgit_agent/inspect', data, { csrfToken: csrfToken });
507
-
508
- // Handle both table result and old single result
509
- let results = [];
510
- if (Array.isArray(result)) {
511
- results = result;
512
- } else {
513
- // Convert single result to array format
514
- results = [{
515
- OBJECT_TYPE: 'UNKNOWN',
516
- OBJECT_NAME: files.join(', '),
517
- SUCCESS: result.SUCCESS !== undefined ? result.SUCCESS === 'X' || result.SUCCESS === true : result.success,
518
- ERROR_COUNT: result.ERROR_COUNT || result.error_count || 0,
519
- ERRORS: result.ERRORS || result.errors || [],
520
- WARNINGS: result.warnings || []
521
- }];
522
- }
523
-
524
- return results;
525
- } catch (error) {
526
- console.error(`\n Error: ${error.message}`);
527
- process.exit(1);
528
- }
529
- }
530
-
531
- /**
532
- * Process a single inspect result
533
- */
534
- async function processInspectResult(res) {
535
- // Handle both uppercase and lowercase keys
536
- const success = res.SUCCESS !== undefined ? res.SUCCESS : res.success;
537
- const objectType = res.OBJECT_TYPE !== undefined ? res.OBJECT_TYPE : res.object_type;
538
- const objectName = res.OBJECT_NAME !== undefined ? res.OBJECT_NAME : res.object_name;
539
- const errorCount = res.ERROR_COUNT !== undefined ? res.ERROR_COUNT : (res.error_count || 0);
540
- const errors = res.ERRORS !== undefined ? res.ERRORS : (res.errors || []);
541
- const warnings = res.WARNINGS !== undefined ? res.WARNINGS : (res.warnings || []);
542
- const infos = res.INFOS !== undefined ? res.INFOS : (res.infos || []);
543
-
544
- if (errorCount > 0 || warnings.length > 0 || infos.length > 0) {
545
- if (errorCount > 0) {
546
- console.log(`❌ ${objectType} ${objectName} - Syntax check failed (${errorCount} error(s)):`);
547
- } else {
548
- const total = warnings.length + infos.length;
549
- console.log(`⚠️ ${objectType} ${objectName} - Syntax check passed with warnings (${total}):`);
550
- }
551
- console.log('\nErrors:');
552
- console.log('─'.repeat(60));
553
-
554
- for (const err of errors) {
555
- const line = err.LINE || err.line || '?';
556
- const column = err.COLUMN || err.column || '?';
557
- const text = err.TEXT || err.text || 'Unknown error';
558
- const methodName = err.METHOD_NAME || err.method_name;
559
- const sobjname = err.SOBJNAME || err.sobjname;
560
-
561
- if (methodName) {
562
- console.log(` Method: ${methodName}`);
563
- }
564
- console.log(` Line ${line}, Column ${column}:`);
565
- if (sobjname && sobjname.includes('====')) {
566
- console.log(` Include: ${sobjname}`);
567
- }
568
- console.log(` ${text}`);
569
- console.log('');
570
- }
571
-
572
- // Show warnings if any
573
- if (warnings.length > 0) {
574
- console.log('Warnings:');
575
- console.log('─'.repeat(60));
576
- for (const warn of warnings) {
577
- const line = warn.LINE || warn.line || '?';
578
- const text = warn.MESSAGE || warn.message || 'Unknown warning';
579
- const methodName = warn.METHOD_NAME || warn.method_name;
580
- const sobjname = warn.SOBJNAME || warn.sobjname;
581
-
582
- if (methodName) {
583
- console.log(` Method: ${methodName}`);
584
- }
585
- console.log(` Line ${line}:`);
586
- if (sobjname && sobjname.includes('====')) {
587
- console.log(` Include: ${sobjname}`);
588
- }
589
- console.log(` ${text}`);
590
- }
591
- }
592
-
593
- // Show infos if any
594
- if (infos.length > 0) {
595
- console.log('Info:');
596
- console.log('─'.repeat(60));
597
- for (const info of infos) {
598
- const line = info.LINE || info.line || '?';
599
- const text = info.MESSAGE || info.message || 'Unknown info';
600
- const methodName = info.METHOD_NAME || info.method_name;
601
- const sobjname = info.SOBJNAME || info.sobjname;
602
-
603
- if (methodName) {
604
- console.log(` Method: ${methodName}`);
605
- }
606
- console.log(` Line ${line}:`);
607
- if (sobjname && sobjname.includes('====')) {
608
- console.log(` Include: ${sobjname}`);
609
- }
610
- console.log(` ${text}`);
611
- }
612
- }
613
- } else if (success === true || success === 'X') {
614
- console.log(`✅ ${objectType} ${objectName} - Syntax check passed`);
615
- } else {
616
- console.log(`⚠️ ${objectType} ${objectName} - Syntax check returned unexpected status`);
617
- }
618
- }
619
-
620
- /**
621
- * Run unit tests for package or objects
622
- */
623
- async function runUnitTests(options) {
624
- console.log('\n Running unit tests');
625
-
626
- try {
627
- const config = loadConfig();
628
-
629
- // Fetch CSRF token first
630
- const csrfToken = await fetchCsrfToken(config);
631
-
632
- const data = {};
633
-
634
- if (options.package) {
635
- data.package = options.package;
636
- }
637
-
638
- if (options.objects && options.objects.length > 0) {
639
- data.objects = options.objects;
640
- }
641
-
642
- const result = await request('POST', '/sap/bc/z_abapgit_agent/unit', data, { csrfToken });
643
-
644
- // Display raw result for debugging
645
- if (process.env.DEBUG) {
646
- console.log('Raw result:', JSON.stringify(result, null, 2));
647
- }
648
-
649
- // Handle uppercase keys from ABAP
650
- const success = result.SUCCESS || result.success;
651
- const testCount = result.TEST_COUNT || result.test_count || 0;
652
- const passedCount = result.PASSED_COUNT || result.passed_count || 0;
653
- const failedCount = result.FAILED_COUNT || result.failed_count || 0;
654
- const message = result.MESSAGE || result.message || '';
655
- const errors = result.ERRORS || result.errors || [];
656
-
657
- console.log('\n');
658
-
659
- if (success === 'X' || success === true) {
660
- console.log(`✅ All tests passed!`);
661
- } else {
662
- console.log(`❌ Some tests failed`);
663
- }
664
-
665
- console.log(` ${message}`);
666
- console.log(` Tests: ${testCount} | Passed: ${passedCount} | Failed: ${failedCount}`);
667
-
668
- if (failedCount > 0 && errors.length > 0) {
669
- console.log('\nFailed Tests:');
670
- console.log('─'.repeat(60));
671
-
672
- for (const err of errors) {
673
- const className = err.CLASS_NAME || err.class_name || '?';
674
- const methodName = err.METHOD_NAME || err.method_name || '?';
675
- const errorKind = err.ERROR_KIND || err.error_kind || '';
676
- const errorText = err.ERROR_TEXT || err.error_text || 'Unknown error';
677
-
678
- console.log(` ✗ ${className}=>${methodName}`);
679
- if (errorKind) {
680
- console.log(` Kind: ${errorKind}`);
681
- }
682
- console.log(` Error: ${errorText}`);
683
- console.log('');
684
- }
685
- }
686
-
687
- return result;
688
- } catch (error) {
689
- console.error(`\n Error: ${error.message}`);
690
- process.exit(1);
691
- }
692
- }
693
-
694
- /**
695
- * Run unit test for a single file
696
- */
697
- async function runUnitTestForFile(sourceFile, csrfToken, config, coverage = false) {
698
- console.log(` Running unit test for: ${sourceFile}`);
699
-
700
- try {
701
- // Read file content
702
- const absolutePath = pathModule.isAbsolute(sourceFile)
703
- ? sourceFile
704
- : pathModule.join(process.cwd(), sourceFile);
705
-
706
- if (!fs.existsSync(absolutePath)) {
707
- console.error(` ❌ File not found: ${absolutePath}`);
708
- return;
709
- }
710
-
711
- // Extract object type and name from file path
712
- // e.g., "zcl_my_test.clas.abap" -> CLAS, ZCL_MY_TEST
713
- const fileName = pathModule.basename(sourceFile).toUpperCase();
714
- const parts = fileName.split('.');
715
- if (parts.length < 3) {
716
- console.error(` ❌ Invalid file format: ${sourceFile}`);
717
- return;
718
- }
719
-
720
- // obj_name is first part (may contain path), obj_type is second part
721
- const objType = parts[1] === 'CLASS' ? 'CLAS' : parts[1];
722
- let objName = parts[0];
723
-
724
- // Handle subdirectory paths
725
- const lastSlash = objName.lastIndexOf('/');
726
- if (lastSlash >= 0) {
727
- objName = objName.substring(lastSlash + 1);
728
- }
729
-
730
- // Send files array to unit endpoint (ABAP expects string_table of file names)
731
- const data = {
732
- files: [sourceFile],
733
- coverage: coverage
734
- };
735
-
736
- const result = await request('POST', '/sap/bc/z_abapgit_agent/unit', data, { csrfToken });
737
-
738
- // Handle uppercase keys from ABAP
739
- const success = result.SUCCESS || result.success;
740
- const testCount = result.TEST_COUNT || result.test_count || 0;
741
- const passedCount = result.PASSED_COUNT || result.passed_count || 0;
742
- const failedCount = result.FAILED_COUNT || result.failed_count || 0;
743
- const message = result.MESSAGE || result.message || '';
744
- const errors = result.ERRORS || result.errors || [];
745
-
746
- // Handle coverage data
747
- const coverageStats = result.COVERAGE_STATS || result.coverage_stats;
748
-
749
- if (testCount === 0) {
750
- console.log(` ➖ ${objName} - No unit tests`);
751
- } else if (success === 'X' || success === true) {
752
- console.log(` ✅ ${objName} - All tests passed`);
753
- } else {
754
- console.log(` ❌ ${objName} - Tests failed`);
755
- }
756
-
757
- console.log(` Tests: ${testCount} | Passed: ${passedCount} | Failed: ${failedCount}`);
758
-
759
- // Display coverage if available
760
- if (coverage && coverageStats) {
761
- const totalLines = coverageStats.TOTAL_LINES || coverageStats.total_lines || 0;
762
- const coveredLines = coverageStats.COVERED_LINES || coverageStats.covered_lines || 0;
763
- const coverageRate = coverageStats.COVERAGE_RATE || coverageStats.coverage_rate || 0;
764
-
765
- console.log(` 📊 Coverage: ${coverageRate}%`);
766
- console.log(` Total Lines: ${totalLines}`);
767
- console.log(` Covered Lines: ${coveredLines}`);
768
- }
769
-
770
- if (failedCount > 0 && errors.length > 0) {
771
- for (const err of errors) {
772
- const className = err.CLASS_NAME || err.class_name || '?';
773
- const methodName = err.METHOD_NAME || err.method_name || '?';
774
- const errorText = err.ERROR_TEXT || err.error_text || 'Unknown error';
775
- console.log(` ✗ ${className}=>${methodName}: ${errorText}`);
776
- }
777
- }
778
-
779
- return result;
780
- } catch (error) {
781
- console.error(`\n Error: ${error.message}`);
782
- }
783
- }
784
-
785
- /**
786
- * Run tree command and return raw result
787
- */
788
- async function runTreeCommand(packageName, depth, includeTypes, csrfToken, config) {
789
- const data = {
790
- package: packageName,
791
- depth: depth,
792
- include_objects: includeTypes
793
- };
794
-
795
- return await request('POST', '/sap/bc/z_abapgit_agent/tree', data, { csrfToken });
796
- }
797
-
798
- /**
799
- * Display tree output in human-readable format
800
- */
801
- async function displayTreeOutput(packageName, depth, includeTypes) {
802
- const config = loadConfig();
803
- const csrfToken = await fetchCsrfToken(config);
804
-
805
- console.log(`\n Getting package tree for: ${packageName}`);
806
-
807
- const result = await runTreeCommand(packageName, depth, includeTypes, csrfToken, config);
808
-
809
- // Handle uppercase keys from ABAP
810
- const success = result.SUCCESS || result.success;
811
- const error = result.ERROR || result.error;
812
-
813
- if (!success || error) {
814
- console.error(`\n ❌ Error: ${error || 'Failed to get tree'}`);
815
- return;
816
- }
817
-
818
- // Parse hierarchy structure (ABAP returns flat nodes with parent refs)
819
- const nodes = result.NODES || result.nodes || [];
820
- const rootPackage = result.PACKAGE || result.package || packageName;
821
- const parentPackage = result.PARENT_PACKAGE || result.parent_package;
822
- const totalPackages = result.TOTAL_PACKAGES || result.total_packages || 0;
823
- const totalObjects = result.TOTAL_OBJECTS || result.total_objects || 0;
824
- const objectTypes = result.OBJECTS || result.objects || [];
825
-
826
- console.log(`\n Package Tree: ${rootPackage}`);
827
-
828
- // Display parent info if available
829
- if (parentPackage && parentPackage !== rootPackage) {
830
- console.log(` ⬆️ Parent: ${parentPackage}`);
831
- }
832
-
833
- console.log('');
834
-
835
- // Build and display tree from flat nodes list
836
- const lines = buildTreeLinesFromNodes(nodes, '', true);
837
- for (const line of lines) {
838
- console.log(` ${line}`);
839
- }
840
-
841
- console.log('');
842
- console.log(' Summary');
843
- console.log(` PACKAGES: ${totalPackages}`);
844
- console.log(` OBJECTS: ${totalObjects}`);
845
-
846
- // Display object types if available
847
- if (includeTypes && objectTypes.length > 0) {
848
- const typeStr = objectTypes.map(t => `${t.OBJECT || t.object}=${t.COUNT || t.count}`).join(' ');
849
- console.log(` TYPES: ${typeStr}`);
850
- }
851
- }
852
-
853
- /**
854
- * Build tree display lines from flat nodes list
855
- */
856
- function buildTreeLinesFromNodes(nodes, prefix, isLast) {
857
- const lines = [];
858
-
859
- if (nodes.length === 0) {
860
- return lines;
861
- }
862
-
863
- // First node is the root
864
- const root = nodes[0];
865
- const icon = '📦';
866
- lines.push(`${prefix}${isLast ? '└─ ' : '├─ '} ${icon} ${root.PACKAGE || root.package}`);
867
-
868
- // Get children (nodes with depth > 0, grouped by depth)
869
- const children = nodes.filter(n => (n.DEPTH || n.depth) > 0);
870
-
871
- // Group children by depth
872
- const byDepth = {};
873
- children.forEach(n => {
874
- const d = n.DEPTH || n.depth;
875
- if (!byDepth[d]) byDepth[d] = [];
876
- byDepth[d].push(n);
877
- });
878
-
879
- // Process depth 1 children
880
- const depth1 = byDepth[1] || [];
881
- const newPrefix = prefix + (isLast ? ' ' : '│ ');
882
-
883
- for (let i = 0; i < depth1.length; i++) {
884
- const child = depth1[i];
885
- const isSubLast = i === depth1.length - 1;
886
- const childLines = buildChildLines(child, newPrefix, isSubLast, byDepth);
887
- lines.push(...childLines);
888
- }
889
-
890
- return lines;
891
- }
892
-
893
- function buildChildLines(node, prefix, isLast, byDepth) {
894
- const lines = [];
895
- const icon = '📦';
896
- lines.push(`${prefix}${isLast ? '└─ ' : '├─ '} ${icon} ${node.PACKAGE || node.package}`);
897
-
898
- const newPrefix = prefix + (isLast ? ' ' : '│ ');
899
- const nodeDepth = node.DEPTH || node.depth;
900
- const children = byDepth[nodeDepth + 1] || [];
901
-
902
- // Find children of this node
903
- const myChildren = children.filter(n => (n.PARENT || n.parent) === (node.PACKAGE || node.package));
904
-
905
- for (let i = 0; i < myChildren.length; i++) {
906
- const child = myChildren[i];
907
- const isSubLast = i === myChildren.length - 1;
908
- const childLines = buildChildLines(child, newPrefix, isSubLast, byDepth);
909
- lines.push(...childLines);
910
- }
911
-
912
- return lines;
913
- }
914
-
915
- /**
916
- * Pull and activate repository
917
- */
918
- async function pull(gitUrl, branch = 'main', files = null, transportRequest = null) {
919
- console.log(`\n🚀 Starting pull for: ${gitUrl}`);
920
- console.log(` Branch: ${branch}`);
921
- if (files && files.length > 0) {
922
- console.log(` Files: ${files.join(', ')}`);
923
- }
924
- if (transportRequest) {
925
- console.log(` Transport Request: ${transportRequest}`);
926
- }
927
-
928
- try {
929
- const config = loadConfig();
930
-
931
- // Fetch CSRF token first
932
- const csrfToken = await fetchCsrfToken(config);
933
-
934
- // Prepare request data with git credentials
935
- const data = {
936
- url: gitUrl,
937
- branch: branch,
938
- username: config.gitUsername,
939
- password: config.gitPassword
940
- };
941
-
942
- // Add files array if specified
943
- if (files && files.length > 0) {
944
- data.files = files;
945
- }
946
-
947
- // Add transport request if specified
948
- if (transportRequest) {
949
- data.transport_request = transportRequest;
950
- }
951
-
952
- const result = await request('POST', '/sap/bc/z_abapgit_agent/pull', data, { csrfToken });
953
-
954
- console.log('\n');
955
-
956
- // Display raw result for debugging
957
- if (process.env.DEBUG) {
958
- console.log('Raw result:', JSON.stringify(result, null, 2));
959
- }
960
-
961
- // Handle uppercase keys from ABAP
962
- const success = result.SUCCESS || result.success;
963
- const jobId = result.JOB_ID || result.job_id;
964
- const message = result.MESSAGE || result.message;
965
- const errorDetail = result.ERROR_DETAIL || result.error_detail;
966
- const transportRequestUsed = result.TRANSPORT_REQUEST || result.transport_request;
967
- const activatedCount = result.ACTIVATED_COUNT || result.activated_count || 0;
968
- const failedCount = result.FAILED_COUNT || result.failed_count || 0;
969
- const logMessages = result.LOG_MESSAGES || result.log_messages || [];
970
- const activatedObjects = result.ACTIVATED_OBJECTS || result.activated_objects || [];
971
- const failedObjects = result.FAILED_OBJECTS || result.failed_objects || [];
972
-
973
- // Icon mapping for message types
974
- const getIcon = (type) => {
975
- const icons = {
976
- 'S': '✅', // Success
977
- 'E': '❌', // Error
978
- 'W': '⚠️', // Warning
979
- 'A': '🛑' // Abort
980
- };
981
- return icons[type] || '';
982
- };
983
-
984
- // Calculate display width accounting for emoji (2 cells) vs ASCII (1 cell)
985
- const calcWidth = (str) => {
986
- if (!str) return 0;
987
- let width = 0;
988
- let i = 0;
989
- while (i < str.length) {
990
- const code = str.codePointAt(i);
991
- if (!code) break;
992
- // Variation selectors (FE00-FE0F) and ZWJ (200D) take 0 width
993
- if (code >= 0xFE00 && code <= 0xFE0F) {
994
- i += 1;
995
- continue;
996
- }
997
- if (code === 0x200D) { // Zero width joiner
998
- i += 1;
999
- continue;
1000
- }
1001
- // Emoji and wide characters take 2 cells
1002
- if (code > 0xFFFF) {
1003
- width += 2;
1004
- i += 2; // Skip surrogate pair
1005
- } else if (code > 127) {
1006
- width += 2;
1007
- i += 1;
1008
- } else {
1009
- width += 1;
1010
- i += 1;
1011
- }
1012
- }
1013
- return width;
1014
- };
1015
-
1016
- // Pad string to display width
1017
- const padToWidth = (str, width) => {
1018
- const s = str || '';
1019
- const currentWidth = calcWidth(s);
1020
- return s + ' '.repeat(Math.max(0, width - currentWidth));
1021
- };
1022
-
1023
- if (success === 'X' || success === true) {
1024
- console.log(`✅ Pull completed successfully!`);
1025
- console.log(` Job ID: ${jobId || 'N/A'}`);
1026
- console.log(` Message: ${message || 'N/A'}`);
1027
- } else {
1028
- console.log(`❌ Pull completed with errors!`);
1029
- console.log(` Job ID: ${jobId || 'N/A'}`);
1030
- console.log(` Message: ${message || 'N/A'}`);
1031
- }
1032
-
1033
- // Display error detail if available
1034
- if (errorDetail && errorDetail.trim()) {
1035
- console.log(`\n📋 Error Details:`);
1036
- console.log('─'.repeat(TERM_WIDTH));
1037
- // Handle escaped newlines from ABAP JSON
1038
- const formattedDetail = errorDetail.replace(/\\n/g, '\n');
1039
- console.log(formattedDetail);
1040
- }
1041
-
1042
- // Display all messages as table (from log_messages)
1043
- if (logMessages && logMessages.length > 0) {
1044
- console.log(`\n📋 Pull Log (${logMessages.length} messages):`);
1045
-
1046
- // Calculate column widths based on terminal width
1047
- const tableWidth = Math.min(TERM_WIDTH, 120);
1048
- const iconCol = 4; // Fixed width for icon column
1049
- const objCol = 28;
1050
- const msgCol = tableWidth - iconCol - objCol - 6; // Account for vertical lines (3 chars)
1051
-
1052
- // Pad string to display width using calcWidth for emoji support
1053
- const padToWidth = (str, width) => {
1054
- const s = str || '';
1055
- const currentWidth = calcWidth(s);
1056
- return s + ' '.repeat(Math.max(0, width - currentWidth));
1057
- };
1058
-
1059
- const headerLine = '─'.repeat(iconCol) + '┼' + '─'.repeat(objCol) + '┼' + '─'.repeat(msgCol);
1060
- const headerText = padToWidth('Icon', iconCol) + '│' + padToWidth('Object', objCol) + '│' + 'Message'.substring(0, msgCol);
1061
- const borderLine = '─'.repeat(tableWidth);
1062
-
1063
- console.log(borderLine);
1064
- console.log(headerText);
1065
- console.log(headerLine);
1066
-
1067
- for (const msg of logMessages) {
1068
- const icon = getIcon(msg.TYPE);
1069
- const objType = msg.OBJ_TYPE || '';
1070
- const objName = msg.OBJ_NAME || '';
1071
- const obj = objType && objName ? `${objType} ${objName}` : '';
1072
- const text = msg.TEXT || '';
1073
-
1074
- // Truncate long text
1075
- const displayText = text.length > msgCol ? text.substring(0, msgCol - 3) + '...' : text;
1076
-
1077
- console.log(padToWidth(icon, iconCol) + '│' + padToWidth(obj.substring(0, objCol), objCol) + '│' + displayText);
1078
- }
1079
- }
1080
-
1081
- // Extract objects with errors from log messages (type 'E' = Error)
1082
- const failedObjectsFromLog = [];
1083
- const objectsWithErrors = new Set();
1084
-
1085
- for (const msg of logMessages) {
1086
- if (msg.TYPE === 'E' && msg.OBJ_TYPE && msg.OBJ_NAME) {
1087
- const objKey = `${msg.OBJ_TYPE} ${msg.OBJ_NAME}`;
1088
- objectsWithErrors.add(objKey);
1089
- failedObjectsFromLog.push({
1090
- OBJ_TYPE: msg.OBJ_TYPE,
1091
- OBJ_NAME: msg.OBJ_NAME,
1092
- TEXT: msg.TEXT || 'Activation failed',
1093
- EXCEPTION: msg.EXCEPTION || ''
1094
- });
1095
- }
1096
- }
1097
-
1098
- // Filter activated objects - only include objects without errors
1099
- const filteredActivatedObjects = activatedObjects.filter(obj => {
1100
- const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
1101
- return objKey && !objectsWithErrors.has(objKey);
1102
- });
1103
-
1104
- // Display unique activated objects (excluding objects with errors)
1105
- if (filteredActivatedObjects && filteredActivatedObjects.length > 0) {
1106
- console.log(`\n📦 Activated Objects (${filteredActivatedObjects.length}):`);
1107
- console.log('─'.repeat(TERM_WIDTH));
1108
- for (const obj of filteredActivatedObjects) {
1109
- console.log(`✅ ${obj.OBJ_TYPE} ${obj.OBJ_NAME}`);
1110
- }
1111
- }
1112
-
1113
- // Display failed objects log (all error messages, duplicates allowed)
1114
- if (failedObjectsFromLog.length > 0) {
1115
- console.log(`\n❌ Failed Objects Log (${failedObjectsFromLog.length} entries):`);
1116
- console.log('─'.repeat(TERM_WIDTH));
1117
- for (const obj of failedObjectsFromLog) {
1118
- const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
1119
- let text = obj.TEXT || 'Activation failed';
1120
- // Include exception text if available
1121
- if (obj.EXCEPTION && obj.EXCEPTION.trim()) {
1122
- text = `${text}\nException: ${obj.EXCEPTION}`;
1123
- }
1124
- console.log(`❌ ${objKey}: ${text}`);
1125
- }
1126
- } else if (failedObjects && failedObjects.length > 0) {
1127
- // Fallback to API failed_objects if no errors in log
1128
- console.log(`\n❌ Failed Objects Log (${failedObjects.length}):`);
1129
- console.log('─'.repeat(TERM_WIDTH));
1130
- for (const obj of failedObjects) {
1131
- const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
1132
- let text = obj.TEXT || 'Activation failed';
1133
- // Include exception text if available
1134
- if (obj.EXCEPTION && obj.EXCEPTION.trim()) {
1135
- text = `${text}\nException: ${obj.EXCEPTION}`;
1136
- }
1137
- console.log(`❌ ${objKey}: ${text}`);
1138
- }
1139
- } else if (failedCount > 0) {
1140
- console.log(`\n❌ Failed Objects Log (${failedCount})`);
1141
- }
1142
-
1143
- return result;
1144
- } catch (error) {
1145
- console.error(`\n❌ Error: ${error.message}`);
1146
- process.exit(1);
1147
- }
1148
- }
1149
-
1150
- /**
1151
- * Copy a file if source exists (helper for init --update)
1152
- * @param {string} srcPath - Source file path
1153
- * @param {string} destPath - Destination file path
1154
- * @param {string} label - Label for console output
1155
- * @param {boolean} createParentDir - Whether to create parent directory
1156
- * @returns {Promise<void>}
1157
- */
1158
- async function copyFileIfExists(srcPath, destPath, label, createParentDir = false) {
1159
- try {
1160
- if (fs.existsSync(srcPath)) {
1161
- if (createParentDir) {
1162
- const parentDir = pathModule.dirname(destPath);
1163
- if (!fs.existsSync(parentDir)) {
1164
- fs.mkdirSync(parentDir, { recursive: true });
1165
- }
1166
- }
1167
- fs.copyFileSync(srcPath, destPath);
1168
- console.log(`✅ Updated ${label}`);
1169
- } else {
1170
- console.log(`⚠️ ${label} not found in abapgit-agent`);
1171
- }
1172
- } catch (error) {
1173
- console.error(`Error copying ${label}: ${error.message}`);
1174
- }
1175
- }
1176
-
1177
- /**
1178
- * Copy guidelines folder (helper for init --update)
1179
- * @param {string} srcPath - Source folder path
1180
- * @param {string} destPath - Destination folder path
1181
- * @param {boolean} overwrite - Whether to overwrite existing files
1182
- * @returns {Promise<void>}
1183
- */
1184
- async function copyGuidelinesFolder(srcPath, destPath, overwrite = false) {
1185
- try {
1186
- if (fs.existsSync(srcPath)) {
1187
- // Create destination directory if needed
1188
- if (!fs.existsSync(destPath)) {
1189
- fs.mkdirSync(destPath, { recursive: true });
1190
- }
1191
-
1192
- const files = fs.readdirSync(srcPath);
1193
- let copied = 0;
1194
-
1195
- for (const file of files) {
1196
- if (file.endsWith('.md')) {
1197
- const srcFile = pathModule.join(srcPath, file);
1198
- const destFile = pathModule.join(destPath, file);
1199
-
1200
- // Skip if file exists and not overwriting
1201
- if (fs.existsSync(destFile) && !overwrite) {
1202
- continue;
1203
- }
1204
-
1205
- fs.copyFileSync(srcFile, destFile);
1206
- copied++;
1207
- }
1208
- }
1209
-
1210
- if (copied > 0) {
1211
- console.log(`✅ Updated guidelines/ (${copied} files)`);
1212
- } else {
1213
- console.log(`⚠️ No guideline files found`);
1214
- }
1215
- } else {
1216
- console.log(`⚠️ guidelines folder not found in abapgit-agent`);
1217
- }
1218
- } catch (error) {
1219
- console.error(`Error copying guidelines: ${error.message}`);
1220
- }
1221
- }
1222
-
1223
- /**
1224
- * Run init command - Initialize local configuration
1225
- */
1226
- async function runInit(args) {
1227
- const folderArgIndex = args.indexOf('--folder');
1228
- const packageArgIndex = args.indexOf('--package');
1229
- const updateMode = args.includes('--update');
1230
-
1231
- // Get parameters
1232
- let folder = folderArgIndex !== -1 && folderArgIndex + 1 < args.length
1233
- ? args[folderArgIndex + 1]
1234
- : '/src/';
1235
-
1236
- // Normalize folder path: ensure it starts with / and ends with /
1237
- folder = folder.trim();
1238
- if (!folder.startsWith('/')) {
1239
- folder = '/' + folder;
1240
- }
1241
- if (!folder.endsWith('/')) {
1242
- folder = folder + '/';
1243
- }
1244
-
1245
- const packageName = packageArgIndex !== -1 && packageArgIndex + 1 < args.length
1246
- ? args[packageArgIndex + 1]
1247
- : null;
1248
-
1249
- // Validate package is required for non-update mode
1250
- if (!updateMode && !packageName) {
1251
- console.error('Error: --package is required');
1252
- console.error('Usage: abapgit-agent init --folder /src --package ZMY_PACKAGE');
1253
- console.error(' abapgit-agent init --update # Update files to latest version');
1254
- process.exit(1);
1255
- }
1256
-
1257
- // Check if current folder is git repo root
1258
- const gitDir = pathModule.join(process.cwd(), '.git');
1259
- if (!fs.existsSync(gitDir)) {
1260
- console.error('Error: Current folder is not a git repository.');
1261
- console.error('Run this command from the root folder of your git repository.');
1262
- process.exit(1);
1263
- }
1264
-
1265
- // In update mode, just copy the files and return
1266
- if (updateMode) {
1267
- console.log(`\n🔄 Updating abapGit Agent files`);
1268
- console.log('');
1269
-
1270
- // Copy CLAUDE.md
1271
- await copyFileIfExists(
1272
- pathModule.join(__dirname, '..', 'abap', 'CLAUDE.md'),
1273
- pathModule.join(process.cwd(), 'CLAUDE.md'),
1274
- 'CLAUDE.md'
1275
- );
1276
-
1277
- // Copy copilot-instructions.md
1278
- await copyFileIfExists(
1279
- pathModule.join(__dirname, '..', 'abap', '.github', 'copilot-instructions.md'),
1280
- pathModule.join(process.cwd(), '.github', 'copilot-instructions.md'),
1281
- '.github/copilot-instructions.md',
1282
- true // create parent dir
1283
- );
1284
-
1285
- // Copy guidelines folder to project root
1286
- await copyGuidelinesFolder(
1287
- pathModule.join(__dirname, '..', 'abap', 'guidelines'),
1288
- pathModule.join(process.cwd(), 'guidelines'),
1289
- true // overwrite
1290
- );
1291
-
1292
- console.log(`
1293
- 📋 Update complete!
1294
- Run 'abapgit-agent ref --list-topics' to see available topics.
1295
- `);
1296
- return;
1297
- }
1298
-
1299
- // Validate package is required
1300
- if (!packageName) {
1301
- console.error('Error: --package is required');
1302
- console.error('Usage: abapgit-agent init --folder /src --package ZMY_PACKAGE');
1303
- console.error(' abapgit-agent init --update # Update files to latest version');
1304
- process.exit(1);
1305
- }
1306
-
1307
- console.log(`\n🚀 Initializing abapGit Agent for local repository`);
1308
- console.log(` Folder: ${folder}`);
1309
- console.log(` Package: ${packageName}`);
1310
- console.log('');
1311
-
1312
- // Detect git remote URL
1313
- const gitUrl = getGitRemoteUrl();
1314
- if (!gitUrl) {
1315
- console.error('Error: No git remote configured.');
1316
- console.error('Configure a remote with: git remote add origin <url>');
1317
- process.exit(1);
1318
- }
1319
- console.log(`📌 Git remote: ${gitUrl}`);
1320
-
1321
- // Check if .abapGitAgent already exists
1322
- const configPath = pathModule.join(process.cwd(), '.abapGitAgent');
1323
- if (fs.existsSync(configPath)) {
1324
- console.error('Error: .abapGitAgent already exists.');
1325
- console.error('To reinitialize, delete the existing file first.');
1326
- process.exit(1);
1327
- }
1328
-
1329
- // Copy .abapGitAgent.example to .abapGitAgent
1330
- const samplePath = pathModule.join(__dirname, '..', '.abapGitAgent.example');
1331
- if (!fs.existsSync(samplePath)) {
1332
- console.error('Error: .abapGitAgent.example not found.');
1333
- process.exit(1);
1334
- }
1335
-
1336
- try {
1337
- // Read sample and update with package/folder
1338
- const sampleContent = fs.readFileSync(samplePath, 'utf8');
1339
- const config = JSON.parse(sampleContent);
1340
- config.package = packageName;
1341
- config.folder = folder;
1342
-
1343
- // Write updated config
1344
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
1345
- console.log(`✅ Created .abapGitAgent`);
1346
- } catch (error) {
1347
- console.error(`Error creating .abapGitAgent: ${error.message}`);
1348
- process.exit(1);
1349
- }
1350
-
1351
- // Update .gitignore
1352
- const gitignorePath = pathModule.join(process.cwd(), '.gitignore');
1353
- const ignoreEntries = ['.abapGitAgent', '.abapgit_agent_cookies.txt'];
1354
- let existingIgnore = '';
1355
-
1356
- if (fs.existsSync(gitignorePath)) {
1357
- existingIgnore = fs.readFileSync(gitignorePath, 'utf8');
1358
- }
22
+ // Import utilities
23
+ const gitUtils = require('../src/utils/git-utils');
24
+ const versionCheck = require('../src/utils/version-check');
25
+ const validators = require('../src/utils/validators');
26
+ const { AbapHttp } = require('../src/utils/abap-http');
27
+ const { loadConfig, getTransport, isAbapIntegrationEnabled } = require('../src/config');
1359
28
 
1360
- let updated = false;
1361
- let newIgnoreContent = existingIgnore;
1362
-
1363
- for (const entry of ignoreEntries) {
1364
- if (!newIgnoreContent.includes(entry)) {
1365
- if (newIgnoreContent && !newIgnoreContent.endsWith('\n')) {
1366
- newIgnoreContent += '\n';
1367
- }
1368
- newIgnoreContent += entry + '\n';
1369
- updated = true;
1370
- }
1371
- }
1372
-
1373
- if (updated) {
1374
- fs.writeFileSync(gitignorePath, newIgnoreContent);
1375
- console.log(`✅ Updated .gitignore`);
1376
- } else {
1377
- fs.writeFileSync(gitignorePath, newIgnoreContent);
1378
- console.log(`✅ .gitignore already up to date`);
1379
- }
1380
-
1381
- // Copy CLAUDE.md
1382
- const claudeMdPath = pathModule.join(__dirname, '..', 'abap', 'CLAUDE.md');
1383
- const localClaudeMdPath = pathModule.join(process.cwd(), 'CLAUDE.md');
1384
- try {
1385
- if (fs.existsSync(claudeMdPath)) {
1386
- fs.copyFileSync(claudeMdPath, localClaudeMdPath);
1387
- console.log(`✅ Created CLAUDE.md`);
1388
- } else {
1389
- console.log(`⚠️ CLAUDE.md not found in abap/ directory`);
1390
- }
1391
- } catch (error) {
1392
- console.error(`Error copying CLAUDE.md: ${error.message}`);
1393
- }
1394
-
1395
- // Copy copilot-instructions.md for GitHub Copilot
1396
- const copilotMdPath = pathModule.join(__dirname, '..', 'abap', '.github', 'copilot-instructions.md');
1397
- const githubDir = pathModule.join(process.cwd(), '.github');
1398
- const localCopilotMdPath = pathModule.join(githubDir, 'copilot-instructions.md');
1399
- try {
1400
- if (fs.existsSync(copilotMdPath)) {
1401
- // Ensure .github directory exists
1402
- if (!fs.existsSync(githubDir)) {
1403
- fs.mkdirSync(githubDir, { recursive: true });
1404
- }
1405
- fs.copyFileSync(copilotMdPath, localCopilotMdPath);
1406
- console.log(`✅ Created .github/copilot-instructions.md`);
1407
- } else {
1408
- console.log(`⚠️ copilot-instructions.md not found in abap/ directory`);
1409
- }
1410
- } catch (error) {
1411
- console.error(`Error copying copilot-instructions.md: ${error.message}`);
1412
- }
1413
-
1414
- // Copy guidelines folder to project root
1415
- const guidelinesSrcPath = pathModule.join(__dirname, '..', 'abap', 'guidelines');
1416
- const guidelinesDestPath = pathModule.join(process.cwd(), 'guidelines');
1417
- try {
1418
- if (fs.existsSync(guidelinesSrcPath)) {
1419
- if (!fs.existsSync(guidelinesDestPath)) {
1420
- // Create guidelines directory
1421
- fs.mkdirSync(guidelinesDestPath, { recursive: true });
1422
- // Copy all files from guidelines folder
1423
- const files = fs.readdirSync(guidelinesSrcPath);
1424
- for (const file of files) {
1425
- if (file.endsWith('.md')) {
1426
- fs.copyFileSync(
1427
- pathModule.join(guidelinesSrcPath, file),
1428
- pathModule.join(guidelinesDestPath, file)
1429
- );
1430
- }
1431
- }
1432
- console.log(`✅ Created guidelines/ (${files.filter(f => f.endsWith('.md')).length} files)`);
1433
- } else {
1434
- console.log(`⚠️ guidelines/ already exists, skipped`);
1435
- }
1436
- } else {
1437
- console.log(`⚠️ guidelines folder not found in abap/ directory`);
1438
- }
1439
- } catch (error) {
1440
- console.error(`Error copying guidelines: ${error.message}`);
1441
- }
1442
-
1443
- // Create folder
1444
- const folderPath = pathModule.join(process.cwd(), folder);
1445
- try {
1446
- if (!fs.existsSync(folderPath)) {
1447
- fs.mkdirSync(folderPath, { recursive: true });
1448
- console.log(`✅ Created folder: ${folder}`);
1449
-
1450
- // Create .gitkeep
1451
- const gitkeepPath = pathModule.join(folderPath, '.gitkeep');
1452
- fs.writeFileSync(gitkeepPath, '');
1453
- } else {
1454
- console.log(`✅ Folder already exists: ${folder}`);
1455
- }
1456
- } catch (error) {
1457
- console.error(`Error creating folder: ${error.message}`);
1458
- }
1459
-
1460
- console.log(`
1461
- 📋 Next steps:
1462
- 1. Edit .abapGitAgent with your ABAP credentials (host, user, password)
1463
- 2. Run 'abapgit-agent create --import' to create online repository
1464
- 3. Run 'abapgit-agent pull' to activate objects
1465
- `);
1466
- }
1467
-
1468
- /**
1469
- * Check agent health
1470
- */
1471
- async function healthCheck() {
1472
- try {
1473
- const result = await request('GET', '/sap/bc/z_abapgit_agent/health');
1474
- console.log(JSON.stringify(result, null, 2));
1475
- return result;
1476
- } catch (error) {
1477
- console.error(`Health check failed: ${error.message}`);
1478
- process.exit(1);
1479
- }
1480
- }
29
+ // Get terminal width for responsive table
30
+ const getTermWidth = () => process.stdout.columns || 80;
31
+ const TERM_WIDTH = getTermWidth();
1481
32
 
1482
- /**
1483
- * Main CLI
1484
- */
1485
33
  async function main() {
1486
34
  const args = process.argv.slice(2);
1487
35
  const command = args[0];
1488
36
 
1489
- // Check if ABAP integration is enabled for this repo
1490
- // (skip for init, help, ref commands - these don't need ABAP connection)
1491
- const noAbapRequired = ['init', 'help', '--help', '-h', 'ref'];
1492
- if (command && !noAbapRequired.includes(command)) {
1493
- if (!isAbapIntegrationEnabled()) {
37
+ // Load command modules (Phase 2 refactoring)
38
+ const commandModules = {
39
+ help: require('../src/commands/help'),
40
+ health: require('../src/commands/health'),
41
+ status: require('../src/commands/status'),
42
+ create: require('../src/commands/create'),
43
+ delete: require('../src/commands/delete'),
44
+ import: require('../src/commands/import'),
45
+ inspect: require('../src/commands/inspect'),
46
+ unit: require('../src/commands/unit'),
47
+ syntax: require('../src/commands/syntax'),
48
+ tree: require('../src/commands/tree'),
49
+ list: require('../src/commands/list'),
50
+ view: require('../src/commands/view'),
51
+ preview: require('../src/commands/preview'),
52
+ where: require('../src/commands/where'),
53
+ ref: require('../src/commands/ref'),
54
+ init: require('../src/commands/init'),
55
+ pull: require('../src/commands/pull')
56
+ };
57
+
58
+ // Check if this is a modular command
59
+ if (commandModules[command] || command === '--help' || command === '-h') {
60
+ const cmd = command === '--help' || command === '-h' ? commandModules.help : commandModules[command];
61
+
62
+ // Check if command requires ABAP configuration
63
+ if (cmd.requiresAbapConfig && !isAbapIntegrationEnabled()) {
1494
64
  console.log(`
1495
65
  ⚠️ ABAP Git Agent not configured for this repository.
1496
66
 
@@ -1510,1397 +80,36 @@ To enable integration:
1510
80
  `);
1511
81
  process.exit(1);
1512
82
  }
1513
- }
1514
-
1515
- // Version compatibility check for commands that interact with ABAP
1516
- const abapCommands = ['create', 'delete', 'import', 'pull', 'inspect', 'unit', 'tree', 'view', 'preview', 'list'];
1517
- if (command && abapCommands.includes(command)) {
1518
- await checkVersionCompatibility();
1519
- }
1520
-
1521
- try {
1522
- switch (command) {
1523
- case 'init':
1524
- await runInit(args);
1525
- return; // Don't check ABAP integration for init
1526
-
1527
- case 'create': {
1528
- const helpIndex = args.findIndex(a => a === '--help' || a === '-h');
1529
-
1530
- // Show help if requested
1531
- if (helpIndex !== -1) {
1532
- console.log(`
1533
- Usage:
1534
- abapgit-agent create
1535
-
1536
- Description:
1537
- Create abapGit online repository in ABAP system.
1538
- Auto-detects URL from git remote and package from .abapGitAgent.
1539
-
1540
- Prerequisites:
1541
- - Run "abapgit-agent init" first
1542
- - Edit .abapGitAgent with credentials (host, user, password)
1543
-
1544
- Examples:
1545
- abapgit-agent create # Create repo in ABAP
1546
- `);
1547
- return;
1548
- }
1549
-
1550
- // Get parameters from config
1551
- const config = loadConfig();
1552
- const repoUrl = getGitRemoteUrl();
1553
-
1554
- if (!repoUrl) {
1555
- console.error('Error: No git remote configured. Please configure a remote origin.');
1556
- process.exit(1);
1557
- }
1558
-
1559
- if (!config.package) {
1560
- console.error('Error: Package not configured. Run "abapgit-agent init" first or set package in .abapGitAgent.');
1561
- process.exit(1);
1562
- }
1563
-
1564
- const branch = getGitBranch();
1565
-
1566
- // Extract repo name from git URL
1567
- const repoName = repoUrl.split('/').pop().replace('.git', '');
1568
-
1569
- console.log(`\n🚀 Creating online repository`);
1570
- console.log(` URL: ${repoUrl}`);
1571
- console.log(` Package: ${config.package}`);
1572
- console.log(` Folder: ${config.folder || '/src/'}`);
1573
- console.log(` Name: ${repoName}`);
1574
- console.log(` Branch: ${branch}`);
1575
-
1576
- const csrfToken = await fetchCsrfToken(config);
1577
-
1578
- const data = {
1579
- url: repoUrl,
1580
- package: config.package,
1581
- name: repoName,
1582
- branch: branch,
1583
- folder: config.folder || '/src/'
1584
- };
1585
-
1586
- if (config.gitUsername) {
1587
- data.username = config.gitUsername;
1588
- }
1589
-
1590
- if (config.gitPassword) {
1591
- data.password = config.gitPassword;
1592
- }
1593
-
1594
- const result = await request('POST', '/sap/bc/z_abapgit_agent/create', data, { csrfToken });
1595
-
1596
- console.log('\n');
1597
-
1598
- // Handle uppercase keys from ABAP
1599
- const success = result.SUCCESS || result.success;
1600
- const repoKey = result.REPO_KEY || result.repo_key;
1601
- const createdRepoName = result.REPO_NAME || result.repo_name;
1602
- const message = result.MESSAGE || result.message;
1603
- const error = result.ERROR || result.error;
1604
-
1605
- if (success === 'X' || success === true) {
1606
- console.log(`✅ Repository created successfully!`);
1607
- console.log(` URL: ${repoUrl}`);
1608
- console.log(` Package: ${config.package}`);
1609
- console.log(` Name: ${createdRepoName || repoName}`);
1610
- } else {
1611
- console.log(`❌ Failed to create repository`);
1612
- console.log(` Error: ${error || message || 'Unknown error'}`);
1613
- process.exit(1);
1614
- }
1615
- break;
1616
- }
1617
-
1618
- case 'delete': {
1619
- const helpIndex = args.findIndex(a => a === '--help' || a === '-h');
1620
-
1621
- // Show help if requested
1622
- if (helpIndex !== -1) {
1623
- console.log(`
1624
- Usage:
1625
- abapgit-agent delete
1626
-
1627
- Description:
1628
- Delete abapGit online repository from ABAP system.
1629
- Auto-detects URL from git remote of current directory.
1630
-
1631
- Prerequisites:
1632
- - Run "abapgit-agent create" first
1633
-
1634
- Examples:
1635
- abapgit-agent delete # Delete repo for current git remote
1636
- `);
1637
- return;
1638
- }
1639
-
1640
- // Get URL from current git remote
1641
- const config = loadConfig();
1642
- const repoUrl = getGitRemoteUrl();
1643
-
1644
- if (!repoUrl) {
1645
- console.error('Error: No git remote configured. Please configure a remote origin.');
1646
- process.exit(1);
1647
- }
1648
-
1649
- console.log(`\n🗑️ Deleting online repository`);
1650
- console.log(` URL: ${repoUrl}`);
1651
-
1652
- const csrfToken = await fetchCsrfToken(config);
1653
-
1654
- const data = {
1655
- url: repoUrl
1656
- };
1657
-
1658
- const result = await request('POST', '/sap/bc/z_abapgit_agent/delete', data, { csrfToken });
1659
-
1660
- console.log('\n');
1661
-
1662
- // Handle uppercase keys from ABAP
1663
- const success = result.SUCCESS || result.success;
1664
- const repoKey = result.REPO_KEY || result.repo_key;
1665
- const message = result.MESSAGE || result.message;
1666
- const error = result.ERROR || result.error;
1667
-
1668
- if (success === 'X' || success === true) {
1669
- console.log(`✅ Repository deleted successfully!`);
1670
- console.log(` Key: ${repoKey}`);
1671
- } else {
1672
- console.log(`❌ Failed to delete repository`);
1673
- console.log(` Error: ${error || message || 'Unknown error'}`);
1674
- process.exit(1);
1675
- }
1676
- break;
1677
- }
1678
-
1679
- case 'import': {
1680
- const helpIndex = args.findIndex(a => a === '--help' || a === '-h');
1681
-
1682
- // Show help if requested
1683
- if (helpIndex !== -1) {
1684
- console.log(`
1685
- Usage:
1686
- abapgit-agent import [--message <message>]
1687
-
1688
- Description:
1689
- Import existing objects from package to git repository.
1690
- Uses the git remote URL to find the abapGit online repository.
1691
-
1692
- Prerequisites:
1693
- - Run "abapgit-agent create" first or create repository in abapGit UI
1694
- - Package must have objects to import
1695
-
1696
- Options:
1697
- --message Commit message (default: "feat: initial import from ABAP package <package>")
1698
-
1699
- Examples:
1700
- abapgit-agent import
1701
- abapgit-agent import --message "Initial import from SAP"
1702
- `);
1703
- return;
1704
- }
1705
-
1706
- // Get parameters from config
1707
- const config = loadConfig();
1708
- const repoUrl = getGitRemoteUrl();
1709
-
1710
- if (!repoUrl) {
1711
- console.error('Error: No git remote configured. Please configure a remote origin.');
1712
- process.exit(1);
1713
- }
1714
-
1715
- const messageArgIndex = args.indexOf('--message');
1716
- let commitMessage = null;
1717
- if (messageArgIndex !== -1 && messageArgIndex + 1 < args.length) {
1718
- commitMessage = args[messageArgIndex + 1];
1719
- }
1720
-
1721
- console.log(`\n📦 Importing objects to git repository`);
1722
- console.log(` URL: ${repoUrl}`);
1723
- if (commitMessage) {
1724
- console.log(` Message: ${commitMessage}`);
1725
- }
1726
-
1727
- const csrfToken = await fetchCsrfToken(config);
1728
-
1729
- const data = {
1730
- url: repoUrl
1731
- };
1732
-
1733
- if (commitMessage) {
1734
- data.message = commitMessage;
1735
- }
1736
-
1737
- if (config.gitUsername) {
1738
- data.username = config.gitUsername;
1739
- }
1740
-
1741
- if (config.gitPassword) {
1742
- data.password = config.gitPassword;
1743
- }
1744
-
1745
- const result = await request('POST', '/sap/bc/z_abapgit_agent/import', data, { csrfToken });
1746
-
1747
- console.log('\n');
1748
-
1749
- // Handle uppercase keys from ABAP
1750
- const success = result.SUCCESS || result.success;
1751
- const filesStaged = result.FILES_STAGED || result.files_staged;
1752
- const abapCommitMessage = result.COMMIT_MESSAGE || result.commit_message;
1753
- const error = result.ERROR || result.error;
1754
-
1755
- if (success === 'X' || success === true) {
1756
- console.log(`✅ Objects imported successfully!`);
1757
- console.log(` Files staged: ${filesStaged}`);
1758
- console.log(` Commit: ${commitMessage || abapCommitMessage}`);
1759
- } else {
1760
- console.log(`❌ Import failed`);
1761
- console.log(` Error: ${error || resultMessage || 'Unknown error'}`);
1762
- process.exit(1);
1763
- }
1764
- break;
1765
- }
1766
-
1767
- case 'pull':
1768
- const urlArgIndex = args.indexOf('--url');
1769
- const branchArgIndex = args.indexOf('--branch');
1770
- const filesArgIndex = args.indexOf('--files');
1771
- const transportArgIndex = args.indexOf('--transport');
1772
-
1773
- // Auto-detect git remote URL if not provided
1774
- let gitUrl = urlArgIndex !== -1 ? args[urlArgIndex + 1] : null;
1775
- let branch = branchArgIndex !== -1 ? args[branchArgIndex + 1] : getGitBranch();
1776
- let files = null;
1777
-
1778
- // Transport: CLI arg takes priority, then config/environment, then null
1779
- let transportRequest = null;
1780
- if (transportArgIndex !== -1 && transportArgIndex + 1 < args.length) {
1781
- // Explicit --transport argument
1782
- transportRequest = args[transportArgIndex + 1];
1783
- } else {
1784
- // Fall back to config or environment variable
1785
- transportRequest = getTransport();
1786
- }
1787
-
1788
- if (filesArgIndex !== -1 && filesArgIndex + 1 < args.length) {
1789
- files = args[filesArgIndex + 1].split(',').map(f => f.trim());
1790
- }
1791
-
1792
- if (!gitUrl) {
1793
- gitUrl = getGitRemoteUrl();
1794
- if (!gitUrl) {
1795
- console.error('Error: Not in a git repository or no remote configured.');
1796
- console.error('Either run from a git repo, or specify --url <git-url>');
1797
- process.exit(1);
1798
- }
1799
- console.log(`📌 Auto-detected git remote: ${gitUrl}`);
1800
- }
1801
-
1802
- await pull(gitUrl, branch, files, transportRequest);
1803
- break;
1804
-
1805
- case 'health':
1806
- await healthCheck();
1807
- break;
1808
-
1809
- case 'status':
1810
- if (isAbapIntegrationEnabled()) {
1811
- console.log('✅ ABAP Git Agent is ENABLED');
1812
- console.log(' Config location:', pathModule.join(process.cwd(), '.abapGitAgent'));
1813
-
1814
- // Check if repo exists in ABAP
1815
- const config = loadConfig();
1816
- const repoUrl = getGitRemoteUrl();
1817
-
1818
- if (repoUrl) {
1819
- try {
1820
- const csrfToken = await fetchCsrfToken(config);
1821
- const result = await request('POST', '/sap/bc/z_abapgit_agent/status', { url: repoUrl }, { csrfToken });
1822
-
1823
- const status = result.status || result.STATUS || result.SUCCESS;
1824
- if (status === 'Found' || status === 'X' || status === true) {
1825
- console.log(' Repository: ✅ Created');
1826
- console.log(` Package: ${result.PACKAGE || result.package}`);
1827
- console.log(` URL: ${repoUrl}`);
1828
- console.log(` Key: ${result.REPO_KEY || result.repo_key}`);
1829
- } else {
1830
- console.log(' Repository: ❌ Not created in ABAP system');
1831
- console.log(` URL: ${repoUrl}`);
1832
- console.log(' Run "abapgit-agent create" to create repository');
1833
- }
1834
- } catch (err) {
1835
- console.log(' Repository: ⚠️ Unable to check status');
1836
- console.log(' Error:', err.message);
1837
- }
1838
- } else {
1839
- console.log(' No git remote configured');
1840
- }
1841
- } else {
1842
- console.log('❌ ABAP Git Agent is NOT configured');
1843
- }
1844
- break;
1845
-
1846
- case 'inspect': {
1847
- const filesArgIndex = args.indexOf('--files');
1848
- if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
1849
- console.error('Error: --files parameter required');
1850
- console.error('Usage: abapgit-agent inspect --files <file1>,<file2>,... [--variant <check-variant>]');
1851
- console.error('Example: abapgit-agent inspect --files zcl_my_class.clas.abap');
1852
- console.error('Example: abapgit-agent inspect --files zcl_my_class.clas.abap --variant ALL_CHECKS');
1853
- process.exit(1);
1854
- }
1855
-
1856
- const filesSyntaxCheck = args[filesArgIndex + 1].split(',').map(f => f.trim());
1857
-
1858
- // Parse optional --variant parameter
1859
- const variantArgIndex = args.indexOf('--variant');
1860
- const variant = variantArgIndex !== -1 ? args[variantArgIndex + 1] : null;
1861
-
1862
- console.log(`\n Inspect for ${filesSyntaxCheck.length} file(s)`);
1863
- if (variant) {
1864
- console.log(` Using variant: ${variant}`);
1865
- }
1866
- console.log('');
1867
-
1868
- const config = loadConfig();
1869
- const csrfToken = await fetchCsrfToken(config);
1870
-
1871
- // Send all files in one request
1872
- const results = await inspectAllFiles(filesSyntaxCheck, csrfToken, config, variant);
1873
-
1874
- // Process results
1875
- for (const result of results) {
1876
- await processInspectResult(result);
1877
- }
1878
- break;
1879
- }
1880
-
1881
- case 'unit': {
1882
- const filesArgIndex = args.indexOf('--files');
1883
- if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
1884
- console.error('Error: --files parameter required');
1885
- console.error('Usage: abapgit-agent unit --files <file1>,<file2>,... [--coverage]');
1886
- console.error('Example: abapgit-agent unit --files zcl_my_test.clas.abap');
1887
- console.error('Example: abapgit-agent unit --files zcl_my_test.clas.abap --coverage');
1888
- process.exit(1);
1889
- }
1890
-
1891
- const files = args[filesArgIndex + 1].split(',').map(f => f.trim());
1892
-
1893
- // Check for coverage option
1894
- const coverage = args.includes('--coverage');
1895
-
1896
- console.log(`\n Running unit tests for ${files.length} file(s)${coverage ? ' (with coverage)' : ''}`);
1897
- console.log('');
1898
-
1899
- const config = loadConfig();
1900
- const csrfToken = await fetchCsrfToken(config);
1901
-
1902
- for (const sourceFile of files) {
1903
- await runUnitTestForFile(sourceFile, csrfToken, config, coverage);
1904
- }
1905
- break;
1906
- }
1907
-
1908
- case 'tree': {
1909
- const packageArgIndex = args.indexOf('--package');
1910
- if (packageArgIndex === -1) {
1911
- console.error('Error: --package parameter required');
1912
- console.error('Usage: abapgit-agent tree --package <package> [--depth <n>] [--include-types] [--json]');
1913
- console.error('Example: abapgit-agent tree --package ZMY_PACKAGE');
1914
- process.exit(1);
1915
- }
1916
-
1917
- // Check if package value is missing (happens when shell variable expands to empty)
1918
- if (packageArgIndex + 1 >= args.length) {
1919
- console.error('Error: --package parameter value is missing');
1920
- console.error('');
1921
- console.error('Tip: If you are using a shell variable, make sure to quote it:');
1922
- console.error(' abapgit-agent tree --package "$ZMY_PACKAGE"');
1923
- console.error(' or escape the $ character:');
1924
- console.error(' abapgit-agent tree --package \\$ZMY_PACKAGE');
1925
- console.error('');
1926
- console.error('Usage: abapgit-agent tree --package <package> [--depth <n>] [--include-types] [--json]');
1927
- console.error('Example: abapgit-agent tree --package ZMY_PACKAGE');
1928
- process.exit(1);
1929
- }
1930
-
1931
- const packageName = args[packageArgIndex + 1];
1932
-
1933
- // Check for empty/whitespace-only package name
1934
- if (!packageName || packageName.trim() === '') {
1935
- console.error('Error: --package parameter cannot be empty');
1936
- console.error('Usage: abapgit-agent tree --package <package> [--depth <n>] [--include-types] [--json]');
1937
- console.error('Example: abapgit-agent tree --package ZMY_PACKAGE');
1938
- process.exit(1);
1939
- }
1940
-
1941
- // Optional depth parameter
1942
- const depthArgIndex = args.indexOf('--depth');
1943
- let depth = 3;
1944
- if (depthArgIndex !== -1 && depthArgIndex + 1 < args.length) {
1945
- depth = parseInt(args[depthArgIndex + 1], 10);
1946
- if (isNaN(depth) || depth < 1) {
1947
- console.error('Error: --depth must be a positive number');
1948
- process.exit(1);
1949
- }
1950
- }
1951
-
1952
- // Optional include-types parameter (--include-objects is deprecated alias)
1953
- const includeTypes = args.includes('--include-types') || args.includes('--include-objects');
1954
-
1955
- // Optional json parameter
1956
- const jsonOutput = args.includes('--json');
1957
-
1958
- if (jsonOutput) {
1959
- const config = loadConfig();
1960
- const csrfToken = await fetchCsrfToken(config);
1961
- const result = await runTreeCommand(packageName, depth, includeTypes, csrfToken, config);
1962
- console.log(JSON.stringify(result, null, 2));
1963
- } else {
1964
- await displayTreeOutput(packageName, depth, includeTypes);
1965
- }
1966
- break;
1967
- }
1968
-
1969
- case 'list': {
1970
- const packageArgIndex = args.indexOf('--package');
1971
- if (packageArgIndex === -1) {
1972
- console.error('Error: --package parameter required');
1973
- console.error('Usage: abapgit-agent list --package <package> [--type <types>] [--name <pattern>] [--limit <n>] [--offset <n>] [--json]');
1974
- console.error('Example: abapgit-agent list --package $ZMY_PACKAGE');
1975
- console.error('Example: abapgit-agent list --package $ZMY_PACKAGE --type CLAS,INTF');
1976
- process.exit(1);
1977
- }
1978
-
1979
- // Check if package value is missing
1980
- if (packageArgIndex + 1 >= args.length) {
1981
- console.error('Error: --package parameter value is missing');
1982
- process.exit(1);
1983
- }
1984
-
1985
- const packageName = args[packageArgIndex + 1];
1986
83
 
1987
- // Validate package name
1988
- if (!packageName || packageName.trim() === '') {
1989
- console.error('Error: --package parameter cannot be empty');
1990
- process.exit(1);
1991
- }
84
+ // Load config if needed
85
+ const config = cmd.requiresAbapConfig ? loadConfig() : null;
1992
86
 
1993
- // Optional type parameter
1994
- const typeArgIndex = args.indexOf('--type');
1995
- const type = typeArgIndex !== -1 && typeArgIndex + 1 < args.length ? args[typeArgIndex + 1] : null;
1996
-
1997
- // Optional name pattern
1998
- const nameArgIndex = args.indexOf('--name');
1999
- const name = nameArgIndex !== -1 && nameArgIndex + 1 < args.length ? args[nameArgIndex + 1] : null;
2000
-
2001
- // Optional limit
2002
- const limitArgIndex = args.indexOf('--limit');
2003
- let limit = 100;
2004
- if (limitArgIndex !== -1 && limitArgIndex + 1 < args.length) {
2005
- limit = parseInt(args[limitArgIndex + 1], 10);
2006
- if (isNaN(limit) || limit < 1) {
2007
- console.error('Error: --limit must be a positive number');
2008
- process.exit(1);
2009
- }
2010
- if (limit > 1000) {
2011
- console.error('Error: --limit value too high (max: 1000)');
2012
- process.exit(1);
2013
- }
2014
- }
2015
-
2016
- // Optional offset
2017
- const offsetArgIndex = args.indexOf('--offset');
2018
- let offset = 0;
2019
- if (offsetArgIndex !== -1 && offsetArgIndex + 1 < args.length) {
2020
- offset = parseInt(args[offsetArgIndex + 1], 10);
2021
- if (isNaN(offset) || offset < 0) {
2022
- console.error('Error: --offset must be a non-negative number');
2023
- process.exit(1);
2024
- }
2025
- }
2026
-
2027
- // Optional json parameter
2028
- const jsonOutput = args.includes('--json');
2029
-
2030
- const config = loadConfig();
2031
- const csrfToken = await fetchCsrfToken(config);
2032
-
2033
- const data = {
2034
- package: packageName,
2035
- limit: limit,
2036
- offset: offset
2037
- };
2038
-
2039
- if (type) {
2040
- data.type = type;
2041
- }
2042
-
2043
- if (name) {
2044
- data.name = name;
2045
- }
2046
-
2047
- const result = await request('POST', '/sap/bc/z_abapgit_agent/list', data, { csrfToken });
2048
-
2049
- // Handle uppercase keys from ABAP
2050
- const success = result.SUCCESS || result.success;
2051
- const error = result.ERROR || result.error;
2052
- const objects = result.OBJECTS || result.objects || [];
2053
- const byType = result.BY_TYPE || result.by_type || [];
2054
- const total = result.TOTAL || result.total || 0;
2055
-
2056
- if (!success || error) {
2057
- console.error(`\n Error: ${error || 'Failed to list objects'}`);
2058
- process.exit(1);
2059
- }
2060
-
2061
- if (jsonOutput) {
2062
- console.log(JSON.stringify(result, null, 2));
2063
- } else {
2064
- // Display human-readable output
2065
- let title = `Objects in ${packageName}`;
2066
- if (type) {
2067
- title += ` (${type} only`;
2068
- if (total !== objects.length) {
2069
- title += `, Total: ${total}`;
2070
- }
2071
- title += ')';
2072
- } else if (total !== objects.length) {
2073
- title += ` (Total: ${total})`;
2074
- }
2075
- console.log(`\n${title}\n`);
2076
-
2077
- // Group objects by type
2078
- const objectsByType = {};
2079
- for (const obj of objects) {
2080
- const objType = (obj.TYPE || obj.type || '???').toUpperCase();
2081
- if (!objectsByType[objType]) {
2082
- objectsByType[objType] = [];
2083
- }
2084
- objectsByType[objType].push(obj.NAME || obj.name);
2085
- }
2086
-
2087
- // Display grouped objects
2088
- for (const objType of Object.keys(objectsByType).sort()) {
2089
- const objNames = objectsByType[objType];
2090
- console.log(` ${objType} (${objNames.length})`);
2091
- for (const objName of objNames) {
2092
- console.log(` ${objName}`);
2093
- }
2094
- console.log('');
2095
- }
2096
- }
2097
- break;
2098
- }
2099
-
2100
- case 'view': {
2101
- const objectsArgIndex = args.indexOf('--objects');
2102
- if (objectsArgIndex === -1 || objectsArgIndex + 1 >= args.length) {
2103
- console.error('Error: --objects parameter required');
2104
- console.error('Usage: abapgit-agent view --objects <obj1>,<obj2>,... [--type <type>] [--json]');
2105
- console.error('Example: abapgit-agent view --objects ZCL_MY_CLASS');
2106
- console.error('Example: abapgit-agent view --objects ZCL_CLASS1,ZCL_CLASS2 --type CLAS');
2107
- process.exit(1);
2108
- }
2109
-
2110
- const objects = args[objectsArgIndex + 1].split(',').map(o => o.trim().toUpperCase());
2111
- const typeArg = args.indexOf('--type');
2112
- const type = typeArg !== -1 ? args[typeArg + 1].toUpperCase() : null;
2113
- const jsonOutput = args.includes('--json');
2114
-
2115
- console.log(`\n Viewing ${objects.length} object(s)`);
2116
-
2117
- const config = loadConfig();
2118
- const csrfToken = await fetchCsrfToken(config);
2119
-
2120
- const data = {
2121
- objects: objects
2122
- };
2123
-
2124
- if (type) {
2125
- data.type = type;
2126
- }
2127
-
2128
- const result = await request('POST', '/sap/bc/z_abapgit_agent/view', data, { csrfToken });
2129
-
2130
- // Handle uppercase keys from ABAP
2131
- const success = result.SUCCESS || result.success;
2132
- const viewObjects = result.OBJECTS || result.objects || [];
2133
- const message = result.MESSAGE || result.message || '';
2134
- const error = result.ERROR || result.error;
2135
-
2136
- if (!success || error) {
2137
- console.error(`\n Error: ${error || 'Failed to view objects'}`);
2138
- break;
2139
- }
2140
-
2141
- if (jsonOutput) {
2142
- console.log(JSON.stringify(result, null, 2));
2143
- } else {
2144
- console.log(`\n ${message}`);
2145
- console.log('');
2146
-
2147
- for (let i = 0; i < viewObjects.length; i++) {
2148
- const obj = viewObjects[i];
2149
- const objName = obj.NAME || obj.name || `Object ${i + 1}`;
2150
- const objType = obj.TYPE || obj.type || '';
2151
- const objTypeText = obj.TYPE_TEXT || obj.type_text || '';
2152
- const description = obj.DESCRIPTION || obj.description || '';
2153
- const methods = obj.METHODS || obj.methods || [];
2154
- const components = obj.COMPONENTS || obj.components || [];
2155
- const notFound = obj.NOT_FOUND || obj.not_found || false;
2156
-
2157
- // Check if object was not found
2158
- if (notFound) {
2159
- console.log(` ❌ ${objName} (${objTypeText})`);
2160
- console.log(` Object not found: ${objName}`);
2161
- continue;
2162
- }
2163
-
2164
- console.log(` 📖 ${objName} (${objTypeText})`);
2165
- if (description) {
2166
- console.log(` ${description}`);
2167
- }
2168
-
2169
- // Display source code for classes, interfaces, CDS views, and programs/source includes
2170
- const source = obj.SOURCE || obj.source || '';
2171
- if (source && (objType === 'INTF' || objType === 'Interface' || objType === 'CLAS' || objType === 'Class' || objType === 'DDLS' || objType === 'CDS View' || objType === 'PROG' || objType === 'Program')) {
2172
- console.log('');
2173
- // Replace escaped newlines with actual newlines and display
2174
- const displaySource = source.replace(/\\n/g, '\n');
2175
- const lines = displaySource.split('\n');
2176
- for (const line of lines) {
2177
- console.log(` ${line}`);
2178
- }
2179
- }
2180
-
2181
- if (methods.length > 0) {
2182
- console.log(` Methods: ${methods.length}`);
2183
- for (const method of methods.slice(0, 5)) {
2184
- const name = method.NAME || method.name || '';
2185
- const visibility = method.VISIBILITY || method.visibility || '';
2186
- console.log(` - ${visibility} ${name}`);
2187
- }
2188
- if (methods.length > 5) {
2189
- console.log(` ... and ${methods.length - 5} more`);
2190
- }
2191
- }
2192
-
2193
- if (components.length > 0) {
2194
- // Check if this is a data element (DTEL) - show domain info in property format
2195
- if (objType === 'DTEL' || objType === 'Data Element') {
2196
- const propWidth = 18;
2197
- const valueWidth = 40;
2198
-
2199
- // Build separator with corners
2200
- const sep = '┌' + '─'.repeat(propWidth + 2) + '┬' + '─'.repeat(valueWidth + 2) + '┐';
2201
- const mid = '├' + '─'.repeat(propWidth + 2) + '┼' + '─'.repeat(valueWidth + 2) + '┤';
2202
- const end = '└' + '─'.repeat(propWidth + 2) + '┴' + '─'.repeat(valueWidth + 2) + '┘';
2203
-
2204
- // Helper to build row
2205
- const buildPropRow = (property, value) => {
2206
- return '│ ' + String(property || '').padEnd(propWidth) + ' │ ' +
2207
- String(value || '').substring(0, valueWidth).padEnd(valueWidth) + ' │';
2208
- };
2209
-
2210
- console.log(` DATA ELEMENT ${objName}:`);
2211
- console.log(sep);
2212
- console.log(buildPropRow('Property', 'Value'));
2213
- console.log(mid);
2214
-
2215
- // Collect properties from top-level fields and components
2216
- const domain = obj.DOMAIN || obj.domain || '';
2217
- const domainType = obj.DOMAIN_TYPE || obj.domain_type || '';
2218
- const domainLength = obj.DOMAIN_LENGTH || obj.domain_length || 0;
2219
- const domainDecimals = obj.DOMAIN_DECIMALS || obj.domain_decimals || 0;
2220
- const description = obj.DESCRIPTION || obj.description || '';
2221
-
2222
- if (domainType) {
2223
- console.log(buildPropRow('Data Type', domainType));
2224
- }
2225
- if (domainLength) {
2226
- console.log(buildPropRow('Length', String(domainLength)));
2227
- }
2228
- if (domainDecimals) {
2229
- console.log(buildPropRow('Decimals', String(domainDecimals)));
2230
- }
2231
- if (description) {
2232
- console.log(buildPropRow('Description', description));
2233
- }
2234
- if (domain) {
2235
- console.log(buildPropRow('Domain', domain));
2236
- }
2237
-
2238
- console.log(end);
2239
- } else if (objType === 'TTYP' || objType === 'Table Type') {
2240
- // Show TTYP details as simple text lines
2241
- console.log('');
2242
- for (const comp of components) {
2243
- const desc = comp.DESCRIPTION || comp.description || '';
2244
- if (desc) {
2245
- console.log(` ${desc}`);
2246
- }
2247
- }
2248
- } else {
2249
- // Build table display for TABL/STRU with Data Element and Description
2250
- const colWidths = {
2251
- field: 16, // Max field name length
2252
- key: 3,
2253
- type: 8,
2254
- length: 8,
2255
- dataelement: 30, // Max data element name length
2256
- description: 60, // Max field description length
2257
- };
2258
-
2259
- // Helper to truncate with ellipsis if needed
2260
- const truncate = (str, maxLen) => {
2261
- const s = String(str || '');
2262
- if (s.length <= maxLen) return s;
2263
- return s.substring(0, maxLen - 1) + '…';
2264
- };
2265
-
2266
- // Helper to build row
2267
- const buildRow = (field, key, type, length, dataelement, description) => {
2268
- return ' | ' + truncate(field, colWidths.field).padEnd(colWidths.field) + ' | ' + String(key || '').padEnd(colWidths.key) + ' | ' + truncate(type, colWidths.type).padEnd(colWidths.type) + ' | ' + String(length || '').padStart(colWidths.length) + ' | ' + truncate(dataelement, colWidths.dataelement).padEnd(colWidths.dataelement) + ' | ' + truncate(description, colWidths.description).padEnd(colWidths.description) + ' |';
2269
- };
2270
-
2271
- // Build separator line (matches row structure with | at ends and + between columns)
2272
- const sep = ' |' + '-'.repeat(colWidths.field + 2) + '+' +
2273
- '-'.repeat(colWidths.key + 2) + '+' +
2274
- '-'.repeat(colWidths.type + 2) + '+' +
2275
- '-'.repeat(colWidths.length + 2) + '+' +
2276
- '-'.repeat(colWidths.dataelement + 2) + '+' +
2277
- '-'.repeat(colWidths.description + 2) + '|';
2278
-
2279
- // Header
2280
- console.log(` TABLE ${objName}:`);
2281
- console.log(sep);
2282
- console.log(buildRow('Field', 'Key', 'Type', 'Length', 'Data Elem', 'Description'));
2283
- console.log(sep);
2284
-
2285
- // Rows
2286
- for (const comp of components) {
2287
- const key = comp.KEY || comp.key || false ? 'X' : '';
2288
- const dataelement = comp.DATAELEMENT || comp.dataelement || '';
2289
- const description = comp.DESCRIPTION || comp.description || '';
2290
- console.log(buildRow(comp.FIELD || comp.field, key, comp.TYPE || comp.type, comp.LENGTH || comp.length, dataelement, description));
2291
- }
2292
-
2293
- console.log(sep);
2294
- }
2295
- }
2296
-
2297
- console.log('');
2298
- }
2299
-
2300
- }
2301
- break;
2302
- }
2303
-
2304
- case 'preview': {
2305
- const objectsArgIndex = args.indexOf('--objects');
2306
- if (objectsArgIndex === -1 || objectsArgIndex + 1 >= args.length) {
2307
- console.error('Error: --objects parameter required');
2308
- console.error('Usage: abapgit-agent preview --objects <table1>,<view1>,... [--type <type>] [--limit <n>] [--offset <n>] [--where <condition>] [--columns <cols>] [--vertical] [--compact] [--json]');
2309
- console.error('Example: abapgit-agent preview --objects SFLIGHT');
2310
- console.error('Example: abapgit-agent preview --objects ZC_MY_CDS_VIEW --type DDLS');
2311
- console.error('Example: abapgit-agent preview --objects SFLIGHT --where "CARRID = \'AA\'"');
2312
- console.error('Example: abapgit-agent preview --objects SFLIGHT --offset 10 --limit 20');
2313
- process.exit(1);
2314
- }
2315
-
2316
- const objects = args[objectsArgIndex + 1].split(',').map(o => o.trim().toUpperCase());
2317
- const typeArg = args.indexOf('--type');
2318
- const type = typeArg !== -1 ? args[typeArg + 1].toUpperCase() : null;
2319
- const limitArg = args.indexOf('--limit');
2320
- const limitRaw = limitArg !== -1 ? args[limitArg + 1] : null;
2321
- const limitParsed = parseInt(limitRaw, 10);
2322
- const limit = limitRaw && !limitRaw.startsWith('--') && !isNaN(limitParsed) ? Math.max(1, limitParsed) : 100;
2323
- const offsetArg = args.indexOf('--offset');
2324
- const offsetRaw = offsetArg !== -1 ? args[offsetArg + 1] : null;
2325
- const offsetParsed = parseInt(offsetRaw, 10);
2326
- const offset = offsetRaw && !offsetRaw.startsWith('--') && !isNaN(offsetParsed) ? Math.max(0, offsetParsed) : 0;
2327
- const whereArg = args.indexOf('--where');
2328
- const where = whereArg !== -1 ? args[whereArg + 1] : null;
2329
- const columnsArg = args.indexOf('--columns');
2330
- const columns = columnsArg !== -1 ? args[columnsArg + 1].split(',').map(c => c.trim().toUpperCase()) : null;
2331
- const verticalOutput = args.includes('--vertical');
2332
- const compactOutput = args.includes('--compact');
2333
- const jsonOutput = args.includes('--json');
2334
-
2335
- console.log(`\n Previewing ${objects.length} object(s)`);
2336
-
2337
- const config = loadConfig();
2338
- const csrfToken = await fetchCsrfToken(config);
2339
-
2340
- const data = {
2341
- objects: objects,
2342
- limit: Math.min(Math.max(1, limit), 500),
2343
- offset: Math.max(0, offset)
2344
- };
2345
-
2346
- if (type) {
2347
- data.type = type;
2348
- }
2349
-
2350
- if (where) {
2351
- data.where = convertDatesInWhereClause(where);
2352
- }
2353
-
2354
- if (columns) {
2355
- data.columns = columns;
2356
- }
2357
-
2358
- const result = await request('POST', '/sap/bc/z_abapgit_agent/preview', data, { csrfToken });
2359
-
2360
- // Handle uppercase keys from ABAP
2361
- const success = result.SUCCESS || result.success;
2362
- const previewObjects = result.OBJECTS || result.objects || [];
2363
- const message = result.MESSAGE || result.message || '';
2364
- const error = result.ERROR || result.error;
2365
-
2366
- if (!success || error) {
2367
- console.error(`\n Error: ${error || 'Failed to preview objects'}`);
2368
- break;
2369
- }
2370
-
2371
- const pagination = result.PAGINATION || result.pagination || null;
2372
-
2373
- if (jsonOutput) {
2374
- console.log(JSON.stringify(result, null, 2));
2375
- } else {
2376
- // Build pagination message
2377
- let paginationMsg = '';
2378
- const paginationTotal = pagination ? (pagination.TOTAL || pagination.total || 0) : 0;
2379
- const paginationHasMore = pagination ? (pagination.HAS_MORE || pagination.has_more || false) : false;
2380
- if (pagination && paginationTotal > 0) {
2381
- // Handle case where offset exceeds total
2382
- if (offset >= paginationTotal) {
2383
- paginationMsg = ` (Offset ${offset} exceeds total ${paginationTotal})`;
2384
- } else {
2385
- const start = offset + 1;
2386
- const end = Math.min(offset + limit, paginationTotal);
2387
- paginationMsg = ` (Showing ${start}-${end} of ${paginationTotal})`;
2388
- if (paginationHasMore) {
2389
- paginationMsg += ` — Use --offset ${offset + limit} to see more`;
2390
- }
2391
- }
2392
- }
2393
-
2394
- console.log(`\n ${message}${paginationMsg}`);
2395
- console.log('');
2396
-
2397
- // Track if columns were explicitly specified
2398
- const columnsExplicit = columns !== null;
2399
-
2400
- for (let i = 0; i < previewObjects.length; i++) {
2401
- const obj = previewObjects[i];
2402
- const objName = obj.NAME || obj.name || `Object ${i + 1}`;
2403
- const objType = obj.TYPE || obj.type || '';
2404
- const objTypeText = obj.TYPE_TEXT || obj.type_text || '';
2405
- // Parse rows - could be a JSON string or array
2406
- let rows = obj.ROWS || obj.rows || [];
2407
- if (typeof rows === 'string') {
2408
- try {
2409
- rows = JSON.parse(rows);
2410
- } catch (e) {
2411
- rows = [];
2412
- }
2413
- }
2414
- const fields = obj.FIELDS || obj.fields || [];
2415
- const rowCount = obj.ROW_COUNT || obj.row_count || 0;
2416
- const totalRows = obj.TOTAL_ROWS || obj.total_rows || 0;
2417
- const notFound = obj.NOT_FOUND || obj.not_found || false;
2418
- const accessDenied = obj.ACCESS_DENIED || obj.access_denied || false;
2419
-
2420
- // Check if object was not found
2421
- if (notFound) {
2422
- console.log(` ❌ ${objName} (${objTypeText})`);
2423
- console.log(` Object not found: ${objName}`);
2424
- continue;
2425
- }
2426
-
2427
- // Check if access denied
2428
- if (accessDenied) {
2429
- console.log(` ❌ ${objName} (${objTypeText})`);
2430
- console.log(` Access denied to: ${objName}`);
2431
- continue;
2432
- }
2433
-
2434
- console.log(` 📊 Preview: ${objName} (${objTypeText})`);
2435
-
2436
- // Check for errors first
2437
- const objError = obj.ERROR || obj.error;
2438
- if (objError) {
2439
- console.log(` ❌ Error: ${objError}`);
2440
- continue;
2441
- }
2442
-
2443
- if (rows.length === 0) {
2444
- console.log(' No data found');
2445
- continue;
2446
- }
2447
-
2448
- // Get all unique field names from all rows
2449
- const allFields = new Set();
2450
- rows.forEach(row => {
2451
- Object.keys(row).forEach(key => allFields.add(key));
2452
- });
2453
- const allFieldNames = Array.from(allFields);
2454
-
2455
- // Display as table - use fields metadata only if --columns was explicitly specified
2456
- let fieldNames;
2457
- let columnsAutoSelected = false;
2458
- if (columnsExplicit && fields && fields.length > 0) {
2459
- // Use fields from metadata (filtered by explicit --columns)
2460
- fieldNames = fields.map(f => f.FIELD || f.field);
2461
- } else {
2462
- // Use all fields - let terminal handle wrapping if needed
2463
- // Terminal width detection is unreliable without a proper TTY
2464
- fieldNames = allFieldNames;
2465
- }
2466
-
2467
- // Calculate column widths - use reasonable defaults
2468
- const colWidths = {};
2469
- const maxColWidth = compactOutput ? 10 : 20;
2470
- fieldNames.forEach(field => {
2471
- let maxWidth = field.length;
2472
- rows.forEach(row => {
2473
- const value = String(row[field] || '');
2474
- maxWidth = Math.max(maxWidth, value.length);
2475
- });
2476
- // Cap at maxColWidth (truncates both headers and data in compact mode)
2477
- colWidths[field] = Math.min(maxWidth, maxColWidth);
2478
- });
2479
-
2480
- // Render output - either vertical or table
2481
- if (verticalOutput) {
2482
- // Vertical format: each field on its own line
2483
- rows.forEach((row, rowIndex) => {
2484
- if (rows.length > 1) {
2485
- console.log(`\n Row ${rowIndex + 1}:`);
2486
- console.log(' ' + '─'.repeat(30));
2487
- }
2488
- fieldNames.forEach(field => {
2489
- const value = String(row[field] || '');
2490
- console.log(` ${field}: ${value}`);
2491
- });
2492
- });
2493
- console.log('');
2494
- continue;
2495
- }
2496
-
2497
- // Build table header
2498
- let headerLine = ' ┌';
2499
- let separatorLine = ' ├';
2500
- fieldNames.forEach(field => {
2501
- const width = colWidths[field];
2502
- headerLine += '─'.repeat(width + 2) + '┬';
2503
- separatorLine += '─'.repeat(width + 2) + '┼';
2504
- });
2505
- headerLine = headerLine.slice(0, -1) + '┐';
2506
- separatorLine = separatorLine.slice(0, -1) + '┤';
2507
-
2508
- // Build header row
2509
- let headerRow = ' │';
2510
- fieldNames.forEach(field => {
2511
- const width = colWidths[field];
2512
- let displayField = String(field);
2513
- if (displayField.length > width) {
2514
- displayField = displayField.slice(0, width - 3) + '...';
2515
- }
2516
- headerRow += ' ' + displayField.padEnd(width) + ' │';
2517
- });
2518
-
2519
- console.log(headerLine);
2520
- console.log(headerRow);
2521
- console.log(separatorLine);
2522
-
2523
- // Build data rows
2524
- rows.forEach(row => {
2525
- let dataRow = ' │';
2526
- fieldNames.forEach(field => {
2527
- const width = colWidths[field];
2528
- const value = String(row[field] || '');
2529
- const displayValue = value.length > width ? value.slice(0, width - 3) + '...' : value;
2530
- dataRow += ' ' + displayValue.padEnd(width) + ' │';
2531
- });
2532
- console.log(dataRow);
2533
- });
2534
-
2535
- // Build bottom border
2536
- let bottomLine = ' └';
2537
- fieldNames.forEach(field => {
2538
- const width = colWidths[field];
2539
- bottomLine += '─'.repeat(width + 2) + '┴';
2540
- });
2541
- bottomLine = bottomLine.slice(0, -1) + '┘';
2542
- console.log(bottomLine);
2543
-
2544
- // Show row count
2545
- if (totalRows > rowCount) {
2546
- console.log(`\n Showing ${rowCount} of ${totalRows} rows`);
2547
- } else {
2548
- console.log(`\n ${rowCount} row(s)`);
2549
- }
2550
-
2551
- // Note about hidden columns only when --columns was explicitly specified
2552
- const columnsDisplayed = fieldNames.length;
2553
- let columnsHidden = [];
2554
-
2555
- if (columnsExplicit) {
2556
- columnsHidden = obj.COLUMNS_HIDDEN || obj.columns_hidden || [];
2557
- if (columnsHidden.length > 0) {
2558
- console.log(`\n ⚠️ ${columnsHidden.length} more columns hidden (${columnsHidden.join(', ')})`);
2559
- console.log(' Use --json for full data');
2560
- }
2561
- }
2562
-
2563
- console.log('');
2564
- }
2565
- }
2566
- break;
2567
- }
2568
-
2569
- case 'where': {
2570
- const objectsArgIndex = args.indexOf('--objects');
2571
- if (objectsArgIndex === -1 || objectsArgIndex + 1 >= args.length) {
2572
- console.error('Error: --objects parameter required');
2573
- console.error('Usage: abapgit-agent where --objects <obj1>,<obj2>,... [--type <type>] [--limit <n>] [--offset <n>] [--json]');
2574
- console.error('Example: abapgit-agent where --objects ZCL_MY_CLASS');
2575
- console.error('Example: abapgit-agent where --objects ZIF_MY_INTERFACE');
2576
- console.error('Example: abapgit-agent where --objects CL_SUT_AUNIT_RUNNER --limit 20');
2577
- console.error('Example: abapgit-agent where --objects CL_SUT_AUNIT_RUNNER --offset 100');
2578
- process.exit(1);
2579
- }
2580
-
2581
- const objects = args[objectsArgIndex + 1].split(',').map(o => o.trim().toUpperCase());
2582
- const typeArg = args.indexOf('--type');
2583
- const type = typeArg !== -1 ? args[typeArg + 1].toUpperCase() : null;
2584
- const limitArg = args.indexOf('--limit');
2585
- const limitRaw = limitArg !== -1 ? args[limitArg + 1] : null;
2586
- const limitParsed = parseInt(limitRaw, 10);
2587
- const limit = limitRaw && !limitRaw.startsWith('--') && !isNaN(limitParsed) ? Math.max(1, limitParsed) : 100;
2588
- const offsetArg = args.indexOf('--offset');
2589
- const offsetRaw = offsetArg !== -1 ? args[offsetArg + 1] : null;
2590
- const offsetParsed = parseInt(offsetRaw, 10);
2591
- const offset = offsetRaw && !offsetRaw.startsWith('--') && !isNaN(offsetParsed) ? Math.max(0, offsetParsed) : 0;
2592
- const jsonOutput = args.includes('--json');
2593
-
2594
- console.log(`\n Where-used list for ${objects.length} object(s)`);
2595
-
2596
- const config = loadConfig();
2597
- const csrfToken = await fetchCsrfToken(config);
2598
-
2599
- const data = {
2600
- objects: objects,
2601
- limit: Math.min(Math.max(1, limit), 500),
2602
- offset: Math.max(0, offset)
2603
- };
2604
-
2605
- if (type) {
2606
- data.type = type;
2607
- }
2608
-
2609
- const result = await request('POST', '/sap/bc/z_abapgit_agent/where', data, { csrfToken });
2610
-
2611
- // Handle uppercase keys from ABAP
2612
- const success = result.SUCCESS || result.success;
2613
- const whereObjects = result.OBJECTS || result.objects || [];
2614
- const message = result.MESSAGE || result.message || '';
2615
- const error = result.ERROR || result.error;
2616
- const pagination = result.PAGINATION || result.pagination || null;
2617
-
2618
- if (!success || error) {
2619
- console.error(`\n Error: ${error || 'Failed to get where-used list'}`);
2620
- break;
2621
- }
2622
-
2623
- if (jsonOutput) {
2624
- console.log(JSON.stringify(result, null, 2));
2625
- } else {
2626
- // Build pagination message
2627
- let paginationMsg = '';
2628
- const paginationTotal = pagination.TOTAL || pagination.total || 0;
2629
- const paginationHasMore = pagination.HAS_MORE || pagination.has_more || false;
2630
- if (pagination && paginationTotal > 0) {
2631
- // Handle case where offset exceeds total
2632
- if (offset >= paginationTotal) {
2633
- paginationMsg = ` (Offset ${offset} exceeds total ${paginationTotal})`;
2634
- } else {
2635
- const start = offset + 1;
2636
- const end = Math.min(offset + limit, paginationTotal);
2637
- paginationMsg = ` (Showing ${start}-${end} of ${paginationTotal})`;
2638
- if (paginationHasMore) {
2639
- paginationMsg += ` — Use --offset ${offset + limit} to see more`;
2640
- }
2641
- }
2642
- }
2643
-
2644
- console.log(`\n ${message}${paginationMsg}`);
2645
- console.log('');
2646
-
2647
- for (let i = 0; i < whereObjects.length; i++) {
2648
- const obj = whereObjects[i];
2649
- const objName = obj.NAME || obj.name || `Object ${i + 1}`;
2650
- const objType = obj.TYPE || obj.type || '';
2651
- const error = obj.ERROR || obj.error || '';
2652
- const references = obj.REFERENCES || obj.references || [];
2653
- const count = obj.COUNT || obj.count || 0;
2654
-
2655
- // Handle object not found error
2656
- if (error) {
2657
- console.log(` ❌ ${objName} (${objType})`);
2658
- console.log(` ${error}`);
2659
- console.log('');
2660
- continue;
2661
- }
2662
-
2663
- if (count === 0) {
2664
- console.log(` ❌ ${objName} (${objType})`);
2665
- console.log(` No references found`);
2666
- console.log('');
2667
- continue;
2668
- }
2669
-
2670
- console.log(` 🔍 ${objName} (${objType})`);
2671
- console.log(` Found ${count} reference(s):`);
2672
- console.log('');
2673
-
2674
- // Display references - one line format: include → method (type) or include (type)
2675
- for (let j = 0; j < references.length; j++) {
2676
- const ref = references[j];
2677
- const includeName = ref.INCLUDE_NAME || ref.include_name || '';
2678
- const includeType = ref.INCLUDE_TYPE || ref.include_type || '';
2679
- const methodName = ref.METHOD_NAME || ref.method_name || '';
2680
-
2681
- let line;
2682
- if (methodName) {
2683
- line = ` ${j + 1}. ${includeName} → ${methodName} (${includeType})`;
2684
- } else {
2685
- line = ` ${j + 1}. ${includeName} (${includeType})`;
2686
- }
2687
- console.log(line);
2688
- }
2689
- console.log('');
2690
- }
2691
- }
2692
- break;
2693
- }
2694
-
2695
- case 'ref': {
2696
- const refSearch = require('../src/ref-search');
2697
- const topicIndex = args.indexOf('--topic');
2698
- const cloneIndex = args.indexOf('--clone');
2699
- const nameIndex = args.indexOf('--name');
2700
- const listTopics = args.includes('--list-topics') || args.includes('-l');
2701
- const listRepos = args.includes('--list-repos') || args.includes('-r');
2702
- const jsonOutput = args.includes('--json');
2703
-
2704
- // Handle --clone option
2705
- if (cloneIndex !== -1 && cloneIndex + 1 < args.length) {
2706
- const repoUrl = args[cloneIndex + 1];
2707
- const name = nameIndex !== -1 && nameIndex + 1 < args.length ? args[nameIndex + 1] : null;
2708
- const result = refSearch.cloneRepository(repoUrl, name);
2709
- if (jsonOutput) {
2710
- console.log(JSON.stringify(result, null, 2));
2711
- } else {
2712
- refSearch.displayCloneResult(result);
2713
- }
2714
- break;
2715
- }
2716
-
2717
- if (listRepos) {
2718
- const result = await refSearch.listRepositories();
2719
- if (jsonOutput) {
2720
- console.log(JSON.stringify(result, null, 2));
2721
- } else {
2722
- refSearch.displayRepositories(result);
2723
- }
2724
- break;
2725
- }
2726
-
2727
- if (listTopics) {
2728
- const result = await refSearch.listTopics();
2729
- if (jsonOutput) {
2730
- console.log(JSON.stringify(result, null, 2));
2731
- } else {
2732
- refSearch.displayTopics(result);
2733
- }
2734
- break;
2735
- }
2736
-
2737
- if (topicIndex !== -1 && topicIndex + 1 < args.length) {
2738
- const topic = args[topicIndex + 1];
2739
- const result = await refSearch.getTopic(topic);
2740
- if (jsonOutput) {
2741
- console.log(JSON.stringify(result, null, 2));
2742
- } else {
2743
- refSearch.displayTopic(result);
2744
- }
2745
- break;
2746
- }
2747
-
2748
- // Pattern search (default)
2749
- const patternIndex = args.findIndex((arg, idx) => idx > 0 && !arg.startsWith('--'));
2750
- if (patternIndex === -1) {
2751
- console.error('Error: No pattern specified');
2752
- console.error('');
2753
- console.error('Usage:');
2754
- console.error(' abapgit-agent ref <pattern> Search for pattern');
2755
- console.error(' abapgit-agent ref --topic <name> View specific topic');
2756
- console.error(' abapgit-agent ref --list-topics List available topics');
2757
- console.error(' abapgit-agent ref --list-repos List reference repositories');
2758
- console.error(' abapgit-agent ref --clone <repo> Clone a repository');
2759
- console.error('');
2760
- console.error('Examples:');
2761
- console.error(' abapgit-agent ref "CORRESPONDING"');
2762
- console.error(' abapgit-agent ref --topic exceptions');
2763
- console.error(' abapgit-agent ref --list-topics');
2764
- console.error(' abapgit-agent ref --list-repos');
2765
- console.error(' abapgit-agent ref --clone SAP-samples/abap-cheat-sheets');
2766
- console.error(' abapgit-agent ref --clone https://github.com/abapGit/abapGit.git');
2767
- process.exit(1);
2768
- }
2769
-
2770
- const pattern = args[patternIndex];
2771
- const result = await refSearch.searchPattern(pattern);
2772
-
2773
- if (jsonOutput) {
2774
- console.log(JSON.stringify(result, null, 2));
2775
- } else {
2776
- refSearch.displaySearchResults(result);
2777
- }
2778
- break;
2779
- }
2780
-
2781
- case 'help':
2782
- case '--help':
2783
- case '-h':
2784
- console.log(`
2785
- ABAP Git Agent
2786
-
2787
- Usage:
2788
- abapgit-agent <command> [options]
2789
-
2790
- Commands:
2791
- init --folder <folder> --package <package>
2792
- Initialize local configuration for an existing git repository.
2793
- init --update
2794
- Update existing files (CLAUDE.md, copilot-instructions.md, guidelines) to latest version.
2795
-
2796
- create
2797
- Create abapGit online repository in ABAP system.
2798
- Auto-detects URL from git remote and package from .abapGitAgent.
2799
-
2800
- import [--message <message>]
2801
- Import existing objects from package to git repository.
2802
- Uses the git remote URL to find the abapGit online repository.
2803
-
2804
- pull [--url <git-url>] [--branch <branch>] [--files <file1,file2,...>] [--transport <request>]
2805
- Pull and activate repository in ABAP system.
2806
- Auto-detects git remote and branch from current directory.
2807
- Use --files to pull only specific files.
2808
- Use --transport to specify a transport request.
2809
-
2810
- inspect --files <file1>,<file2>,...
2811
- Inspect ABAP source file(s) for issues. Currently runs syntax check.
2812
-
2813
- unit --files <file1>,<file2>,...
2814
- Run AUnit tests for ABAP test class files (.testclasses.abap)
2815
-
2816
- tree --package <package> [--depth <n>] [--include-types] [--json]
2817
- Display package hierarchy tree from ABAP system
2818
-
2819
- list --package <package> [--type <types>] [--name <pattern>] [--limit <n>] [--offset <n>] [--json]
2820
- List ABAP objects in a package with filtering and pagination
2821
-
2822
- view --objects <obj1>,<obj2>,... [--type <type>] [--json]
2823
- View ABAP object definitions from the ABAP system
2824
-
2825
- where --objects <obj1>,<obj2>,... [--type <type>] [--limit <n>] [--json]
2826
- Find where-used list for ABAP objects (classes, interfaces, programs)
2827
-
2828
- ref <pattern> [--json]
2829
- Search ABAP reference repositories for patterns. Requires referenceFolder in .abapGitAgent.
2830
-
2831
- ref --topic <topic> [--json]
2832
- View specific topic from cheat sheets (exceptions, sql, unit-tests, etc.)
2833
-
2834
- ref --list-topics
2835
- List available topics for reference search
2836
-
2837
- ref --list-repos
2838
- List all reference repositories in the reference folder
2839
-
2840
- ref --clone <repo> [--name <folder>]
2841
- Clone a GitHub repository to the reference folder
2842
- - Use full URL: https://github.com/user/repo.git
2843
- - Or short name: user/repo or user/repo (assumes github.com)
2844
-
2845
- health
2846
- Check if ABAP REST API is healthy
2847
-
2848
- status
2849
- Check if ABAP integration is configured for this repo
87
+ // Version check if needed
88
+ if (cmd.requiresVersionCheck && config) {
89
+ await versionCheck.checkCompatibility(config);
90
+ }
2850
91
 
2851
- Examples:
2852
- abapgit-agent init --folder /src --package ZMY_PACKAGE # Initialize
2853
- abapgit-agent init --update # Update files to latest
2854
- abapgit-agent create # Create repo
2855
- abapgit-agent delete # Delete repo
2856
- abapgit-agent import # Import objects to git
2857
- abapgit-agent import --message "Initial import" # Import with message
2858
- abapgit-agent pull # Auto-detect from git
2859
- abapgit-agent pull --branch develop # Specific branch
2860
- abapgit-agent pull --files zcl_my_class.clas.abap # Specific file
2861
- abapgit-agent pull --transport DEVK900001 # With transport
2862
- abapgit-agent inspect --files zcl_my_class.clas.abap # Syntax check
2863
- abapgit-agent unit --files zcl_my_test.clas.testclasses.abap # Run tests
2864
- abapgit-agent tree --package \$ZMY_PACKAGE # Show package tree
2865
- abapgit-agent tree --package \$ZMY_PACKAGE --depth 2 # Shallow tree
2866
- abapgit-agent tree --package \$ZMY_PACKAGE --include-types # With type counts
2867
- abapgit-agent tree --package \$ZMY_PACKAGE --json # JSON output
2868
- abapgit-agent list --package $ZMY_PACKAGE # List all objects
2869
- abapgit-agent list --package $ZMY_PACKAGE --type CLAS,INTF # Filter by type
2870
- abapgit-agent list --package $ZMY_PACKAGE --name ZCL_* # Filter by name
2871
- abapgit-agent list --package $ZMY_PACKAGE --limit 50 # Limit results
2872
- abapgit-agent list --package $ZMY_PACKAGE --json # JSON output
2873
- abapgit-agent view --objects ZCL_MY_CLASS # View class definition
2874
- abapgit-agent view --objects ZIF_MY_INTERFACE --type INTF # View interface
2875
- abapgit-agent view --objects ZMY_TABLE --type TABL # View table structure
2876
- abapgit-agent view --objects ZCL_CLASS1,ZCL_CLASS2 --json # Multiple objects
2877
- abapgit-agent where --objects ZCL_SUT_AUNIT_RUNNER # Find where class is used
2878
- abapgit-agent where --objects ZIF_MY_INTERFACE # Find interface implementations
2879
- abapgit-agent where --objects CL_SUT_AUNIT_RUNNER --limit 20 # Limit results
2880
- abapgit-agent where --objects ZIF_MY_INTERFACE --json # JSON output
2881
- abapgit-agent ref "CORRESPONDING" # Search all reference repos
2882
- abapgit-agent ref "CX_SY_" # Search exceptions
2883
- abapgit-agent ref --topic exceptions # View exception topic
2884
- abapgit-agent ref --topic sql # View SQL topic
2885
- abapgit-agent ref --topic unit-tests # View unit test topic
2886
- abapgit-agent ref --list-topics # List all topics
2887
- abapgit-agent ref --list-repos # List reference repositories
2888
- abapgit-agent ref --clone SAP-samples/abap-cheat-sheets # Clone repo to reference folder
2889
- abapgit-agent ref --clone https://github.com/user/repo.git # Clone from URL
2890
- abapgit-agent health
2891
- abapgit-agent status
2892
- `);
2893
- break;
92
+ // Build context for command
93
+ const context = {
94
+ config,
95
+ gitUtils,
96
+ validators,
97
+ versionCheck,
98
+ isAbapIntegrationEnabled,
99
+ loadConfig,
100
+ AbapHttp,
101
+ getTransport
102
+ };
2894
103
 
2895
- default:
2896
- console.error(`Unknown command: ${command}`);
2897
- console.error('Use: abapgit-agent help');
2898
- process.exit(1);
2899
- }
2900
- } catch (error) {
2901
- console.error(`Error: ${error.message}`);
2902
- process.exit(1);
104
+ // Execute command
105
+ await cmd.execute(args.slice(1), context);
106
+ return;
2903
107
  }
108
+
109
+ // Unknown command - show error
110
+ console.error(`Unknown command: ${command}`);
111
+ console.error('Run "abapgit-agent help" for usage information');
112
+ process.exit(1);
2904
113
  }
2905
114
 
2906
115
  main();