abapgit-agent 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.abapGitAgent.example +11 -0
  2. package/README.md +7 -7
  3. package/abap/.github/copilot-instructions.md +254 -0
  4. package/abap/CLAUDE.md +432 -0
  5. package/abap/guidelines/00_index.md +8 -0
  6. package/abap/guidelines/01_sql.md +8 -0
  7. package/abap/guidelines/02_exceptions.md +8 -0
  8. package/abap/guidelines/03_testing.md +8 -0
  9. package/abap/guidelines/04_cds.md +8 -0
  10. package/abap/guidelines/05_classes.md +8 -0
  11. package/abap/guidelines/06_objects.md +8 -0
  12. package/abap/guidelines/07_json.md +8 -0
  13. package/abap/guidelines/08_abapgit.md +8 -0
  14. package/abap/guidelines/09_unit_testable_code.md +8 -0
  15. package/bin/abapgit-agent +61 -2789
  16. package/package.json +25 -5
  17. package/src/agent.js +213 -20
  18. package/src/commands/create.js +102 -0
  19. package/src/commands/delete.js +72 -0
  20. package/src/commands/health.js +24 -0
  21. package/src/commands/help.js +111 -0
  22. package/src/commands/import.js +99 -0
  23. package/src/commands/init.js +321 -0
  24. package/src/commands/inspect.js +184 -0
  25. package/src/commands/list.js +143 -0
  26. package/src/commands/preview.js +277 -0
  27. package/src/commands/pull.js +278 -0
  28. package/src/commands/ref.js +96 -0
  29. package/src/commands/status.js +52 -0
  30. package/src/commands/syntax.js +290 -0
  31. package/src/commands/tree.js +209 -0
  32. package/src/commands/unit.js +133 -0
  33. package/src/commands/view.js +215 -0
  34. package/src/commands/where.js +138 -0
  35. package/src/config.js +11 -1
  36. package/src/utils/abap-http.js +347 -0
  37. package/src/{ref-search.js → utils/abap-reference.js} +119 -1
  38. package/src/utils/git-utils.js +58 -0
  39. package/src/utils/validators.js +72 -0
  40. package/src/utils/version-check.js +80 -0
  41. package/src/abap-client.js +0 -523
@@ -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
+ };
@@ -8,6 +8,7 @@
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const { promisify } = require('util');
11
+ const { execSync } = require('child_process');
11
12
  const readdir = promisify(fs.readdir);
12
13
  const readFile = promisify(fs.readFile);
13
14
  const stat = promisify(fs.stat);
@@ -63,6 +64,120 @@ const TOPIC_MAP = {
63
64
  'tables': '01_Internal_Tables.md'
64
65
  };
65
66
 
