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