abapgit-agent 1.8.5 ā 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
package/src/commands/import.js
CHANGED
|
@@ -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
|
-
|
|
20
|
+
try {
|
|
21
|
+
const { loadConfig, gitUtils, AbapHttp } = context;
|
|
13
22
|
|
|
14
|
-
|
|
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š¦
|
|
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
|
-
|
|
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(
|
|
95
|
+
console.log(`ā
Job started: ${jobInfo.jobNumber}`);
|
|
96
|
+
console.log('');
|
|
82
97
|
|
|
83
|
-
//
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(`
|
|
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
|
}
|
|
@@ -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
|
+
};
|