abapgit-agent 1.8.4 → 1.8.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.8.4",
3
+ "version": "1.8.6",
4
4
  "description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -2,6 +2,14 @@
2
2
  * Import command - Import existing objects from package to git repository
3
3
  */
4
4
 
5
+ const {
6
+ startBackgroundJob,
7
+ pollForCompletion,
8
+ displayProgress,
9
+ formatTimestamp,
10
+ calculateTimeSpent
11
+ } = require('../utils/backgroundJobPoller');
12
+
5
13
  module.exports = {
6
14
  name: 'import',
7
15
  description: 'Import existing objects from package to git repository',
@@ -9,9 +17,10 @@ module.exports = {
9
17
  requiresVersionCheck: true,
10
18
 
11
19
  async execute(args, context) {
12
- const { loadConfig, gitUtils, AbapHttp } = context;
20
+ try {
21
+ const { loadConfig, gitUtils, AbapHttp } = context;
13
22
 
14
- // Show help if requested
23
+ // Show help if requested
15
24
  const helpIndex = args.findIndex(a => a === '--help' || a === '-h');
16
25
  if (helpIndex !== -1) {
17
26
  console.log(`
@@ -22,6 +31,9 @@ Description:
22
31
  Import existing objects from package to git repository.
23
32
  Uses the git remote URL to find the abapGit online repository.
24
33
 
34
+ This command runs asynchronously using a background job and displays
35
+ real-time progress updates.
36
+
25
37
  Prerequisites:
26
38
  - Run "abapgit-agent create" first or create repository in abapGit UI
27
39
  - Package must have objects to import
@@ -51,7 +63,7 @@ Examples:
51
63
  commitMessage = args[messageArgIndex + 1];
52
64
  }
53
65
 
54
- console.log(`\nšŸ“¦ Importing objects to git repository`);
66
+ console.log(`\nšŸ“¦ Starting import job`);
55
67
  console.log(` URL: ${repoUrl}`);
56
68
  if (commitMessage) {
57
69
  console.log(` Message: ${commitMessage}`);
@@ -76,23 +88,69 @@ Examples:
76
88
  data.password = config.gitPassword;
77
89
  }
78
90
 
79
- const result = await http.post('/sap/bc/z_abapgit_agent/import', data, { csrfToken });
91
+ // Step 1: Start the background job
92
+ const endpoint = '/sap/bc/z_abapgit_agent/import';
93
+ const jobInfo = await startBackgroundJob(http, endpoint, data, csrfToken);
80
94
 
81
- console.log('\n');
95
+ console.log(`āœ… Job started: ${jobInfo.jobNumber}`);
96
+ console.log('');
82
97
 
83
- // Handle uppercase keys from ABAP
84
- const success = result.SUCCESS || result.success;
85
- const filesStaged = result.FILES_STAGED || result.files_staged;
86
- const abapCommitMessage = result.COMMIT_MESSAGE || result.commit_message;
87
- const error = result.ERROR || result.error;
98
+ // Step 2: Poll for completion with progress updates
99
+ const finalResult = await pollForCompletion(http, endpoint, jobInfo.jobNumber, {
100
+ pollInterval: 2000,
101
+ maxAttempts: 300,
102
+ onProgress: (progress, message) => {
103
+ displayProgress(progress, message);
104
+ }
105
+ });
88
106
 
89
- if (success === 'X' || success === true) {
90
- console.log(`āœ… Objects imported successfully!`);
91
- console.log(` Files staged: ${filesStaged}`);
92
- console.log(` Commit: ${commitMessage || abapCommitMessage}`);
107
+ // Step 3: Show final result
108
+ console.log('\n');
109
+
110
+ if (finalResult.status === 'completed' && finalResult.result) {
111
+ // Parse result JSON string
112
+ let resultData;
113
+ try {
114
+ if (typeof finalResult.result === 'string') {
115
+ resultData = JSON.parse(finalResult.result);
116
+ } else {
117
+ resultData = finalResult.result;
118
+ }
119
+ } catch (e) {
120
+ resultData = { filesStaged: 'unknown', commitMessage: commitMessage };
121
+ }
122
+
123
+ console.log(`āœ… Import completed successfully!`);
124
+ console.log(` Files staged: ${resultData.filesStaged || resultData.FILES_STAGED || 'unknown'}`);
125
+ console.log(` Commit: ${resultData.commitMessage || resultData.COMMIT_MESSAGE || commitMessage || 'Initial import'}`);
126
+ console.log(``);
127
+
128
+ // Calculate time spent
129
+ if (finalResult.startedAt && finalResult.completedAt) {
130
+ const timeSpent = calculateTimeSpent(finalResult.startedAt, finalResult.completedAt);
131
+ console.log(`ā±ļø Time spent: ${timeSpent}`);
132
+ }
133
+
134
+ console.log(`šŸ“ˆ Stats:`);
135
+ console.log(` Job number: ${jobInfo.jobNumber}`);
136
+ if (finalResult.startedAt) {
137
+ console.log(` Started: ${formatTimestamp(finalResult.startedAt)}`);
138
+ }
139
+ if (finalResult.completedAt) {
140
+ console.log(` Completed: ${formatTimestamp(finalResult.completedAt)}`);
141
+ }
93
142
  } else {
94
143
  console.log(`āŒ Import failed`);
95
- console.log(` Error: ${error || 'Unknown error'}`);
144
+ console.log(` Status: ${finalResult.status}`);
145
+ process.exit(1);
146
+ }
147
+ } catch (error) {
148
+ console.error('\nāŒ Error during import:');
149
+ console.error(` ${error.message || error}`);
150
+ if (error.response) {
151
+ console.error(` HTTP Status: ${error.response.status}`);
152
+ console.error(` Response: ${JSON.stringify(error.response.data)}`);
153
+ }
96
154
  process.exit(1);
97
155
  }
98
156
  }
@@ -71,7 +71,11 @@ module.exports = {
71
71
  // Check if object was not found
72
72
  if (notFound) {
73
73
  console.log(` āŒ ${objName} (${objTypeText})`);
74
- console.log(` Object not found: ${objName}`);
74
+ if (description) {
75
+ console.log(` ${description}`);
76
+ } else {
77
+ console.log(` Object not found: ${objName}`);
78
+ }
75
79
  continue;
76
80
  }
77
81
 
@@ -80,9 +84,9 @@ module.exports = {
80
84
  console.log(` ${description}`);
81
85
  }
82
86
 
83
- // Display source code for classes, interfaces, CDS views, and programs/source includes
87
+ // Display source code for classes, interfaces, CDS views, programs/source includes, and STOB
84
88
  const source = obj.SOURCE || obj.source || '';
85
- if (source && (objType === 'INTF' || objType === 'Interface' || objType === 'CLAS' || objType === 'Class' || objType === 'DDLS' || objType === 'CDS View' || objType === 'PROG' || objType === 'Program')) {
89
+ if (source && (objType === 'INTF' || objType === 'Interface' || objType === 'CLAS' || objType === 'Class' || objType === 'DDLS' || objType === 'CDS View' || objType === 'PROG' || objType === 'Program' || objType === 'STOB' || objType === 'Structured Object')) {
86
90
  console.log('');
87
91
  // Replace escaped newlines with actual newlines and display
88
92
  const displaySource = source.replace(/\\n/g, '\n');
@@ -0,0 +1,218 @@
1
+ # Background Job Polling Utility
2
+
3
+ Generic utility for commands that run as ABAP background jobs with progress reporting.
4
+
5
+ ## Overview
6
+
7
+ This utility provides a reusable pattern for CLI commands that need to:
8
+ 1. Start an ABAP background job
9
+ 2. Poll for job status with real-time progress updates
10
+ 3. Display progress bars and final results
11
+
12
+ ## Usage
13
+
14
+ ### Basic Example
15
+
16
+ ```javascript
17
+ const {
18
+ startBackgroundJob,
19
+ pollForCompletion,
20
+ displayProgress,
21
+ formatTimestamp,
22
+ calculateTimeSpent
23
+ } = require('../utils/backgroundJobPoller');
24
+
25
+ // In your command's execute() method:
26
+ async execute(args, context) {
27
+ const { AbapHttp, loadConfig } = context;
28
+ const config = loadConfig();
29
+ const http = new AbapHttp(config);
30
+ const csrfToken = await http.fetchCsrfToken();
31
+
32
+ // 1. Start background job
33
+ const endpoint = '/sap/bc/z_abapgit_agent/mycommand';
34
+ const jobInfo = await startBackgroundJob(http, endpoint, data, csrfToken);
35
+
36
+ console.log(`āœ… Job started: ${jobInfo.jobNumber}`);
37
+
38
+ // 2. Poll for completion
39
+ const finalResult = await pollForCompletion(http, endpoint, jobInfo.jobNumber, {
40
+ pollInterval: 2000,
41
+ maxAttempts: 300,
42
+ onProgress: (progress, message) => {
43
+ displayProgress(progress, message);
44
+ }
45
+ });
46
+
47
+ // 3. Show final result
48
+ console.log('\n');
49
+ if (finalResult.status === 'completed') {
50
+ console.log(`āœ… Command completed successfully!`);
51
+
52
+ const timeSpent = calculateTimeSpent(finalResult.startedAt, finalResult.completedAt);
53
+ console.log(`ā±ļø Time spent: ${timeSpent}`);
54
+ console.log(` Started: ${formatTimestamp(finalResult.startedAt)}`);
55
+ console.log(` Completed: ${formatTimestamp(finalResult.completedAt)}`);
56
+ }
57
+ }
58
+ ```
59
+
60
+ ## API Reference
61
+
62
+ ### `startBackgroundJob(http, endpoint, data, csrfToken)`
63
+
64
+ Start a background job.
65
+
66
+ **Parameters:**
67
+ - `http` - AbapHttp instance
68
+ - `endpoint` - API endpoint (e.g., `/sap/bc/z_abapgit_agent/import`)
69
+ - `data` - Request data object
70
+ - `csrfToken` - CSRF token from `http.fetchCsrfToken()`
71
+
72
+ **Returns:** `{ jobNumber, jobName, status }`
73
+
74
+ **Throws:** Error if job failed to start
75
+
76
+ ### `pollForCompletion(http, endpoint, jobNumber, options)`
77
+
78
+ Poll for job completion with progress updates.
79
+
80
+ **Parameters:**
81
+ - `http` - AbapHttp instance
82
+ - `endpoint` - Status endpoint (same as start endpoint)
83
+ - `jobNumber` - Job number from `startBackgroundJob()`
84
+ - `options` - Polling options:
85
+ - `pollInterval` - Milliseconds between polls (default: 2000)
86
+ - `maxAttempts` - Max polling attempts (default: 300 = 10 minutes)
87
+ - `onProgress(progress, message)` - Callback for progress updates
88
+
89
+ **Returns:**
90
+ ```javascript
91
+ {
92
+ status: 'completed',
93
+ result: '{"filesStaged": 100}', // JSON string or object
94
+ startedAt: '20260305103045',
95
+ completedAt: '20260305103520',
96
+ jobNumber: '12345678',
97
+ jobName: 'IMPORT_20260305103045'
98
+ }
99
+ ```
100
+
101
+ **Throws:** Error if job fails or times out
102
+
103
+ ### `displayProgress(progress, message, options)`
104
+
105
+ Display progress bar in console.
106
+
107
+ **Parameters:**
108
+ - `progress` - Progress percentage (0-100)
109
+ - `message` - Progress message
110
+ - `options` - Display options:
111
+ - `barWidth` - Width of progress bar (default: 30)
112
+ - `showPercentage` - Show percentage number (default: false)
113
+
114
+ **Example Output:**
115
+ ```
116
+ [=============== ] Staging files (1250 of 3701)
117
+ ```
118
+
119
+ ### `formatTimestamp(timestamp)`
120
+
121
+ Format ABAP timestamp to readable local format.
122
+
123
+ **Parameters:**
124
+ - `timestamp` - ABAP timestamp (YYYYMMDDHHMMSS)
125
+
126
+ **Returns:** Formatted timestamp (YYYY-MM-DD HH:MM:SS)
127
+
128
+ **Example:**
129
+ ```javascript
130
+ formatTimestamp('20260305103045')
131
+ // => "2026-03-05 10:30:45"
132
+ ```
133
+
134
+ ### `calculateTimeSpent(startTimestamp, endTimestamp)`
135
+
136
+ Calculate time difference between two ABAP timestamps.
137
+
138
+ **Parameters:**
139
+ - `startTimestamp` - Start timestamp (YYYYMMDDHHMMSS)
140
+ - `endTimestamp` - End timestamp (YYYYMMDDHHMMSS)
141
+
142
+ **Returns:** Human-readable duration
143
+
144
+ **Examples:**
145
+ ```javascript
146
+ calculateTimeSpent('20260305103000', '20260305103015')
147
+ // => "15 seconds"
148
+
149
+ calculateTimeSpent('20260305103000', '20260305103145')
150
+ // => "1 minute 45 seconds"
151
+
152
+ calculateTimeSpent('20260305103000', '20260305123015')
153
+ // => "2 hours 0 minutes"
154
+ ```
155
+
156
+ ## ABAP Requirements
157
+
158
+ For a command to use this utility, the ABAP side must:
159
+
160
+ 1. **Implement `zif_abgagt_progressable` interface** on the command class:
161
+ ```abap
162
+ CLASS zcl_abgagt_command_mycommand DEFINITION.
163
+ PUBLIC SECTION.
164
+ INTERFACES zif_abgagt_command.
165
+ INTERFACES zif_abgagt_progressable. " ← Add this
166
+ ENDCLASS.
167
+ ```
168
+
169
+ 2. **Raise progress events** during execution:
170
+ ```abap
171
+ METHOD zif_abgagt_progressable~execute_with_progress.
172
+ " Update progress
173
+ RAISE EVENT zif_abgagt_progressable~progress_update
174
+ EXPORTING
175
+ iv_stage = 'PROCESSING'
176
+ iv_message = 'Processing items'
177
+ iv_progress = 50
178
+ iv_current = 500
179
+ iv_total = 1000.
180
+ ENDMETHOD.
181
+ ```
182
+
183
+ 3. **Return structured result** in JSON format:
184
+ ```abap
185
+ " Final result
186
+ ls_result = VALUE #(
187
+ success = abap_true
188
+ items_processed = 1000
189
+ message = 'Processing completed'
190
+ ).
191
+ ```
192
+
193
+ The background job infrastructure will automatically:
194
+ - āœ… Detect the `progressable` interface
195
+ - āœ… Schedule the command as a background job
196
+ - āœ… Listen to progress events and update status
197
+ - āœ… Return HTTP 202 Accepted with job number
198
+ - āœ… Support status polling via GET endpoint
199
+
200
+ ## Example Commands Using This Utility
201
+
202
+ - **import** - Import objects from ABAP package to git (already implemented)
203
+ - **pull** - Could be enhanced to support async execution for large file sets
204
+ - **inspect** - Could be enhanced for package-wide inspection
205
+
206
+ ## Testing
207
+
208
+ Unit tests are available in `tests/unit/backgroundJobPoller.test.js`:
209
+
210
+ ```bash
211
+ npm test -- tests/unit/backgroundJobPoller.test.js
212
+ ```
213
+
214
+ ## See Also
215
+
216
+ - [Background Job Architecture](../../docs/architecture/README.md) - Complete architecture documentation
217
+ - [Import Command](../commands/import.js) - Reference implementation
218
+ - [API Documentation](../../API.md) - REST API for async commands
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Generic background job polling utility
3
+ *
4
+ * Provides reusable functions for commands that run as background jobs
5
+ * with progress reporting.
6
+ */
7
+
8
+ /**
9
+ * Start a background job for a command
10
+ *
11
+ * @param {Object} http - AbapHttp instance
12
+ * @param {string} endpoint - API endpoint (e.g., '/sap/bc/z_abapgit_agent/import')
13
+ * @param {Object} data - Request data
14
+ * @param {Object} csrfToken - CSRF token
15
+ * @returns {Object} Job info with jobNumber, jobName
16
+ * @throws {Error} If job failed to start
17
+ */
18
+ async function startBackgroundJob(http, endpoint, data, csrfToken) {
19
+ const result = await http.post(endpoint, data, { csrfToken });
20
+
21
+ const success = result.SUCCESS || result.success;
22
+ const jobNumber = result.JOB_NUMBER || result.jobNumber;
23
+ const jobName = result.JOB_NAME || result.jobName;
24
+ const error = result.ERROR || result.error;
25
+
26
+ if (success !== 'X' && success !== true) {
27
+ throw new Error(error || 'Failed to start background job');
28
+ }
29
+
30
+ return {
31
+ jobNumber,
32
+ jobName,
33
+ status: result.STATUS || result.status
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Poll for job completion with progress updates
39
+ *
40
+ * @param {Object} http - AbapHttp instance
41
+ * @param {string} endpoint - Status endpoint (e.g., '/sap/bc/z_abapgit_agent/import')
42
+ * @param {string} jobNumber - Job number to poll
43
+ * @param {Object} options - Polling options
44
+ * @param {number} options.pollInterval - Milliseconds between polls (default: 2000)
45
+ * @param {number} options.maxAttempts - Max polling attempts (default: 300 = 10 minutes)
46
+ * @param {Function} options.onProgress - Callback for progress updates (progress, message)
47
+ * @returns {Object} Final job result
48
+ * @throws {Error} If job fails or times out
49
+ */
50
+ async function pollForCompletion(http, endpoint, jobNumber, options = {}) {
51
+ const {
52
+ pollInterval = 2000,
53
+ maxAttempts = 300, // 10 minutes with 2-second intervals
54
+ onProgress = () => {}
55
+ } = options;
56
+
57
+ let status = 'running';
58
+ let lastProgress = -1;
59
+ let pollCount = 0;
60
+
61
+ while (status === 'running' || status === 'scheduled') {
62
+ // Wait before polling (except first time)
63
+ if (pollCount > 0) {
64
+ await sleep(pollInterval);
65
+ }
66
+ pollCount++;
67
+
68
+ // Check timeout
69
+ if (pollCount > maxAttempts) {
70
+ throw new Error(`Job polling timeout after ${maxAttempts * pollInterval / 1000} seconds`);
71
+ }
72
+
73
+ // Get status
74
+ const statusResult = await http.get(`${endpoint}?jobNumber=${jobNumber}`);
75
+
76
+ status = statusResult.STATUS || statusResult.status;
77
+ const progress = statusResult.PROGRESS || statusResult.progress || 0;
78
+ const message = statusResult.MESSAGE || statusResult.message || '';
79
+ const errorMessage = statusResult.ERROR_MESSAGE || statusResult.errorMessage;
80
+
81
+ // Handle job not found
82
+ if (statusResult.ERROR || statusResult.error) {
83
+ throw new Error(statusResult.ERROR || statusResult.error);
84
+ }
85
+
86
+ // Show progress if changed
87
+ if (progress !== lastProgress) {
88
+ onProgress(progress, message);
89
+ lastProgress = progress;
90
+ }
91
+
92
+ // Handle error status
93
+ if (status === 'error') {
94
+ throw new Error(errorMessage || 'Job failed with unknown error');
95
+ }
96
+ }
97
+
98
+ // Get final result
99
+ const finalResult = await http.get(`${endpoint}?jobNumber=${jobNumber}`);
100
+
101
+ return {
102
+ status: finalResult.STATUS || finalResult.status,
103
+ result: finalResult.RESULT || finalResult.result,
104
+ startedAt: finalResult.STARTED_AT || finalResult.startedAt,
105
+ completedAt: finalResult.COMPLETED_AT || finalResult.completedAt,
106
+ jobNumber: finalResult.JOB_NUMBER || finalResult.jobNumber,
107
+ jobName: finalResult.JOB_NAME || finalResult.jobName
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Display progress bar in console
113
+ *
114
+ * @param {number} progress - Progress percentage (0-100)
115
+ * @param {string} message - Progress message
116
+ * @param {Object} options - Display options
117
+ * @param {number} options.barWidth - Width of progress bar (default: 30)
118
+ * @param {boolean} options.showPercentage - Show percentage number (default: false)
119
+ */
120
+ function displayProgress(progress, message, options = {}) {
121
+ const {
122
+ barWidth = 30,
123
+ showPercentage = false
124
+ } = options;
125
+
126
+ const filled = Math.floor(progress * barWidth / 100);
127
+ const empty = barWidth - filled;
128
+ const bar = '[' + '='.repeat(filled) + ' '.repeat(empty) + ']';
129
+
130
+ // Clear line and print progress
131
+ let output = `\r${bar}`;
132
+ if (showPercentage) {
133
+ output += ` ${progress}%`;
134
+ }
135
+ output += ` ${message}`;
136
+
137
+ process.stdout.write(output);
138
+ }
139
+
140
+ /**
141
+ * Format ABAP timestamp (YYYYMMDDHHMMSS) to readable local format
142
+ *
143
+ * @param {string|number} timestamp - ABAP timestamp
144
+ * @returns {string} Formatted timestamp (YYYY-MM-DD HH:MM:SS)
145
+ */
146
+ function formatTimestamp(timestamp) {
147
+ if (!timestamp) return timestamp;
148
+
149
+ const ts = timestamp.toString();
150
+ if (ts.length !== 14) return timestamp;
151
+
152
+ const year = parseInt(ts.substr(0, 4));
153
+ const month = parseInt(ts.substr(4, 2)) - 1; // JS months are 0-indexed
154
+ const day = parseInt(ts.substr(6, 2));
155
+ const hour = parseInt(ts.substr(8, 2));
156
+ const minute = parseInt(ts.substr(10, 2));
157
+ const second = parseInt(ts.substr(12, 2));
158
+
159
+ // Create UTC date
160
+ const utcDate = new Date(Date.UTC(year, month, day, hour, minute, second));
161
+
162
+ // Format in local timezone
163
+ const localYear = utcDate.getFullYear();
164
+ const localMonth = String(utcDate.getMonth() + 1).padStart(2, '0');
165
+ const localDay = String(utcDate.getDate()).padStart(2, '0');
166
+ const localHour = String(utcDate.getHours()).padStart(2, '0');
167
+ const localMinute = String(utcDate.getMinutes()).padStart(2, '0');
168
+ const localSecond = String(utcDate.getSeconds()).padStart(2, '0');
169
+
170
+ return `${localYear}-${localMonth}-${localDay} ${localHour}:${localMinute}:${localSecond}`;
171
+ }
172
+
173
+ /**
174
+ * Calculate time difference between two ABAP timestamps
175
+ *
176
+ * @param {string|number} startTimestamp - Start timestamp (YYYYMMDDHHMMSS)
177
+ * @param {string|number} endTimestamp - End timestamp (YYYYMMDDHHMMSS)
178
+ * @returns {string} Human-readable duration (e.g., "2 minutes 30 seconds")
179
+ */
180
+ function calculateTimeSpent(startTimestamp, endTimestamp) {
181
+ if (!startTimestamp || !endTimestamp) return 'unknown';
182
+
183
+ const start = parseAbapTimestamp(startTimestamp);
184
+ const end = parseAbapTimestamp(endTimestamp);
185
+
186
+ if (!start || !end) return 'unknown';
187
+
188
+ const diffMs = end - start;
189
+ const diffSeconds = Math.floor(diffMs / 1000);
190
+
191
+ if (diffSeconds < 60) {
192
+ return `${diffSeconds} second${diffSeconds !== 1 ? 's' : ''}`;
193
+ }
194
+
195
+ const minutes = Math.floor(diffSeconds / 60);
196
+ const seconds = diffSeconds % 60;
197
+
198
+ if (minutes < 60) {
199
+ if (seconds === 0) {
200
+ return `${minutes} minute${minutes !== 1 ? 's' : ''}`;
201
+ }
202
+ return `${minutes} minute${minutes !== 1 ? 's' : ''} ${seconds} second${seconds !== 1 ? 's' : ''}`;
203
+ }
204
+
205
+ const hours = Math.floor(minutes / 60);
206
+ const remainingMinutes = minutes % 60;
207
+
208
+ return `${hours} hour${hours !== 1 ? 's' : ''} ${remainingMinutes} minute${remainingMinutes !== 1 ? 's' : ''}`;
209
+ }
210
+
211
+ /**
212
+ * Parse ABAP timestamp to JavaScript Date
213
+ *
214
+ * @param {string|number} timestamp - ABAP timestamp (YYYYMMDDHHMMSS)
215
+ * @returns {Date|null} JavaScript Date object or null if invalid
216
+ */
217
+ function parseAbapTimestamp(timestamp) {
218
+ if (!timestamp) return null;
219
+
220
+ const ts = timestamp.toString();
221
+ if (ts.length !== 14) return null;
222
+
223
+ const year = parseInt(ts.substr(0, 4));
224
+ const month = parseInt(ts.substr(4, 2)) - 1;
225
+ const day = parseInt(ts.substr(6, 2));
226
+ const hour = parseInt(ts.substr(8, 2));
227
+ const minute = parseInt(ts.substr(10, 2));
228
+ const second = parseInt(ts.substr(12, 2));
229
+
230
+ return new Date(Date.UTC(year, month, day, hour, minute, second));
231
+ }
232
+
233
+ /**
234
+ * Sleep for specified milliseconds
235
+ *
236
+ * @param {number} ms - Milliseconds to sleep
237
+ * @returns {Promise} Promise that resolves after ms milliseconds
238
+ */
239
+ function sleep(ms) {
240
+ return new Promise(resolve => setTimeout(resolve, ms));
241
+ }
242
+
243
+ module.exports = {
244
+ startBackgroundJob,
245
+ pollForCompletion,
246
+ displayProgress,
247
+ formatTimestamp,
248
+ calculateTimeSpent,
249
+ parseAbapTimestamp,
250
+ sleep
251
+ };