abapgit-agent 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +7 -7
  2. package/abap/CLAUDE.md +145 -26
  3. package/abap/guidelines/00_index.md +8 -0
  4. package/abap/guidelines/01_sql.md +8 -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 +8 -0
  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 +8 -0
  12. package/abap/guidelines/09_unit_testable_code.md +8 -0
  13. package/bin/abapgit-agent +61 -2852
  14. package/package.json +21 -5
  15. package/src/agent.js +205 -16
  16. package/src/commands/create.js +102 -0
  17. package/src/commands/delete.js +72 -0
  18. package/src/commands/health.js +24 -0
  19. package/src/commands/help.js +111 -0
  20. package/src/commands/import.js +99 -0
  21. package/src/commands/init.js +321 -0
  22. package/src/commands/inspect.js +184 -0
  23. package/src/commands/list.js +143 -0
  24. package/src/commands/preview.js +277 -0
  25. package/src/commands/pull.js +278 -0
  26. package/src/commands/ref.js +96 -0
  27. package/src/commands/status.js +52 -0
  28. package/src/commands/syntax.js +290 -0
  29. package/src/commands/tree.js +209 -0
  30. package/src/commands/unit.js +133 -0
  31. package/src/commands/view.js +215 -0
  32. package/src/commands/where.js +138 -0
  33. package/src/config.js +11 -1
  34. package/src/utils/abap-http.js +347 -0
  35. package/src/utils/git-utils.js +58 -0
  36. package/src/utils/validators.js +72 -0
  37. package/src/utils/version-check.js +80 -0
  38. package/src/abap-client.js +0 -526
  39. /package/src/{ref-search.js → utils/abap-reference.js} +0 -0