67
+ /**
68
+ * Ensure reference folder exists, create if necessary
69
+ * @returns {string|null} Path to reference folder or null if cannot create
70
+ */
71
+ function ensureReferenceFolder() {
72
+ let refFolder = detectReferenceFolder();
73
+
74
+ if (refFolder && fs.existsSync(refFolder)) {
75
+ return refFolder;
76
+ }
77
+
78
+ // Try to create the reference folder at default location
79
+ const homeDir = require('os').homedir();
80
+ const defaultPath = path.join(homeDir, 'abap-reference');
81
+
82
+ try {
83
+ if (!fs.existsSync(defaultPath)) {
84
+ fs.mkdirSync(defaultPath, { recursive: true });
85
+ }
86
+ return defaultPath;
87
+ } catch (error) {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Clone a repository to the reference folder
94
+ * @param {string} repoUrl - Git repository URL or short name (e.g., "SAP-samples/abap-cheat-sheets")
95
+ * @param {string|null} name - Optional folder name for the cloned repo
96
+ * @returns {Object} Clone result
97
+ */
98
+ function cloneRepository(repoUrl, name = null) {
99
+ const refFolder = ensureReferenceFolder();
100
+
101
+ if (!refFolder) {
102
+ return {
103
+ success: false,
104
+ error: 'Could not create reference folder',
105
+ hint: 'Set referenceFolder in .abapGitAgent or ensure ~/abap-reference is writable'
106
+ };
107
+ }
108
+
109
+ // Parse the repo URL
110
+ let targetName = name;
111
+ let cloneUrl = repoUrl;
112
+
113
+ // Handle short names (e.g., "SAP-samples/abap-cheat-sheets")
114
+ if (!repoUrl.startsWith('http://') && !repoUrl.startsWith('https://') && !repoUrl.startsWith('git@')) {
115
+ cloneUrl = `https://github.com/${repoUrl}.git`;
116
+ }
117
+
118
+ // If no name provided, extract from URL
119
+ if (!targetName) {
120
+ // Extract repo name from URL
121
+ const urlParts = cloneUrl.split('/');
122
+ const repoWithGit = urlParts[urlParts.length - 1];
123
+ targetName = repoWithGit.replace(/\.git$/, '');
124
+ }
125
+
126
+ const targetPath = path.join(refFolder, targetName);
127
+
128
+ // Check if already exists
129
+ if (fs.existsSync(targetPath)) {
130
+ return {
131
+ success: false,
132
+ error: `Repository already exists: ${targetName}`,
133
+ hint: `Delete '${targetPath}' to re-clone, or use --name to specify a different folder name`,
134
+ existingPath: targetPath
135
+ };
136
+ }
137
+
138
+ try {
139
+ // Run git clone
140
+ execSync(`git clone "${cloneUrl}" "${targetPath}"`, {
141
+ stdio: 'pipe',
142
+ encoding: 'utf8'
143
+ });
144
+
145
+ return {
146
+ success: true,
147
+ message: `Successfully cloned ${repoUrl}`,
148
+ repository: targetName,
149
+ folder: targetPath,
150
+ referenceFolder: refFolder
151
+ };
152
+ } catch (error) {
153
+ return {
154
+ success: false,
155
+ error: `Failed to clone: ${error.message}`,
156
+ hint: 'Check the repository URL and your network connection'
157
+ };
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Display clone result in console format
163
+ * @param {Object} result - Clone result
164
+ */
165
+ function displayCloneResult(result) {
166
+ if (result.success) {
167
+ console.log(`\n ✅ ${result.message}`);
168
+ console.log(`\n 📁 Repository: ${result.repository}`);
169
+ console.log(` 📁 Location: ${result.folder}`);
170
+ console.log(` 📁 Reference folder: ${result.referenceFolder}`);
171
+ console.log(`\n 💡 You can now search this repository with:`);
172
+ console.log(` abapgit-agent ref --list-repos`);
173
+ } else {
174
+ console.error(`\n ❌ ${result.error}`);
175
+ if (result.hint) {
176
+ console.error(`\n 💡 ${result.hint}`);
177
+ }
178
+ }
179
+ }
180
+
66
181
  /**
67
182
  * Detect reference folder from config or common locations
68
183
  * @returns {string|null} Path to reference folder or null if not found
@@ -1005,7 +1120,7 @@ function displayInitResult(result) {
1005
1120
  console.log(`\n 💡 Next steps:`);
1006
1121
  console.log(` 1. Review the guidelines in abap/guidelines/`);
1007
1122
  console.log(` 2. Customize as needed for your project`);
1008
- console.log(` 3. Run 'abapgit-agent ref --export' to make them searchable`);
1123
+ console.log(` 3. Guidelines are automatically searchable with 'ref' command`);
1009
1124
  } else {
1010
1125
  console.error(`\n ❌ ${result.error}`);
1011
1126
  if (result.hint) {
@@ -1016,6 +1131,7 @@ function displayInitResult(result) {
1016
1131
 
1017
1132
  module.exports = {
1018
1133
  detectReferenceFolder,
1134
+ ensureReferenceFolder,
1019
1135
  detectGuidelinesFolder,
1020
1136
  getBuiltInGuidelinesPath,
1021
1137
  initGuidelines,
@@ -1027,11 +1143,13 @@ module.exports = {
1027
1143
  listTopics,
1028
1144
  listRepositories,
1029
1145
  exportGuidelines,
1146
+ cloneRepository,
1030
1147
  displaySearchResults,
1031
1148
  displayTopic,
1032
1149
  displayTopics,
1033
1150
  displayRepositories,
1034
1151
  displayExportResult,
1035
1152
  displayInitResult,
1153
+ displayCloneResult,
1036
1154
  TOPIC_MAP
1037
1155
  };
@@ -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
+ };