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,314 @@
|
|
|
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
|
+
// Singleton instance
|
|
302
|
+
let instance = null;
|
|
303
|
+
|
|
304
|
+
function getClient() {
|
|
305
|
+
if (!instance) {
|
|
306
|
+
instance = new ABAPClient();
|
|
307
|
+
}
|
|
308
|
+
return instance;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
ABAPClient,
|
|
313
|
+
getClient
|
|
314
|
+
};
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ABAP Git Agent - Main agent class
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { getClient } = require('./abap-client');
|
|
6
|
+
const logger = require('./logger');
|
|
7
|
+
|
|
8
|
+
class ABAPGitAgent {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.abap = getClient();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pull repository and activate objects
|
|
15
|
+
* @param {string} repoUrl - Git repository URL
|
|
16
|
+
* @param {string} branch - Branch name (default: main)
|
|
17
|
+
* @param {string} username - Git username (optional)
|
|
18
|
+
* @param {string} password - Git password/token (optional)
|
|
19
|
+
* @param {Array} files - Specific files to pull (optional)
|
|
20
|
+
* @returns {object} Pull result with success, job_id, message, error_detail
|
|
21
|
+
*/
|
|
22
|
+
async pull(repoUrl, branch = 'main', username = null, password = null, files = null) {
|
|
23
|
+
logger.info('Starting pull operation', { repoUrl, branch, username: !!username, files });
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const result = await this.abap.pull(repoUrl, branch, username, password, files);
|
|
27
|
+
|
|
28
|
+
// Return the result directly from ABAP (handle uppercase keys from /UI2/CL_JSON)
|
|
29
|
+
return {
|
|
30
|
+
success: result.SUCCESS === 'X' || result.success === 'X' || result.success === true,
|
|
31
|
+
job_id: result.JOB_ID || result.job_id,
|
|
32
|
+
message: result.MESSAGE || result.message,
|
|
33
|
+
error_detail: result.ERROR_DETAIL || result.error_detail || null,
|
|
34
|
+
activated_count: result.ACTIVATED_COUNT || result.activated_count || 0,
|
|
35
|
+
failed_count: result.FAILED_COUNT || result.failed_count || 0,
|
|
36
|
+
activated_objects: result.ACTIVATED_OBJECTS || result.activated_objects || [],
|
|
37
|
+
failed_objects: result.FAILED_OBJECTS || result.failed_objects || []
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
} catch (error) {
|
|
41
|
+
logger.error('Pull failed', { error: error.message });
|
|
42
|
+
throw new Error(`Pull failed: ${error.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Health check
|
|
48
|
+
* @returns {object} Health status
|
|
49
|
+
*/
|
|
50
|
+
async healthCheck() {
|
|
51
|
+
try {
|
|
52
|
+
const result = await this.abap.healthCheck();
|
|
53
|
+
return {
|
|
54
|
+
status: 'healthy',
|
|
55
|
+
abap: 'connected',
|
|
56
|
+
version: result.version || '1.0.0'
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
status: 'unhealthy',
|
|
61
|
+
abap: 'disconnected',
|
|
62
|
+
error: error.message
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check syntax of an ABAP object
|
|
69
|
+
* @param {string} objectType - ABAP object type (e.g., 'CLAS', 'PROG', 'INTF')
|
|
70
|
+
* @param {string} objectName - ABAP object name
|
|
71
|
+
* @returns {object} Syntax check result with errors (if any)
|
|
72
|
+
*/
|
|
73
|
+
async syntaxCheck(objectType, objectName) {
|
|
74
|
+
logger.info('Starting syntax check', { objectType, objectName });
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const result = await this.abap.syntaxCheck(objectType, objectName);
|
|
78
|
+
return {
|
|
79
|
+
success: result.SUCCESS === 'X' || result.success === 'X' || result.success === true,
|
|
80
|
+
object_type: result.OBJECT_TYPE || result.object_type,
|
|
81
|
+
object_name: result.OBJECT_NAME || result.object_name,
|
|
82
|
+
error_count: result.ERROR_COUNT || result.error_count || 0,
|
|
83
|
+
errors: result.ERRORS || result.errors || []
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
logger.error('Syntax check failed', { error: error.message });
|
|
87
|
+
throw new Error(`Syntax check failed: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run unit tests for package or objects
|
|
93
|
+
* @param {string} packageName - Package name to run tests for (optional)
|
|
94
|
+
* @param {Array} objects - Array of {object_type, object_name} objects (optional)
|
|
95
|
+
* @returns {object} Unit test results with test_count, passed_count, failed_count, results
|
|
96
|
+
*/
|
|
97
|
+
async unitCheck(packageName = null, objects = []) {
|
|
98
|
+
logger.info('Starting unit tests', { package: packageName, objects });
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const result = await this.abap.unitTest(packageName, objects);
|
|
102
|
+
return {
|
|
103
|
+
success: result.SUCCESS === 'X' || result.success === 'X' || result.success === true,
|
|
104
|
+
test_count: result.TEST_COUNT || result.test_count || 0,
|
|
105
|
+
passed_count: result.PASSED_COUNT || result.passed_count || 0,
|
|
106
|
+
failed_count: result.FAILED_COUNT || result.failed_count || 0,
|
|
107
|
+
message: result.MESSAGE || result.message || '',
|
|
108
|
+
errors: result.ERRORS || result.errors || []
|
|
109
|
+
};
|
|
110
|
+
} catch (error) {
|
|
111
|
+
logger.error('Unit tests failed', { error: error.message });
|
|
112
|
+
throw new Error(`Unit tests failed: ${error.message}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
ABAPGitAgent
|
|
119
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loader
|
|
3
|
+
* Loads config from .abapGitAgent or environment variables
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
let config = null;
|
|
10
|
+
|
|
11
|
+
function loadConfig() {
|
|
12
|
+
if (config) return config;
|
|
13
|
+
|
|
14
|
+
// First check current working directory (repo root) - for system-level integration
|
|
15
|
+
const repoConfigPath = path.join(process.cwd(), '.abapGitAgent');
|
|
16
|
+
if (fs.existsSync(repoConfigPath)) {
|
|
17
|
+
config = JSON.parse(fs.readFileSync(repoConfigPath, 'utf8'));
|
|
18
|
+
return config;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Fallback to package directory (for legacy/compatibility)
|
|
22
|
+
const configPath = process.env.CONFIG_PATH || path.join(__dirname, '..', '.abapGitAgent');
|
|
23
|
+
|
|
24
|
+
if (fs.existsSync(configPath)) {
|
|
25
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
26
|
+
} else {
|
|
27
|
+
// Load from environment variables
|
|
28
|
+
config = {
|
|
29
|
+
host: process.env.ABAP_HOST,
|
|
30
|
+
sapport: parseInt(process.env.ABAP_PORT, 10) || 443,
|
|
31
|
+
client: process.env.ABAP_CLIENT || '100',
|
|
32
|
+
user: process.env.ABAP_USER,
|
|
33
|
+
password: process.env.ABAP_PASSWORD,
|
|
34
|
+
language: process.env.ABAP_LANGUAGE || 'EN',
|
|
35
|
+
gitUsername: process.env.GIT_USERNAME,
|
|
36
|
+
gitPassword: process.env.GIT_PASSWORD
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getAbapConfig() {
|
|
44
|
+
const cfg = loadConfig();
|
|
45
|
+
return {
|
|
46
|
+
host: cfg.host,
|
|
47
|
+
sapport: cfg.sapport || 443,
|
|
48
|
+
client: cfg.client,
|
|
49
|
+
user: cfg.user,
|
|
50
|
+
password: cfg.password,
|
|
51
|
+
language: cfg.language || 'EN',
|
|
52
|
+
gitUsername: cfg.gitUsername || process.env.GIT_USERNAME,
|
|
53
|
+
gitPassword: cfg.gitPassword || process.env.GIT_PASSWORD
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getAgentConfig() {
|
|
58
|
+
const cfg = loadConfig();
|
|
59
|
+
return cfg.agent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
loadConfig,
|
|
64
|
+
getAbapConfig,
|
|
65
|
+
getAgentConfig
|
|
66
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ABAP Git Agent - Package Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Exports functions for programmatic use:
|
|
5
|
+
* const { pull, healthCheck } = require('abapgit-agent');
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { ABAPGitAgent } = require('./agent');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Pull repository and activate objects
|
|
12
|
+
* @param {string} repoUrl - Git repository URL
|
|
13
|
+
* @param {string} branch - Branch name (default: main)
|
|
14
|
+
* @param {string} username - Git username (optional)
|
|
15
|
+
* @param {string} password - Git password/token (optional)
|
|
16
|
+
* @returns {object} Pull result with success, job_id, message, error_detail
|
|
17
|
+
*/
|
|
18
|
+
async function pull(repoUrl, branch = 'main', username = null, password = null) {
|
|
19
|
+
const agent = new ABAPGitAgent();
|
|
20
|
+
return await agent.pull(repoUrl, branch, username, password);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check agent health
|
|
25
|
+
* @returns {object} Health status
|
|
26
|
+
*/
|
|
27
|
+
async function healthCheck() {
|
|
28
|
+
const agent = new ABAPGitAgent();
|
|
29
|
+
return await agent.healthCheck();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if integration is configured for current directory
|
|
34
|
+
* @returns {boolean} True if .abapGitAgent exists
|
|
35
|
+
*/
|
|
36
|
+
function isConfigured() {
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const repoConfigPath = path.join(process.cwd(), '.abapGitAgent');
|
|
40
|
+
return fs.existsSync(repoConfigPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
pull,
|
|
45
|
+
healthCheck,
|
|
46
|
+
isConfigured,
|
|
47
|
+
ABAPGitAgent
|
|
48
|
+
};
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger module using Winston
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const winston = require('winston');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
9
|
+
const logDir = path.join(__dirname, '..', 'logs');
|
|
10
|
+
if (!fs.existsSync(logDir)) {
|
|
11
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const logger = winston.createLogger({
|
|
15
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
16
|
+
format: winston.format.combine(
|
|
17
|
+
winston.format.timestamp(),
|
|
18
|
+
winston.format.errors({ stack: true }),
|
|
19
|
+
winston.format.json()
|
|
20
|
+
),
|
|
21
|
+
defaultMeta: { service: 'abapgit-agent' },
|
|
22
|
+
transports: [
|
|
23
|
+
new winston.transports.File({
|
|
24
|
+
filename: path.join(logDir, 'error.log'),
|
|
25
|
+
level: 'error'
|
|
26
|
+
}),
|
|
27
|
+
new winston.transports.File({
|
|
28
|
+
filename: path.join(logDir, 'combined.log')
|
|
29
|
+
}),
|
|
30
|
+
new winston.transports.Console({
|
|
31
|
+
format: winston.format.combine(
|
|
32
|
+
winston.format.colorize(),
|
|
33
|
+
winston.format.simple()
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
]
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
module.exports = logger;
|
package/src/server.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Server for Claude Integration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const express = require('express');
|
|
6
|
+
const cors = require('cors');
|
|
7
|
+
const { ABAPGitAgent } = require('./agent');
|
|
8
|
+
const { getAgentConfig } = require('./config');
|
|
9
|
+
const logger = require('./logger');
|
|
10
|
+
|
|
11
|
+
class Server {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.app = express();
|
|
14
|
+
this.agent = new ABAPGitAgent();
|
|
15
|
+
this.agentConfig = getAgentConfig();
|
|
16
|
+
|
|
17
|
+
this.setupMiddleware();
|
|
18
|
+
this.setupRoutes();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
setupMiddleware() {
|
|
22
|
+
this.app.use(cors());
|
|
23
|
+
this.app.use(express.json());
|
|
24
|
+
|
|
25
|
+
// Request logging
|
|
26
|
+
this.app.use((req, res, next) => {
|
|
27
|
+
logger.debug(`${req.method} ${req.path}`, { body: req.body });
|
|
28
|
+
next();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Error handling
|
|
32
|
+
this.app.use((err, req, res, next) => {
|
|
33
|
+
logger.error('Request error', { error: err.message, stack: err.stack });
|
|
34
|
+
res.status(500).json({
|
|
35
|
+
success: false,
|
|
36
|
+
error: err.message
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setupRoutes() {
|
|
42
|
+
// Health check
|
|
43
|
+
this.app.get('/api/health', async (req, res) => {
|
|
44
|
+
try {
|
|
45
|
+
const health = await this.agent.healthCheck();
|
|
46
|
+
res.json(health);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
res.status(503).json({
|
|
49
|
+
status: 'unhealthy',
|
|
50
|
+
error: error.message
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Pull repository (synchronous - returns immediately with result)
|
|
56
|
+
this.app.post('/api/pull', async (req, res) => {
|
|
57
|
+
try {
|
|
58
|
+
const { url, branch, username, password } = req.body;
|
|
59
|
+
|
|
60
|
+
if (!url) {
|
|
61
|
+
return res.status(400).json({
|
|
62
|
+
success: false,
|
|
63
|
+
error: 'Missing required parameter: url'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = await this.agent.pull(url, branch, username, password);
|
|
68
|
+
res.json(result);
|
|
69
|
+
|
|
70
|
+
} catch (error) {
|
|
71
|
+
logger.error('Pull failed', { error: error.message });
|
|
72
|
+
res.status(500).json({
|
|
73
|
+
success: false,
|
|
74
|
+
error: error.message
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
start() {
|
|
81
|
+
const port = this.agentConfig.port || 3000;
|
|
82
|
+
|
|
83
|
+
this.server = this.app.listen(port, () => {
|
|
84
|
+
logger.info(`ABAP AI Bridge server started on port ${port}`);
|
|
85
|
+
console.log(`\nš ABAP AI Bridge is running!`);
|
|
86
|
+
console.log(` Health: http://localhost:${port}/api/health`);
|
|
87
|
+
console.log(` Pull: POST http://localhost:${port}/api/pull`);
|
|
88
|
+
console.log(`\nš API Documentation:`);
|
|
89
|
+
console.log(` POST /api/pull { "url": "git-url", "branch": "main" }`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Graceful shutdown
|
|
93
|
+
process.on('SIGTERM', () => this.shutdown());
|
|
94
|
+
process.on('SIGINT', () => this.shutdown());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
shutdown() {
|
|
98
|
+
logger.info('Shutting down server...');
|
|
99
|
+
if (this.server) {
|
|
100
|
+
this.server.close(() => {
|
|
101
|
+
logger.info('Server closed');
|
|
102
|
+
process.exit(0);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Start server if run directly
|
|
109
|
+
if (require.main === module) {
|
|
110
|
+
const server = new Server();
|
|
111
|
+
server.start();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
Server
|
|
116
|
+
};
|