abapgit-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.abapGitAgent.example +11 -0
  2. package/API.md +271 -0
  3. package/CLAUDE.md +445 -0
  4. package/CLAUDE_MEM.md +88 -0
  5. package/ERROR_HANDLING.md +30 -0
  6. package/INSTALL.md +160 -0
  7. package/README.md +127 -0
  8. package/abap/CLAUDE.md +492 -0
  9. package/abap/package.devc.xml +10 -0
  10. package/abap/zcl_abgagt_agent.clas.abap +769 -0
  11. package/abap/zcl_abgagt_agent.clas.xml +15 -0
  12. package/abap/zcl_abgagt_cmd_factory.clas.abap +43 -0
  13. package/abap/zcl_abgagt_cmd_factory.clas.xml +15 -0
  14. package/abap/zcl_abgagt_command_inspect.clas.abap +192 -0
  15. package/abap/zcl_abgagt_command_inspect.clas.testclasses.abap +121 -0
  16. package/abap/zcl_abgagt_command_inspect.clas.xml +16 -0
  17. package/abap/zcl_abgagt_command_pull.clas.abap +80 -0
  18. package/abap/zcl_abgagt_command_pull.clas.testclasses.abap +87 -0
  19. package/abap/zcl_abgagt_command_pull.clas.xml +16 -0
  20. package/abap/zcl_abgagt_command_unit.clas.abap +297 -0
  21. package/abap/zcl_abgagt_command_unit.clas.xml +15 -0
  22. package/abap/zcl_abgagt_resource_health.clas.abap +25 -0
  23. package/abap/zcl_abgagt_resource_health.clas.xml +15 -0
  24. package/abap/zcl_abgagt_resource_inspect.clas.abap +62 -0
  25. package/abap/zcl_abgagt_resource_inspect.clas.xml +15 -0
  26. package/abap/zcl_abgagt_resource_pull.clas.abap +71 -0
  27. package/abap/zcl_abgagt_resource_pull.clas.xml +15 -0
  28. package/abap/zcl_abgagt_resource_unit.clas.abap +64 -0
  29. package/abap/zcl_abgagt_resource_unit.clas.xml +15 -0
  30. package/abap/zcl_abgagt_rest_handler.clas.abap +27 -0
  31. package/abap/zcl_abgagt_rest_handler.clas.xml +15 -0
  32. package/abap/zcl_abgagt_util.clas.abap +93 -0
  33. package/abap/zcl_abgagt_util.clas.testclasses.abap +84 -0
  34. package/abap/zcl_abgagt_util.clas.xml +16 -0
  35. package/abap/zif_abgagt_agent.intf.abap +134 -0
  36. package/abap/zif_abgagt_agent.intf.xml +15 -0
  37. package/abap/zif_abgagt_cmd_factory.intf.abap +7 -0
  38. package/abap/zif_abgagt_cmd_factory.intf.xml +15 -0
  39. package/abap/zif_abgagt_command.intf.abap +21 -0
  40. package/abap/zif_abgagt_command.intf.xml +15 -0
  41. package/abap/zif_abgagt_util.intf.abap +28 -0
  42. package/abap/zif_abgagt_util.intf.xml +15 -0
  43. package/bin/abapgit-agent +902 -0
  44. package/img/claude.png +0 -0
  45. package/package.json +31 -0
  46. package/scripts/claude-integration.js +351 -0
  47. package/scripts/test-integration.js +139 -0
  48. package/src/abap-client.js +314 -0
  49. package/src/agent.js +119 -0
  50. package/src/config.js +66 -0
  51. package/src/index.js +48 -0
  52. package/src/logger.js +39 -0
  53. package/src/server.js +116 -0
@@ -0,0 +1,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
+ };