@@ -1,526 +0,0 @@
1
- /**
2
- * ABAP Client - Connects to SAP ABAP system via REST/HTTP
3
- */
4
-
5
- const https = require('https');
6
- const http = require('http');
7
- const fs = require('fs');
8
- const path = require('path');
9
- const { getAbapConfig } = require('./config');
10
- const logger = require('./logger');
11
-
12
- class ABAPClient {
13
- constructor() {
14
- this.config = null;
15
- this.cookieFile = path.join(__dirname, '..', '.abapgit_agent_cookies.txt');
16
- this.csrfToken = null;
17
- }
18
-
19
- /**
20
- * Get ABAP configuration
21
- */
22
- getConfig() {
23
- if (!this.config) {
24
- const cfg = getAbapConfig();
25
- this.config = {
26
- baseUrl: `https://${cfg.host}:${cfg.sapport || 44300}/sap/bc/z_abapgit_agent`,
27
- username: cfg.user,
28
- password: cfg.password,
29
- client: cfg.client,
30
- language: cfg.language || 'EN',
31
- gitUsername: cfg.gitUsername,
32
- gitPassword: cfg.gitPassword
33
- };
34
- }
35
- return this.config;
36
- }
37
-
38
- /**
39
- * Read cookies from Netscape format cookie file
40
- */
41
- readNetscapeCookies() {
42
- if (!fs.existsSync(this.cookieFile)) return '';
43
-
44
- const content = fs.readFileSync(this.cookieFile, 'utf8');
45
- const lines = content.split('\n');
46
- const cookies = [];
47
-
48
- for (const line of lines) {
49
- const trimmed = line.trim();
50
- // Skip empty lines and only the header comments (starting with #)
51
- // but NOT HttpOnly cookies which start with #HttpOnly_
52
- if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#HttpOnly'))) continue;
53
-
54
- const parts = trimmed.split('\t');
55
- if (parts.length >= 7) {
56
- // Format: domain, flag, path, secure, expiration, name, value
57
- cookies.push(`${parts[5]}=${parts[6]}`);
58
- }
59
- }
60
-
61
- return cookies.join('; ');
62
- }
63
-
64
- /**
65
- * Make HTTP request
66
- */
67
- async request(method, path, data = null, options = {}) {
68
- const cfg = this.getConfig();
69
-
70
- return new Promise((resolve, reject) => {
71
- const url = new URL(`${cfg.baseUrl}${path}`);
72
-
73
- const headers = {
74
- 'Content-Type': 'application/json',
75
- 'sap-client': cfg.client,
76
- 'sap-language': cfg.language
77
- };
78
-
79
- // Add authorization
80
- if (cfg.username) {
81
- headers['Authorization'] = `Basic ${Buffer.from(`${cfg.username}:${cfg.password}`).toString('base64')}`;
82
- }
83
-
84
- // Add CSRF token for POST
85
- if (method === 'POST' && options.csrfToken) {
86
- headers['X-CSRF-Token'] = options.csrfToken;
87
- }
88
-
89
- // Add cookies if available (handle Netscape format)
90
- const cookieHeader = this.readNetscapeCookies();
91
- if (cookieHeader) {
92
- headers['Cookie'] = cookieHeader;
93
- }
94
-
95
- const reqOptions = {
96
- hostname: url.hostname,
97
- port: url.port,
98
- path: url.pathname,
99
- method,
100
- headers,
101
- agent: new https.Agent({ rejectUnauthorized: false })
102
- };
103
-
104
- const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (res) => {
105
- // Update cookies
106
- const setCookie = res.headers['set-cookie'];
107
- if (setCookie) {
108
- const cookies = Array.isArray(setCookie)
109
- ? setCookie.map(c => c.split(';')[0]).join('; ')
110
- : setCookie.split(';')[0];
111
- fs.writeFileSync(this.cookieFile, cookies);
112
- }
113
-
114
- // Get CSRF token from response headers (for GET /pull with fetch)
115
- if (res.headers['x-csrf-token'] && !this.csrfToken) {
116
- this.csrfToken = res.headers['x-csrf-token'];
117
- }
118
-
119
- let body = '';
120
- res.on('data', chunk => body += chunk);
121
- res.on('end', () => {
122
- try {
123
- if (res.statusCode >= 400) {
124
- logger.error(`REST request failed`, { status: res.statusCode, body });
125
- reject(new Error(`REST request failed: ${res.statusCode}`));
126
- } else if (body) {
127
- resolve(JSON.parse(body));
128
- } else {
129
- resolve({});
130
- }
131
- } catch (e) {
132
- resolve(body);
133
- }
134
- });
135
- });
136
-
137
- req.on('error', reject);
138
-
139
- if (data) {
140
- req.write(JSON.stringify(data));
141
- }
142
- req.end();
143
- });
144
- }
145
-
146
- /**
147
- * Fetch CSRF token using GET /pull with X-CSRF-Token: fetch
148
- */
149
- async fetchCsrfToken() {
150
- const cfg = this.getConfig();
151
-
152
- return new Promise((resolve, reject) => {
153
- const url = new URL(`${cfg.baseUrl}/pull`);
154
-
155
- // Clear stale cookies before fetching new token
156
- if (fs.existsSync(this.cookieFile)) {
157
- fs.unlinkSync(this.cookieFile);
158
- }
159
-
160
- // Read cookies for sending (handle Netscape format)
161
- const cookieHeader = this.readNetscapeCookies();
162
-
163
- const options = {
164
- hostname: url.hostname,
165
- port: url.port,
166
- path: url.pathname,
167
- method: 'GET',
168
- headers: {
169
- 'Authorization': `Basic ${Buffer.from(`${cfg.username}:${cfg.password}`).toString('base64')}`,
170
- 'sap-client': cfg.client,
171
- 'sap-language': cfg.language,
172
- 'X-CSRF-Token': 'fetch',
173
- 'Content-Type': 'application/json',
174
- ...(cookieHeader && { 'Cookie': cookieHeader })
175
- },
176
- agent: new https.Agent({ rejectUnauthorized: false })
177
- };
178
-
179
- const req = https.request(options, (res) => {
180
- const csrfToken = res.headers['x-csrf-token'];
181
-
182
- // Save new cookies from response - the CSRF token is tied to this new session!
183
- const setCookie = res.headers['set-cookie'];
184
- if (setCookie) {
185
- const cookies = Array.isArray(setCookie)
186
- ? setCookie.map(c => c.split(';')[0]).join('; ')
187
- : setCookie.split(';')[0];
188
- fs.writeFileSync(this.cookieFile, cookies);
189
- }
190
-
191
- let body = '';
192
- res.on('data', chunk => body += chunk);
193
- res.on('end', () => {
194
- // Store token in instance for use by POST
195
- this.csrfToken = csrfToken;
196
- resolve({ token: csrfToken });
197
- });
198
- });
199
-
200
- req.on('error', reject);
201
- req.end();
202
- });
203
- }
204
-
205
- /**
206
- * Pull repository and activate
207
- * Uses command API if useCommandApi config is enabled, otherwise uses legacy /pull endpoint
208
- * @param {string} repoUrl - Repository URL
209
- * @param {string} branch - Branch name (default: 'main')
210
- * @param {string} gitUsername - Git username (optional)
211
- * @param {string} gitPassword - Git password/token (optional)
212
- * @param {Array} files - Array of file paths to pull (optional)
213
- * @param {string} transportRequest - Transport request number (optional)
214
- * @returns {object} Pull result
215
- */
216
- async pull(repoUrl, branch = 'main', gitUsername = null, gitPassword = null, files = null, transportRequest = null) {
217
- const cfg = this.getConfig();
218
-
219
- // Fetch CSRF token first (using GET /pull with X-CSRF-Token: fetch)
220
- await this.fetchCsrfToken();
221
-
222
- const data = {
223
- url: repoUrl,
224
- branch: branch
225
- };
226
-
227
- // Add files if specified
228
- if (files && files.length > 0) {
229
- data.files = files;
230
- }
231
-
232
- // Add transport request if specified
233
- if (transportRequest) {
234
- data.transport_request = transportRequest;
235
- }
236
-
237
- // Use config git credentials if no override provided
238
- data.username = gitUsername || cfg.gitUsername;
239
- data.password = gitPassword || cfg.gitPassword;
240
-
241
- logger.info('Starting pull operation', { repoUrl, branch, transportRequest, service: 'abapgit-agent' });
242
-
243
- return await this.request('POST', '/pull', data, { csrfToken: this.csrfToken });
244
- }
245
-
246
- /**
247
- * Health check
248
- */
249
- async healthCheck() {
250
- try {
251
- const result = await this.request('GET', '/health');
252
- return { status: 'healthy', abap: 'connected', ...result };
253
- } catch (error) {
254
- return { status: 'unhealthy', abap: 'disconnected', error: error.message };
255
- }
256
- }
257
-
258
- /**
259
- * Check syntax of an ABAP object
260
- */
261
- async syntaxCheck(objectType, objectName) {
262
- // Fetch CSRF token first
263
- await this.fetchCsrfToken();
264
-
265
- const data = {
266
- object_type: objectType,
267
- object_name: objectName
268
- };
269
-
270
- logger.info('Starting syntax check', { objectType, objectName, service: 'abapgit-agent' });
271
-
272
- return await this.request('POST', '/syntax-check', data, { csrfToken: this.csrfToken });
273
- }
274
-
275
- /**
276
- * Run unit tests for package or objects
277
- * @param {string} packageName - Package name to run tests for (optional)
278
- * @param {Array} objects - Array of {object_type, object_name} objects (optional)
279
- * @returns {object} Unit test results
280
- */
281
- async unitTest(packageName = null, objects = []) {
282
- // Fetch CSRF token first
283
- await this.fetchCsrfToken();
284
-
285
- const data = {};
286
-
287
- if (packageName) {
288
- data.package = packageName;
289
- }
290
-
291
- if (objects && objects.length > 0) {
292
- data.objects = objects;
293
- }
294
-
295
- logger.info('Starting unit tests', { package: packageName, objects, service: 'abapgit-agent' });
296
-
297
- return await this.request('POST', '/unit', data, { csrfToken: this.csrfToken });
298
- }
299
-
300
- /**
301
- * Create a new online repository
302
- * @param {string} repoUrl - Git repository URL
303
- * @param {string} packageName - ABAP package name
304
- * @param {string} branch - Branch name (default: 'main')
305
- * @param {string} displayName - Display name for the repository (optional)
306
- * @param {string} name - Repository name (optional)
307
- * @param {string} folderLogic - Folder logic: 'PREFIX' or 'FULL' (default: 'PREFIX')
308
- * @returns {object} Create result
309
- */
310
- async create(repoUrl, packageName, branch = 'main', displayName = null, name = null, folderLogic = 'PREFIX') {
311
- // Fetch CSRF token first
312
- await this.fetchCsrfToken();
313
-
314
- const data = {
315
- url: repoUrl,
316
- package: packageName,
317
- branch: branch
318
- };
319
-
320
- if (displayName) {
321
- data.display_name = displayName;
322
- }
323
-
324
- if (name) {
325
- data.name = name;
326
- }
327
-
328
- if (folderLogic) {
329
- data.folder_logic = folderLogic;
330
- }
331
-
332
- logger.info('Creating repository', { repoUrl, packageName, branch, service: 'abapgit-agent' });
333
-
334
- return await this.request('POST', '/create', data, { csrfToken: this.csrfToken });
335
- }
336
-
337
- /**
338
- * Import existing objects from package to git repository
339
- * @param {string} repoUrl - Git repository URL
340
- * @param {string} message - Commit message (optional)
341
- * @returns {object} Import result with success, files_staged, commit_sha
342
- */
343
- async import(repoUrl, message = null) {
344
- // Fetch CSRF token first
345
- await this.fetchCsrfToken();
346
-
347
- const data = {
348
- url: repoUrl
349
- };
350
-
351
- if (message) {
352
- data.message = message;
353
- }
354
-
355
- logger.info('Starting import operation', { repoUrl, message, service: 'abapgit-agent' });
356
-
357
- return await this.request('POST', '/import', data, { csrfToken: this.csrfToken });
358
- }
359
-
360
- /**
361
- * Get package hierarchy tree
362
- * @param {string} packageName - ABAP package name
363
- * @param {number} depth - Maximum depth to traverse (default: 3, max: 10)
364
- * @param {boolean} includeObjects - Include object counts breakdown
365
- * @returns {object} Tree result with hierarchy and summary
366
- */
367
- async tree(packageName, depth = 3, includeObjects = false) {
368
- // Fetch CSRF token first
369
- await this.fetchCsrfToken();
370
-
371
- const data = {
372
- package: packageName,
373
- depth: Math.min(Math.max(1, depth), 10),
374
- include_objects: includeObjects
375
- };
376
-
377
- logger.info('Getting package tree', { package: packageName, depth: data.depth, includeObjects, service: 'abapgit-agent' });
378
-
379
- return await this.request('POST', '/tree', data, { csrfToken: this.csrfToken });
380
- }
381
-
382
- async preview(objects, type = null, limit = 100, offset = 0, where = null, columns = null) {
383
- // Fetch CSRF token first
384
- await this.fetchCsrfToken();
385
-
386
- const data = {
387
- objects: objects,
388
- limit: Math.min(Math.max(1, limit), 500),
389
- offset: Math.max(0, offset)
390
- };
391
-
392
- if (type) {
393
- data.type = type;
394
- }
395
-
396
- if (where) {
397
- // Convert ISO date format (YYYY-MM-DD) to ABAP DATS format (YYYYMMDD)
398
- // This handles date literals in WHERE clauses like "FLDATE = '2024-10-24'"
399
- data.where = this.convertDatesInWhereClause(where);
400
- }
401
-
402
- if (columns) {
403
- data.columns = columns;
404
- }
405
-
406
- logger.info('Previewing data', { objects, type, limit: data.limit, offset: data.offset, where: data.where, service: 'abapgit-agent' });
407
-
408
- return await this.request('POST', '/preview', data, { csrfToken: this.csrfToken });
409
- }
410
-
411
- /**
412
- * Convert ISO date formats to ABAP DATS format in WHERE clause
413
- * @param {string} whereClause - SQL WHERE clause
414
- * @returns {string} - WHERE clause with dates converted to YYYYMMDD format
415
- */
416
- convertDatesInWhereClause(whereClause) {
417
- if (!whereClause) return whereClause;
418
-
419
- // Pattern to match ISO date format: 'YYYY-MM-DD'
420
- // Uses negative lookbehind and lookahead to ensure we're matching complete dates
421
- const isoDatePattern = /'\d{4}-\d{2}-\d{2}'/g;
422
-
423
- return whereClause.replace(isoDatePattern, (match) => {
424
- // Extract YYYY, MM, DD from 'YYYY-MM-DD'
425
- const dateContent = match.slice(1, -1); // Remove quotes: YYYY-MM-DD
426
- const [year, month, day] = dateContent.split('-');
427
- // Return in ABAP format: 'YYYYMMDD'
428
- return `'${year}${month}${day}'`;
429
- });
430
- }
431
-
432
- /**
433
- * List objects in an ABAP package
434
- * @param {string} packageName - ABAP package name
435
- * @param {string} type - Comma-separated object types to filter (optional, e.g., 'CLAS,INTF')
436
- * @param {string} name - Name pattern to filter (optional)
437
- * @param {number} limit - Maximum number of objects to return (default: 100, max: 1000)
438
- * @param {number} offset - Offset for pagination (default: 0)
439
- * @returns {object} List result with objects, by_type, and total
440
- */
441
- async list(packageName, type = null, name = null, limit = 100, offset = 0) {
442
- // Fetch CSRF token first
443
- await this.fetchCsrfToken();
444
-
445
- const data = {
446
- package: packageName,
447
- limit: Math.min(Math.max(1, limit), 1000),
448
- offset: Math.max(0, offset)
449
- };
450
-
451
- if (type) {
452
- data.type = type;
453
- }
454
-
455
- if (name) {
456
- data.name = name;
457
- }
458
-
459
- logger.info('Listing objects', { package: packageName, type, name, limit: data.limit, offset: data.offset, service: 'abapgit-agent' });
460
-
461
- return await this.request('POST', '/list', data, { csrfToken: this.csrfToken });
462
- }
463
-
464
- /**
465
- * View ABAP object definitions
466
- * @param {Array} objects - Array of object names to view
467
- * @param {string} type - Object type (optional, e.g., 'CLAS', 'TABL')
468
- * @returns {object} View result with object definitions
469
- */
470
- async view(objects, type = null) {
471
- await this.fetchCsrfToken();
472
-
473
- const data = {
474
- objects: objects
475
- };
476
-
477
- if (type) {
478
- data.type = type;
479
- }
480
-
481
- logger.info('Viewing objects', { objects, type, service: 'abapgit-agent' });
482
-
483
- return await this.request('POST', '/view', data, { csrfToken: this.csrfToken });
484
- }
485
-
486
- /**
487
- * Find where-used list for ABAP objects
488
- * @param {Array} objects - Array of object names to search
489
- * @param {string} type - Object type (optional)
490
- * @param {number} limit - Maximum results (default: 100, max: 500)
491
- * @param {number} offset - Number of results to skip (default: 0)
492
- * @returns {object} Where-used result with found objects
493
- */
494
- async where(objects, type = null, limit = 100, offset = 0) {
495
- await this.fetchCsrfToken();
496
-
497
- const data = {
498
- objects: objects,
499
- limit: Math.min(Math.max(1, limit), 500),
500
- offset: Math.max(0, offset)
501
- };
502
-
503
- if (type) {
504
- data.type = type;
505
- }
506
-
507
- logger.info('Finding where-used', { objects, type, limit: data.limit, offset: data.offset, service: 'abapgit-agent' });
508
-
509
- return await this.request('POST', '/where', data, { csrfToken: this.csrfToken });
510
- }
511
- }
512
-
513
- // Singleton instance
514
- let instance = null;
515
-
516
- function getClient() {
517
- if (!instance) {
518
- instance = new ABAPClient();
519
- }
520
- return instance;
521
- }
522
-
523
- module.exports = {
524
- ABAPClient,
525
- getClient
526
- };
File without changes