abapgit-agent 1.7.2 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -8
- package/abap/CLAUDE.md +146 -26
- package/abap/guidelines/00_index.md +8 -0
- package/abap/guidelines/01_sql.md +28 -0
- package/abap/guidelines/02_exceptions.md +8 -0
- package/abap/guidelines/03_testing.md +8 -0
- package/abap/guidelines/04_cds.md +151 -36
- package/abap/guidelines/05_classes.md +8 -0
- package/abap/guidelines/06_objects.md +8 -0
- package/abap/guidelines/07_json.md +8 -0
- package/abap/guidelines/08_abapgit.md +52 -3
- package/abap/guidelines/09_unit_testable_code.md +8 -0
- package/abap/guidelines/10_common_errors.md +95 -0
- package/bin/abapgit-agent +61 -2852
- package/package.json +21 -5
- package/src/agent.js +205 -16
- package/src/commands/create.js +102 -0
- package/src/commands/delete.js +72 -0
- package/src/commands/health.js +24 -0
- package/src/commands/help.js +111 -0
- package/src/commands/import.js +99 -0
- package/src/commands/init.js +321 -0
- package/src/commands/inspect.js +184 -0
- package/src/commands/list.js +143 -0
- package/src/commands/preview.js +277 -0
- package/src/commands/pull.js +278 -0
- package/src/commands/ref.js +96 -0
- package/src/commands/status.js +52 -0
- package/src/commands/syntax.js +340 -0
- package/src/commands/tree.js +209 -0
- package/src/commands/unit.js +133 -0
- package/src/commands/view.js +215 -0
- package/src/commands/where.js +138 -0
- package/src/config.js +11 -1
- package/src/utils/abap-http.js +347 -0
- package/src/utils/git-utils.js +58 -0
- package/src/utils/validators.js +72 -0
- package/src/utils/version-check.js +80 -0
- package/src/abap-client.js +0 -526
- /package/src/{ref-search.js → utils/abap-reference.js} +0 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ABAP HTTP request wrapper with CSRF token and session management
|
|
3
|
+
*/
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ABAP HTTP client with CSRF token, cookie, and session caching
|
|
13
|
+
*/
|
|
14
|
+
class AbapHttp {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.csrfToken = null;
|
|
18
|
+
this.cookies = null;
|
|
19
|
+
|
|
20
|
+
// Session cache file path
|
|
21
|
+
const configHash = crypto.createHash('md5')
|
|
22
|
+
.update(`${config.host}:${config.user}:${config.client}`)
|
|
23
|
+
.digest('hex')
|
|
24
|
+
.substring(0, 8);
|
|
25
|
+
|
|
26
|
+
this.sessionFile = path.join(os.tmpdir(), `abapgit-session-${configHash}.json`);
|
|
27
|
+
|
|
28
|
+
// Try to load cached session
|
|
29
|
+
this.loadSession();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load session from cache file if valid
|
|
34
|
+
*/
|
|
35
|
+
loadSession() {
|
|
36
|
+
if (!fs.existsSync(this.sessionFile)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const session = JSON.parse(fs.readFileSync(this.sessionFile, 'utf8'));
|
|
42
|
+
|
|
43
|
+
// Check if expired (with 2-minute safety margin)
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const safetyMargin = 2 * 60 * 1000; // 2 minutes
|
|
46
|
+
|
|
47
|
+
if (session.expiresAt > now + safetyMargin) {
|
|
48
|
+
this.csrfToken = session.csrfToken;
|
|
49
|
+
this.cookies = session.cookies;
|
|
50
|
+
// Silent - no console output for cached session
|
|
51
|
+
} else {
|
|
52
|
+
// Session expired
|
|
53
|
+
this.clearSession();
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// Corrupted cache file - clear it
|
|
57
|
+
this.clearSession();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Save session to cache file
|
|
63
|
+
*/
|
|
64
|
+
saveSession() {
|
|
65
|
+
// Conservative expiration: 15 minutes
|
|
66
|
+
// (ABAP default session timeout is typically 20 minutes)
|
|
67
|
+
const expiresAt = Date.now() + (15 * 60 * 1000);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
fs.writeFileSync(this.sessionFile, JSON.stringify({
|
|
71
|
+
csrfToken: this.csrfToken,
|
|
72
|
+
cookies: this.cookies,
|
|
73
|
+
expiresAt,
|
|
74
|
+
savedAt: Date.now()
|
|
75
|
+
}));
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// Ignore write errors - session caching is optional
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clear cached session
|
|
83
|
+
*/
|
|
84
|
+
clearSession() {
|
|
85
|
+
this.csrfToken = null;
|
|
86
|
+
this.cookies = null;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
if (fs.existsSync(this.sessionFile)) {
|
|
90
|
+
fs.unlinkSync(this.sessionFile);
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Ignore file deletion errors
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Detect if error is due to expired/invalid session
|
|
99
|
+
* @param {Error|object} error - Error object or response
|
|
100
|
+
* @returns {boolean} True if auth error
|
|
101
|
+
*/
|
|
102
|
+
isAuthError(error) {
|
|
103
|
+
// HTTP status codes
|
|
104
|
+
if (error.statusCode === 401) return true; // Unauthorized
|
|
105
|
+
if (error.statusCode === 403) return true; // Forbidden
|
|
106
|
+
|
|
107
|
+
// Error message patterns
|
|
108
|
+
const message = (error.message || error.error || '').toLowerCase();
|
|
109
|
+
if (message.includes('csrf')) return true;
|
|
110
|
+
if (message.includes('token')) return true;
|
|
111
|
+
if (message.includes('session')) return true;
|
|
112
|
+
if (message.includes('expired')) return true;
|
|
113
|
+
if (message.includes('unauthorized')) return true;
|
|
114
|
+
if (message.includes('forbidden')) return true;
|
|
115
|
+
|
|
116
|
+
// Response body patterns
|
|
117
|
+
if (error.body) {
|
|
118
|
+
const bodyStr = JSON.stringify(error.body).toLowerCase();
|
|
119
|
+
if (bodyStr.includes('csrf')) return true;
|
|
120
|
+
if (bodyStr.includes('session')) return true;
|
|
121
|
+
if (bodyStr.includes('expired')) return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fetch CSRF token using GET /health with X-CSRF-Token: fetch
|
|
129
|
+
* @returns {Promise<string>} CSRF token
|
|
130
|
+
*/
|
|
131
|
+
async fetchCsrfToken() {
|
|
132
|
+
const url = new URL(`/sap/bc/z_abapgit_agent/health`, `https://${this.config.host}:${this.config.sapport}`);
|
|
133
|
+
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const options = {
|
|
136
|
+
hostname: url.hostname,
|
|
137
|
+
port: url.port,
|
|
138
|
+
path: url.pathname,
|
|
139
|
+
method: 'GET',
|
|
140
|
+
headers: {
|
|
141
|
+
'Authorization': `Basic ${Buffer.from(`${this.config.user}:${this.config.password}`).toString('base64')}`,
|
|
142
|
+
'sap-client': this.config.client,
|
|
143
|
+
'sap-language': this.config.language || 'EN',
|
|
144
|
+
'X-CSRF-Token': 'fetch',
|
|
145
|
+
'Content-Type': 'application/json'
|
|
146
|
+
},
|
|
147
|
+
agent: new https.Agent({ rejectUnauthorized: false })
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const req = https.request(options, (res) => {
|
|
151
|
+
const csrfToken = res.headers['x-csrf-token'];
|
|
152
|
+
|
|
153
|
+
// Save cookies from response
|
|
154
|
+
const setCookie = res.headers['set-cookie'];
|
|
155
|
+
if (setCookie) {
|
|
156
|
+
this.cookies = Array.isArray(setCookie)
|
|
157
|
+
? setCookie.map(c => c.split(';')[0]).join('; ')
|
|
158
|
+
: setCookie.split(';')[0];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let body = '';
|
|
162
|
+
res.on('data', chunk => body += chunk);
|
|
163
|
+
res.on('end', () => {
|
|
164
|
+
this.csrfToken = csrfToken;
|
|
165
|
+
|
|
166
|
+
// Save session to cache
|
|
167
|
+
this.saveSession();
|
|
168
|
+
|
|
169
|
+
resolve(csrfToken);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
req.on('error', reject);
|
|
174
|
+
req.end();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Make HTTP request to ABAP REST endpoint with automatic retry on auth failure
|
|
180
|
+
* @param {string} method - HTTP method (GET, POST, DELETE)
|
|
181
|
+
* @param {string} urlPath - URL path
|
|
182
|
+
* @param {object} data - Request body (for POST)
|
|
183
|
+
* @param {object} options - Additional options (headers, csrfToken, isRetry)
|
|
184
|
+
* @returns {Promise<object>} Response JSON
|
|
185
|
+
*/
|
|
186
|
+
async request(method, urlPath, data = null, options = {}) {
|
|
187
|
+
try {
|
|
188
|
+
// Try with current session (cached or fresh)
|
|
189
|
+
return await this._makeRequest(method, urlPath, data, options);
|
|
190
|
+
|
|
191
|
+
} catch (error) {
|
|
192
|
+
// Check if it's an authentication/authorization failure
|
|
193
|
+
if (this.isAuthError(error) && !options.isRetry) {
|
|
194
|
+
// Session expired - refresh and retry once
|
|
195
|
+
console.error('⚠️ Session expired, refreshing...');
|
|
196
|
+
|
|
197
|
+
// Clear stale session
|
|
198
|
+
this.clearSession();
|
|
199
|
+
|
|
200
|
+
// Fetch fresh token/cookies
|
|
201
|
+
await this.fetchCsrfToken();
|
|
202
|
+
|
|
203
|
+
// Retry ONCE with fresh session
|
|
204
|
+
return await this._makeRequest(method, urlPath, data, {
|
|
205
|
+
...options,
|
|
206
|
+
isRetry: true // Prevent infinite loop
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Not an auth error or already retried - propagate
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Internal request implementation (no retry logic)
|
|
217
|
+
* @param {string} method - HTTP method
|
|
218
|
+
* @param {string} urlPath - URL path
|
|
219
|
+
* @param {object} data - Request body
|
|
220
|
+
* @param {object} options - Additional options
|
|
221
|
+
* @returns {Promise<object>} Response JSON
|
|
222
|
+
*/
|
|
223
|
+
async _makeRequest(method, urlPath, data = null, options = {}) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const url = new URL(urlPath, `https://${this.config.host}:${this.config.sapport}`);
|
|
226
|
+
|
|
227
|
+
const headers = {
|
|
228
|
+
'Content-Type': 'application/json',
|
|
229
|
+
'sap-client': this.config.client,
|
|
230
|
+
'sap-language': this.config.language || 'EN',
|
|
231
|
+
...options.headers
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Add authorization
|
|
235
|
+
headers['Authorization'] = `Basic ${Buffer.from(`${this.config.user}:${this.config.password}`).toString('base64')}`;
|
|
236
|
+
|
|
237
|
+
// Add CSRF token for POST
|
|
238
|
+
if (method === 'POST' && options.csrfToken) {
|
|
239
|
+
headers['X-CSRF-Token'] = options.csrfToken;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Add cookies if available
|
|
243
|
+
if (this.cookies) {
|
|
244
|
+
headers['Cookie'] = this.cookies;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const reqOptions = {
|
|
248
|
+
hostname: url.hostname,
|
|
249
|
+
port: url.port,
|
|
250
|
+
path: url.pathname + url.search,
|
|
251
|
+
method,
|
|
252
|
+
headers,
|
|
253
|
+
agent: new https.Agent({ rejectUnauthorized: false })
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (res) => {
|
|
257
|
+
// Check for auth errors
|
|
258
|
+
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
259
|
+
reject({
|
|
260
|
+
statusCode: res.statusCode,
|
|
261
|
+
message: `Authentication failed: ${res.statusCode}`,
|
|
262
|
+
isAuthError: true
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let body = '';
|
|
268
|
+
res.on('data', chunk => body += chunk);
|
|
269
|
+
res.on('end', () => {
|
|
270
|
+
try {
|
|
271
|
+
// Handle unescaped newlines from ABAP - replace actual newlines with \n
|
|
272
|
+
const cleanedBody = body.replace(/\n/g, '\\n');
|
|
273
|
+
const parsed = JSON.parse(cleanedBody);
|
|
274
|
+
|
|
275
|
+
// Check for CSRF/session errors in response body
|
|
276
|
+
if (this.isAuthError(parsed)) {
|
|
277
|
+
reject({
|
|
278
|
+
statusCode: res.statusCode,
|
|
279
|
+
message: 'CSRF token or session error',
|
|
280
|
+
body: parsed,
|
|
281
|
+
isAuthError: true
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
resolve(parsed);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
// Fallback: try to extract JSON from response
|
|
289
|
+
const jsonMatch = body.match(/\{[\s\S]*\}/);
|
|
290
|
+
if (jsonMatch) {
|
|
291
|
+
try {
|
|
292
|
+
const parsed = JSON.parse(jsonMatch[0].replace(/\n/g, '\\n'));
|
|
293
|
+
resolve(parsed);
|
|
294
|
+
} catch (e2) {
|
|
295
|
+
resolve({ raw: body, error: e2.message });
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
resolve({ raw: body, error: e.message });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
req.on('error', reject);
|
|
305
|
+
|
|
306
|
+
if (data) {
|
|
307
|
+
req.write(JSON.stringify(data));
|
|
308
|
+
}
|
|
309
|
+
req.end();
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Convenience method for GET requests
|
|
315
|
+
* @param {string} urlPath - URL path
|
|
316
|
+
* @param {object} options - Additional options
|
|
317
|
+
* @returns {Promise<object>} Response JSON
|
|
318
|
+
*/
|
|
319
|
+
async get(urlPath, options = {}) {
|
|
320
|
+
return this.request('GET', urlPath, null, options);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Convenience method for POST requests
|
|
325
|
+
* @param {string} urlPath - URL path
|
|
326
|
+
* @param {object} data - Request body
|
|
327
|
+
* @param {object} options - Additional options (must include csrfToken)
|
|
328
|
+
* @returns {Promise<object>} Response JSON
|
|
329
|
+
*/
|
|
330
|
+
async post(urlPath, data, options = {}) {
|
|
331
|
+
return this.request('POST', urlPath, data, options);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Convenience method for DELETE requests
|
|
336
|
+
* @param {string} urlPath - URL path
|
|
337
|
+
* @param {object} options - Additional options
|
|
338
|
+
* @returns {Promise<object>} Response JSON
|
|
339
|
+
*/
|
|
340
|
+
async delete(urlPath, options = {}) {
|
|
341
|
+
return this.request('DELETE', urlPath, null, options);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
AbapHttp
|
|
347
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-related utilities
|
|
3
|
+
*/
|
|
4
|
+
const pathModule = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get git remote URL from .git/config
|
|
9
|
+
* @returns {string|null} Git remote URL or null if not found
|
|
10
|
+
*/
|
|
11
|
+
function getRemoteUrl() {
|
|
12
|
+
const gitConfigPath = pathModule.join(process.cwd(), '.git', 'config');
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(gitConfigPath)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const content = fs.readFileSync(gitConfigPath, 'utf8');
|
|
19
|
+
const remoteMatch = content.match(/\[remote "origin"\]/);
|
|
20
|
+
|
|
21
|
+
if (!remoteMatch) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const urlMatch = content.match(/ url = (.+)/);
|
|
26
|
+
return urlMatch ? urlMatch[1].trim() : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get current git branch from .git/HEAD
|
|
31
|
+
* @returns {string} Current branch name or 'main' as default
|
|
32
|
+
*/
|
|
33
|
+
function getBranch() {
|
|
34
|
+
const headPath = pathModule.join(process.cwd(), '.git', 'HEAD');
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(headPath)) {
|
|
37
|
+
return 'main';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content = fs.readFileSync(headPath, 'utf8').trim();
|
|
41
|
+
const match = content.match(/ref: refs\/heads\/(.+)/);
|
|
42
|
+
return match ? match[1] : 'main';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if current directory is a git repository
|
|
47
|
+
* @returns {boolean} True if .git directory exists
|
|
48
|
+
*/
|
|
49
|
+
function isGitRepo() {
|
|
50
|
+
const gitPath = pathModule.join(process.cwd(), '.git');
|
|
51
|
+
return fs.existsSync(gitPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
getRemoteUrl,
|
|
56
|
+
getBranch,
|
|
57
|
+
isGitRepo
|
|
58
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert ISO date formats (YYYY-MM-DD) to ABAP DATS format (YYYYMMDD) in WHERE clause
|
|
7
|
+
* This allows users to use familiar ISO date formats while ensuring compatibility with ABAP SQL
|
|
8
|
+
* @param {string} whereClause - SQL WHERE clause
|
|
9
|
+
* @returns {string} - WHERE clause with dates converted to YYYYMMDD format
|
|
10
|
+
*/
|
|
11
|
+
function convertDatesInWhereClause(whereClause) {
|
|
12
|
+
if (!whereClause) return whereClause;
|
|
13
|
+
|
|
14
|
+
// Pattern to match ISO date format: 'YYYY-MM-DD'
|
|
15
|
+
const isoDatePattern = /'\d{4}-\d{2}-\d{2}'/g;
|
|
16
|
+
|
|
17
|
+
return whereClause.replace(isoDatePattern, (match) => {
|
|
18
|
+
// Extract YYYY, MM, DD from 'YYYY-MM-DD'
|
|
19
|
+
const dateContent = match.slice(1, -1); // Remove quotes: YYYY-MM-DD
|
|
20
|
+
const [year, month, day] = dateContent.split('-');
|
|
21
|
+
// Return in ABAP format: 'YYYYMMDD'
|
|
22
|
+
return `'${year}${month}${day}'`;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate package name format
|
|
28
|
+
* @param {string} packageName - ABAP package name
|
|
29
|
+
* @returns {boolean} True if valid
|
|
30
|
+
*/
|
|
31
|
+
function isValidPackageName(packageName) {
|
|
32
|
+
if (!packageName) return false;
|
|
33
|
+
// Package names: $PACKAGE or ZPACKAGE (can contain _ and alphanumeric)
|
|
34
|
+
return /^(\$|Z|Y)[A-Z0-9_]{0,29}$/i.test(packageName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate object name format
|
|
39
|
+
* @param {string} objectName - ABAP object name
|
|
40
|
+
* @returns {boolean} True if valid
|
|
41
|
+
*/
|
|
42
|
+
function isValidObjectName(objectName) {
|
|
43
|
+
if (!objectName) return false;
|
|
44
|
+
// Object names: up to 30 characters, alphanumeric + underscore
|
|
45
|
+
return /^[A-Z0-9_/]{1,30}$/i.test(objectName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse file path to extract object type and name
|
|
50
|
+
* @param {string} filePath - File path (e.g., "src/zcl_my_class.clas.abap")
|
|
51
|
+
* @returns {object|null} {type, name} or null if invalid
|
|
52
|
+
*/
|
|
53
|
+
function parseObjectFromFile(filePath) {
|
|
54
|
+
const fileName = filePath.split('/').pop();
|
|
55
|
+
|
|
56
|
+
// Match patterns: zcl_class.clas.abap, zif_intf.intf.abap, etc.
|
|
57
|
+
const match = fileName.match(/^([a-z0-9_]+)\.(clas|intf|prog|fugr|ddls|tabl|dtel|ttyp|stru)\..*$/i);
|
|
58
|
+
|
|
59
|
+
if (!match) return null;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
name: match[1].toUpperCase(),
|
|
63
|
+
type: match[2].toUpperCase()
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
convertDatesInWhereClause,
|
|
69
|
+
isValidPackageName,
|
|
70
|
+
isValidObjectName,
|
|
71
|
+
parseObjectFromFile
|
|
72
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI <> ABAP version compatibility check
|
|
3
|
+
*/
|
|
4
|
+
const pathModule = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const https = require('https');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get CLI version from package.json
|
|
10
|
+
* @returns {string} CLI version or '1.0.0' as default
|
|
11
|
+
*/
|
|
12
|
+
function getCliVersion() {
|
|
13
|
+
const packageJsonPath = pathModule.join(__dirname, '..', '..', 'package.json');
|
|
14
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
15
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
16
|
+
return pkg.version || '1.0.0';
|
|
17
|
+
}
|
|
18
|
+
return '1.0.0';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check version compatibility between CLI and ABAP API
|
|
23
|
+
* @param {object} config - ABAP connection config
|
|
24
|
+
* @returns {Promise<object>} Version compatibility result
|
|
25
|
+
*/
|
|
26
|
+
async function checkCompatibility(config) {
|
|
27
|
+
const cliVersion = getCliVersion();
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(`/sap/bc/z_abapgit_agent/health`, `https://${config.host}:${config.sapport}`);
|
|
31
|
+
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const options = {
|
|
34
|
+
hostname: url.hostname,
|
|
35
|
+
port: url.port,
|
|
36
|
+
path: url.pathname,
|
|
37
|
+
method: 'GET',
|
|
38
|
+
headers: {
|
|
39
|
+
'Authorization': `Basic ${Buffer.from(`${config.user}:${config.password}`).toString('base64')}`,
|
|
40
|
+
'sap-client': config.client,
|
|
41
|
+
'sap-language': config.language || 'EN',
|
|
42
|
+
'Content-Type': 'application/json'
|
|
43
|
+
},
|
|
44
|
+
agent: new https.Agent({ rejectUnauthorized: false })
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const req = https.request(options, (res) => {
|
|
48
|
+
let body = '';
|
|
49
|
+
res.on('data', chunk => body += chunk);
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
try {
|
|
52
|
+
const result = JSON.parse(body);
|
|
53
|
+
const apiVersion = result.version || '1.0.0';
|
|
54
|
+
|
|
55
|
+
if (cliVersion !== apiVersion) {
|
|
56
|
+
console.log(`\n⚠️ Version mismatch: CLI ${cliVersion}, ABAP API ${apiVersion}`);
|
|
57
|
+
console.log(' Some commands may not work correctly.');
|
|
58
|
+
console.log(' Update ABAP code: abapgit-agent pull\n');
|
|
59
|
+
}
|
|
60
|
+
resolve({ cliVersion, apiVersion, compatible: cliVersion === apiVersion });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
resolve({ cliVersion, apiVersion: null, compatible: false, error: e.message });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
req.on('error', (e) => {
|
|
68
|
+
resolve({ cliVersion, apiVersion: null, compatible: false, error: e.message });
|
|
69
|
+
});
|
|
70
|
+
req.end();
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return { cliVersion, apiVersion: null, compatible: false, error: error.message };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = {
|
|
78
|
+
getCliVersion,
|
|
79
|
+
checkCompatibility
|
|
80
|
+
};
|