abapgit-agent 1.8.8 → 1.9.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.
- package/README.md +5 -0
- package/abap/CLAUDE.md +111 -0
- package/bin/abapgit-agent +1 -0
- package/package.json +2 -1
- package/src/commands/dump.js +327 -0
- package/src/commands/help.js +6 -1
- package/src/commands/pull.js +20 -6
- package/src/commands/upgrade.js +56 -11
- package/src/utils/version-check.js +6 -6
package/README.md
CHANGED
|
@@ -144,6 +144,10 @@ abapgit-agent preview --objects SFLIGHT --compact
|
|
|
144
144
|
abapgit-agent where --objects ZCL_MY_CLASS
|
|
145
145
|
abapgit-agent where --objects ZIF_MY_INTERFACE
|
|
146
146
|
abapgit-agent where --objects ZCL_MY_CLASS --type CLAS
|
|
147
|
+
|
|
148
|
+
# Query short dumps (ST22)
|
|
149
|
+
abapgit-agent dump --date TODAY
|
|
150
|
+
abapgit-agent dump --user DEVELOPER --date TODAY --detail 1
|
|
147
151
|
```
|
|
148
152
|
|
|
149
153
|
### Utility Commands
|
|
@@ -188,6 +192,7 @@ npm run pull -- --url <git-url> --branch main
|
|
|
188
192
|
| view Command | [docs/view-command.md](docs/view-command.md) |
|
|
189
193
|
| preview Command | [docs/preview-command.md](docs/preview-command.md) |
|
|
190
194
|
| where Command | [docs/where-command.md](docs/where-command.md) |
|
|
195
|
+
| dump Command | [docs/dump-command.md](docs/dump-command.md) |
|
|
191
196
|
| ref Command | [docs/ref-command.md](docs/ref-command.md) |
|
|
192
197
|
| REST API Reference | [API.md](API.md) |
|
|
193
198
|
| Error Handling | [ERROR_HANDLING.md](ERROR_HANDLING.md) |
|
package/abap/CLAUDE.md
CHANGED
|
@@ -256,6 +256,110 @@ abapgit-agent unit --files src/zcl_test1.clas.testclasses.abap,src/zcl_test2.cla
|
|
|
256
256
|
|
|
257
257
|
---
|
|
258
258
|
|
|
259
|
+
### 9. Use `dump` Command to Investigate Runtime Errors (ST22)
|
|
260
|
+
|
|
261
|
+
**When a user reports an ABAP runtime error, OR when any command returns HTTP 500 / an unexpected ABAP error, proactively check for short dumps.**
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
❌ WRONG: Ask user to open ST22 transaction manually
|
|
265
|
+
❌ WRONG: Give up after an HTTP 500 without checking what caused it
|
|
266
|
+
✅ CORRECT: Use abapgit-agent dump to query short dumps and find the root cause
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### When to Use `dump`
|
|
270
|
+
|
|
271
|
+
| Scenario | Action |
|
|
272
|
+
|----------|--------|
|
|
273
|
+
| User reports "there was a dump in production" | `dump --user <user> --date TODAY` |
|
|
274
|
+
| Any command returns HTTP 500 or unexpected error | `dump --date TODAY` (check for recent dumps) |
|
|
275
|
+
| `pull` or `inspect` fails with internal ABAP error | `dump --date TODAY` |
|
|
276
|
+
| Test run fails with runtime error | `dump --program <program> --date TODAY` |
|
|
277
|
+
| Investigating TIME_OUT or other specific error | `dump --error TIME_OUT` |
|
|
278
|
+
| Finding all errors from a specific user | `dump --user DEVELOPER` |
|
|
279
|
+
| Full error analysis with source location | `dump --detail 1` (after listing) |
|
|
280
|
+
|
|
281
|
+
#### Typical Investigation Workflow
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# Step 1: List recent dumps (last 7 days by default)
|
|
285
|
+
abapgit-agent dump
|
|
286
|
+
|
|
287
|
+
# Step 2: Narrow by program or user if known
|
|
288
|
+
abapgit-agent dump --program ZMY_PROGRAM --date TODAY
|
|
289
|
+
|
|
290
|
+
# Step 3: View full details of the first result
|
|
291
|
+
abapgit-agent dump --program ZMY_PROGRAM --date TODAY --detail 1
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
The `--detail` output includes:
|
|
295
|
+
- **What happened** — high-level error description
|
|
296
|
+
- **Error analysis** — detailed SAP explanation
|
|
297
|
+
- **Call stack** — method-by-method trace with line numbers
|
|
298
|
+
- **Source with `>>>>>` marker** — exact line where the dump occurred
|
|
299
|
+
|
|
300
|
+
#### Reading Detail Output
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
Short Dump Detail
|
|
304
|
+
|
|
305
|
+
Error MESSAGE_TYPE_X
|
|
306
|
+
Date 2024-01-15 (Europe/Berlin)
|
|
307
|
+
Time 09:26:53
|
|
308
|
+
User DEVELOPER
|
|
309
|
+
Program ZMY_PROGRAM
|
|
310
|
+
|
|
311
|
+
What happened:
|
|
312
|
+
-------------------------------------------------------
|
|
313
|
+
A RAISE statement raised the exception "MESSAGE_TYPE_X".
|
|
314
|
+
|
|
315
|
+
Call stack:
|
|
316
|
+
-------------------------------------------------------
|
|
317
|
+
1 ZCL_MY_CLASS->DO_SOMETHING (line 42)
|
|
318
|
+
2 ZMY_PROGRAM START-OF-SELECTION (line 5)
|
|
319
|
+
|
|
320
|
+
Source (ZCL_MY_CLASS=============CM003, line 42):
|
|
321
|
+
-------------------------------------------------------
|
|
322
|
+
METHOD do_something.
|
|
323
|
+
DATA lv_val TYPE i.
|
|
324
|
+
>>>>> RAISE EXCEPTION TYPE cx_my_error. ← error here
|
|
325
|
+
lv_val = lv_val + 1.
|
|
326
|
+
ENDMETHOD.
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
**After identifying the error location**, use `view` to understand the class context:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# View the class/method where the dump occurred
|
|
333
|
+
abapgit-agent view --objects ZCL_MY_CLASS
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### Filters Reference
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
# Filter by user
|
|
340
|
+
abapgit-agent dump --user DEVELOPER
|
|
341
|
+
|
|
342
|
+
# Filter by date (system timezone)
|
|
343
|
+
abapgit-agent dump --date TODAY
|
|
344
|
+
abapgit-agent dump --date YESTERDAY
|
|
345
|
+
abapgit-agent dump --date 2024-01-01..2024-01-31
|
|
346
|
+
|
|
347
|
+
# Filter by program
|
|
348
|
+
abapgit-agent dump --program ZMY_PROGRAM
|
|
349
|
+
|
|
350
|
+
# Filter by error type
|
|
351
|
+
abapgit-agent dump --error TIME_OUT
|
|
352
|
+
abapgit-agent dump --error MESSAGE_TYPE_X
|
|
353
|
+
|
|
354
|
+
# Show full detail for result #N (combine with same filters)
|
|
355
|
+
abapgit-agent dump --user DEVELOPER --date TODAY --detail 1
|
|
356
|
+
|
|
357
|
+
# Use explicit timezone (e.g., for distributed teams)
|
|
358
|
+
abapgit-agent dump --date TODAY --timezone Europe/Berlin
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
259
363
|
## Development Workflow
|
|
260
364
|
|
|
261
365
|
This project's workflow mode is configured in `.abapGitAgent` under `workflow.mode`.
|
|
@@ -672,6 +776,13 @@ abapgit-agent view --objects ZTABLE --type TABL
|
|
|
672
776
|
|
|
673
777
|
# Display package tree
|
|
674
778
|
abapgit-agent tree --package \$MY_PACKAGE
|
|
779
|
+
|
|
780
|
+
# Investigate runtime errors (ST22 short dumps)
|
|
781
|
+
abapgit-agent dump # Last 7 days
|
|
782
|
+
abapgit-agent dump --user DEVELOPER --date TODAY # Today's dumps for a user
|
|
783
|
+
abapgit-agent dump --program ZMY_PROGRAM # Dumps from a specific program
|
|
784
|
+
abapgit-agent dump --error TIME_OUT # Dumps by error type
|
|
785
|
+
abapgit-agent dump --user DEVELOPER --detail 1 # Full detail of first result
|
|
675
786
|
```
|
|
676
787
|
|
|
677
788
|
---
|
package/bin/abapgit-agent
CHANGED
|
@@ -50,6 +50,7 @@ async function main() {
|
|
|
50
50
|
view: require('../src/commands/view'),
|
|
51
51
|
preview: require('../src/commands/preview'),
|
|
52
52
|
where: require('../src/commands/where'),
|
|
53
|
+
dump: require('../src/commands/dump'),
|
|
53
54
|
ref: require('../src/commands/ref'),
|
|
54
55
|
init: require('../src/commands/init'),
|
|
55
56
|
pull: require('../src/commands/pull'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abapgit-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
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": [
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"test:cmd:view": "node tests/run-all.js --cmd --command=view",
|
|
34
34
|
"test:cmd:preview": "node tests/run-all.js --cmd --command=preview",
|
|
35
35
|
"test:cmd:tree": "node tests/run-all.js --cmd --command=tree",
|
|
36
|
+
"test:cmd:dump": "node tests/run-all.js --cmd --command=dump",
|
|
36
37
|
"test:cmd:upgrade": "node tests/run-all.js --cmd --command=upgrade",
|
|
37
38
|
"test:lifecycle": "node tests/run-all.js --lifecycle",
|
|
38
39
|
"test:pull": "node tests/run-all.js --pull",
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dump command - Query short dumps (ST22) from ABAP system
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Convert a local date/time in the given IANA timezone to a UTC Date.
|
|
8
|
+
// Uses an iterative approach to reliably handle DST transitions.
|
|
9
|
+
function localToUTC(dateStr, timeStr, timezone) {
|
|
10
|
+
const iso = `${dateStr.slice(0,4)}-${dateStr.slice(4,6)}-${dateStr.slice(6,8)}` +
|
|
11
|
+
`T${timeStr.slice(0,2)}:${timeStr.slice(2,4)}:${timeStr.slice(4,6)}`;
|
|
12
|
+
let candidate = new Date(iso + 'Z');
|
|
13
|
+
for (let i = 0; i < 3; i++) {
|
|
14
|
+
const localStr = candidate.toLocaleString('sv-SE', { timeZone: timezone }).replace(' ', 'T');
|
|
15
|
+
const diff = new Date(iso + 'Z').getTime() - new Date(localStr + 'Z').getTime();
|
|
16
|
+
candidate = new Date(candidate.getTime() + diff);
|
|
17
|
+
}
|
|
18
|
+
return candidate;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Format a UTC Date as a local date/time in the given IANA timezone.
|
|
22
|
+
// Returns { date: 'YYYY-MM-DD', time: 'HH:MM:SS' }.
|
|
23
|
+
function utcToLocal(utcDate, timezone) {
|
|
24
|
+
const local = utcDate.toLocaleString('sv-SE', { timeZone: timezone });
|
|
25
|
+
const [date, time] = local.split(' ');
|
|
26
|
+
return { date, time };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parse a 14-char UTC timestamp string (YYYYMMDDhhmmss) to a Date.
|
|
30
|
+
function parseUTCTimestamp(ts) {
|
|
31
|
+
const s = String(ts).padStart(14, '0');
|
|
32
|
+
return new Date(`${s.slice(0,4)}-${s.slice(4,6)}-${s.slice(6,8)}T${s.slice(8,10)}:${s.slice(10,12)}:${s.slice(12,14)}Z`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Format a UTC Date as a 14-char timestamp string (YYYYMMDDhhmmss).
|
|
36
|
+
function toTimestampStr(date) {
|
|
37
|
+
return date.toISOString().replace(/[-T:Z]/g, '').slice(0, 14);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Parse the user's --date argument into { from, to } as YYYYMMDD strings,
|
|
41
|
+
// respecting the given timezone for TODAY/YESTERDAY keywords.
|
|
42
|
+
function parseDateArg(dateStr, timezone) {
|
|
43
|
+
const nowLocal = utcToLocal(new Date(), timezone);
|
|
44
|
+
const todayStr = nowLocal.date.replace(/-/g, '');
|
|
45
|
+
|
|
46
|
+
const upper = dateStr.toUpperCase();
|
|
47
|
+
if (upper === 'TODAY') {
|
|
48
|
+
return { from: todayStr, to: todayStr };
|
|
49
|
+
}
|
|
50
|
+
if (upper === 'YESTERDAY') {
|
|
51
|
+
const d = new Date(nowLocal.date + 'T00:00:00Z');
|
|
52
|
+
d.setUTCDate(d.getUTCDate() - 1);
|
|
53
|
+
const yStr = d.toISOString().slice(0, 10).replace(/-/g, '');
|
|
54
|
+
return { from: yStr, to: yStr };
|
|
55
|
+
}
|
|
56
|
+
if (dateStr.includes('..')) {
|
|
57
|
+
const [a, b] = dateStr.split('..');
|
|
58
|
+
return { from: a.replace(/-/g, ''), to: b.replace(/-/g, '') };
|
|
59
|
+
}
|
|
60
|
+
const d = dateStr.replace(/-/g, '');
|
|
61
|
+
return { from: d, to: d };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseTimeArg(timeStr) {
|
|
65
|
+
const padTime = (t) => (t.replace(/:/g, '') + '000000').substring(0, 6);
|
|
66
|
+
if (timeStr.includes('..')) {
|
|
67
|
+
const [a, b] = timeStr.split('..');
|
|
68
|
+
return { from: padTime(a), to: padTime(b) };
|
|
69
|
+
}
|
|
70
|
+
const t = padTime(timeStr);
|
|
71
|
+
return { from: t, to: t };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Resolve display date/time for a dump entry, using utc_timestamp when available.
|
|
75
|
+
function resolveDateTime(dump, timezone) {
|
|
76
|
+
const ts = dump.UTC_TIMESTAMP || dump.utc_timestamp;
|
|
77
|
+
if (ts && timezone) {
|
|
78
|
+
const utcDate = parseUTCTimestamp(ts);
|
|
79
|
+
return utcToLocal(utcDate, timezone);
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
date: dump.DATE || dump.date || '',
|
|
83
|
+
time: dump.TIME || dump.time || '',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderList(dumps, total, limit, timezone) {
|
|
88
|
+
const totalNum = Number(total) || dumps.length;
|
|
89
|
+
const tzLabel = timezone ? ` [${timezone}]` : '';
|
|
90
|
+
console.log(`\n Short Dumps (${totalNum} found)${tzLabel}\n`);
|
|
91
|
+
|
|
92
|
+
if (dumps.length === 0) {
|
|
93
|
+
console.log(' No short dumps found for the given filters.\n');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const W = { num: 3, date: 10, time: 8, user: 12, prog: 30, err: 40 };
|
|
98
|
+
const hdr = ` ${'#'.padStart(W.num)} ${'Date'.padEnd(W.date)} ${'Time'.padEnd(W.time)} ${'User'.padEnd(W.user)} ${'Program'.padEnd(W.prog)} Error`;
|
|
99
|
+
const sep = ' ' + '-'.repeat(W.num + 2 + W.date + 2 + W.time + 2 + W.user + 2 + W.prog + 2 + W.err);
|
|
100
|
+
console.log(hdr);
|
|
101
|
+
console.log(sep);
|
|
102
|
+
|
|
103
|
+
dumps.forEach((d, i) => {
|
|
104
|
+
const { date, time } = resolveDateTime(d, timezone);
|
|
105
|
+
const user = (d.USER || d.user || '').substring(0, W.user);
|
|
106
|
+
const prog = (d.PROGRAM || d.program || '').substring(0, W.prog);
|
|
107
|
+
const err = (d.ERROR || d.error || '');
|
|
108
|
+
console.log(` ${String(i + 1).padStart(W.num)} ${date.padEnd(W.date)} ${time.padEnd(W.time)} ${user.padEnd(W.user)} ${prog.padEnd(W.prog)} ${err}`);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log('');
|
|
112
|
+
if (totalNum > limit) {
|
|
113
|
+
console.log(` Showing ${dumps.length} of ${totalNum} dumps. Use --limit to see more.`);
|
|
114
|
+
}
|
|
115
|
+
console.log(' Use --detail <number> to see full dump details\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderDetail(dump, timezone) {
|
|
119
|
+
console.log('\n Short Dump Detail\n');
|
|
120
|
+
|
|
121
|
+
const { date, time } = resolveDateTime(dump, timezone);
|
|
122
|
+
const tzLabel = timezone ? ` (${timezone})` : '';
|
|
123
|
+
|
|
124
|
+
const fields = [
|
|
125
|
+
['Error', dump.ERROR || dump.error || ''],
|
|
126
|
+
['Date', date + tzLabel],
|
|
127
|
+
['Time', time],
|
|
128
|
+
['User', dump.USER || dump.user || ''],
|
|
129
|
+
['Program', dump.PROGRAM || dump.program || ''],
|
|
130
|
+
['Object', dump.OBJECT || dump.object || ''],
|
|
131
|
+
['Package', dump.PACKAGE || dump.package || ''],
|
|
132
|
+
['Exception', dump.EXCEPTION || dump.exception || ''],
|
|
133
|
+
];
|
|
134
|
+
fields.forEach(([label, val]) => {
|
|
135
|
+
if (val) {
|
|
136
|
+
console.log(` ${label.padEnd(12)}${val}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const normalize = (s) => s.replace(/\\n/g, '\n').replace(/\\r/g, '');
|
|
141
|
+
const whatHappened = normalize(dump.WHAT_HAPPENED || dump.what_happened || '');
|
|
142
|
+
const errorAnalysis = normalize(dump.ERROR_ANALYSIS || dump.error_analysis || '');
|
|
143
|
+
|
|
144
|
+
if (whatHappened) {
|
|
145
|
+
console.log('\n What happened:');
|
|
146
|
+
console.log(' ' + '-'.repeat(55));
|
|
147
|
+
console.log(whatHappened.split('\n').map(l => ' ' + l).join('\n'));
|
|
148
|
+
}
|
|
149
|
+
if (errorAnalysis) {
|
|
150
|
+
console.log('\n Error analysis:');
|
|
151
|
+
console.log(' ' + '-'.repeat(55));
|
|
152
|
+
console.log(errorAnalysis.split('\n').map(l => ' ' + l).join('\n'));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const stack = dump.CALL_STACK || dump.call_stack || [];
|
|
156
|
+
|
|
157
|
+
// Separate structured frames (have level) from source block (no level, has raw text)
|
|
158
|
+
const frames = stack.filter(f => (f.LEVEL || f.level));
|
|
159
|
+
const srcEntries = stack.filter(f => !(f.LEVEL || f.level) && (f.METHOD || f.method));
|
|
160
|
+
|
|
161
|
+
if (frames.length > 0) {
|
|
162
|
+
console.log('\n Call stack:');
|
|
163
|
+
console.log(' ' + '-'.repeat(55));
|
|
164
|
+
frames.forEach((frame) => {
|
|
165
|
+
const level = frame.LEVEL || frame.level || '';
|
|
166
|
+
const cls = frame.CLASS || frame.class || '';
|
|
167
|
+
const method = frame.METHOD || frame.method || '';
|
|
168
|
+
const program = frame.PROGRAM || frame.program || '';
|
|
169
|
+
const include = frame.INCLUDE || frame.include || '';
|
|
170
|
+
const line = frame.LINE || frame.line || '';
|
|
171
|
+
const location = cls ? `${cls}->${method}` : (method ? `${program} ${method}` : program || include);
|
|
172
|
+
const lineRef = line ? ` (line ${line})` : '';
|
|
173
|
+
console.log(` ${String(level).padStart(3)} ${location}${lineRef}`);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (srcEntries.length > 0) {
|
|
178
|
+
const srcInc = dump.SOURCE_INCLUDE || dump.source_include || '';
|
|
179
|
+
const srcLine = dump.SOURCE_LINE || dump.source_line || 0;
|
|
180
|
+
const heading = srcInc ? ` Source (${srcInc}, line ${srcLine}):` : ' Source:';
|
|
181
|
+
console.log('\n' + heading);
|
|
182
|
+
console.log(' ' + '-'.repeat(55));
|
|
183
|
+
const rawText = srcEntries[0].METHOD || srcEntries[0].method || '';
|
|
184
|
+
console.log(normalize(rawText).split('\n').map(l => ' ' + l).join('\n'));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log('');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = {
|
|
191
|
+
name: 'dump',
|
|
192
|
+
description: 'Query short dumps (ST22) from ABAP system',
|
|
193
|
+
requiresAbapConfig: true,
|
|
194
|
+
requiresVersionCheck: false,
|
|
195
|
+
|
|
196
|
+
async execute(args, context) {
|
|
197
|
+
const { loadConfig, AbapHttp } = context;
|
|
198
|
+
|
|
199
|
+
const idx = (flag) => args.indexOf(flag);
|
|
200
|
+
const val = (flag) => {
|
|
201
|
+
const i = idx(flag);
|
|
202
|
+
return i !== -1 && i + 1 < args.length ? args[i + 1] : null;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const userRaw = val('--user');
|
|
206
|
+
const dateRaw = val('--date');
|
|
207
|
+
const timeRaw = val('--time');
|
|
208
|
+
const programRaw = val('--program');
|
|
209
|
+
const errorRaw = val('--error');
|
|
210
|
+
const limitRaw = val('--limit');
|
|
211
|
+
const detailRaw = val('--detail');
|
|
212
|
+
const timezoneRaw = val('--timezone');
|
|
213
|
+
const jsonOutput = args.includes('--json');
|
|
214
|
+
|
|
215
|
+
const user = userRaw ? userRaw.toUpperCase() : null;
|
|
216
|
+
const program = programRaw ? programRaw.toUpperCase() : null;
|
|
217
|
+
const error = errorRaw ? errorRaw.toUpperCase() : null;
|
|
218
|
+
|
|
219
|
+
// Resolve timezone: explicit flag > system default
|
|
220
|
+
const timezone = timezoneRaw || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
221
|
+
|
|
222
|
+
let limit = 20;
|
|
223
|
+
if (limitRaw) {
|
|
224
|
+
const parsed = parseInt(limitRaw, 10);
|
|
225
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
226
|
+
limit = Math.min(parsed, 100);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Build UTC timestamp range from user's date/time in their timezone
|
|
231
|
+
let tsFrom = null, tsTo = null;
|
|
232
|
+
if (dateRaw) {
|
|
233
|
+
const dates = parseDateArg(dateRaw, timezone);
|
|
234
|
+
const timeF = timeRaw ? parseTimeArg(timeRaw).from : '000000';
|
|
235
|
+
const timeT = timeRaw ? parseTimeArg(timeRaw).to : '235959';
|
|
236
|
+
tsFrom = toTimestampStr(localToUTC(dates.from, timeF, timezone));
|
|
237
|
+
tsTo = toTimestampStr(localToUTC(dates.to, timeT, timezone));
|
|
238
|
+
} else if (timeRaw) {
|
|
239
|
+
// Time filter without date: apply to the default 7-day window
|
|
240
|
+
const now = utcToLocal(new Date(), timezone);
|
|
241
|
+
const today = now.date.replace(/-/g, '');
|
|
242
|
+
const sevenAgo = (() => {
|
|
243
|
+
const d = new Date(now.date + 'T00:00:00Z');
|
|
244
|
+
d.setUTCDate(d.getUTCDate() - 7);
|
|
245
|
+
return d.toISOString().slice(0, 10).replace(/-/g, '');
|
|
246
|
+
})();
|
|
247
|
+
const times = parseTimeArg(timeRaw);
|
|
248
|
+
tsFrom = toTimestampStr(localToUTC(sevenAgo, times.from, timezone));
|
|
249
|
+
tsTo = toTimestampStr(localToUTC(today, times.to, timezone));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const detailN = detailRaw ? parseInt(detailRaw, 10) : null;
|
|
253
|
+
|
|
254
|
+
const config = loadConfig();
|
|
255
|
+
const http = new AbapHttp(config);
|
|
256
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
257
|
+
|
|
258
|
+
const buildData = (extra) => {
|
|
259
|
+
const data = { limit };
|
|
260
|
+
if (user) data.user = user;
|
|
261
|
+
if (program) data.program = program;
|
|
262
|
+
if (error) data.error = error;
|
|
263
|
+
if (tsFrom) {
|
|
264
|
+
data.ts_from = tsFrom;
|
|
265
|
+
data.ts_to = tsTo;
|
|
266
|
+
}
|
|
267
|
+
return Object.assign(data, extra);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (detailN) {
|
|
271
|
+
// Two-step: first list (using limit 100 to get full page), then detail
|
|
272
|
+
const listResult = await http.post('/sap/bc/z_abapgit_agent/dump', buildData({ limit: 100 }), { csrfToken });
|
|
273
|
+
const listSuccess = listResult.SUCCESS || listResult.success;
|
|
274
|
+
const listErr = listResult.ERROR || listResult.error;
|
|
275
|
+
|
|
276
|
+
if (!listSuccess || listErr) {
|
|
277
|
+
console.error(`\n Error: ${listErr || 'Failed to query short dumps'}\n`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const dumps = listResult.DUMPS || listResult.dumps || [];
|
|
282
|
+
if (detailN < 1 || detailN > dumps.length) {
|
|
283
|
+
console.error(`\n Error: Row number ${detailN} not found in results (found ${dumps.length} dump(s))\n`);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const targetId = dumps[detailN - 1].ID || dumps[detailN - 1].id;
|
|
288
|
+
const detailResult = await http.post('/sap/bc/z_abapgit_agent/dump', buildData({ detail: targetId }), { csrfToken });
|
|
289
|
+
|
|
290
|
+
const success = detailResult.SUCCESS || detailResult.success;
|
|
291
|
+
const errMsg = detailResult.ERROR || detailResult.error;
|
|
292
|
+
|
|
293
|
+
if (!success || errMsg) {
|
|
294
|
+
console.error(`\n Error: ${errMsg || 'Failed to load dump detail'}\n`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (jsonOutput) {
|
|
299
|
+
console.log(JSON.stringify(detailResult, null, 2));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const detailDumps = detailResult.DUMPS || detailResult.dumps || [];
|
|
304
|
+
renderDetail(detailDumps[0] || {}, timezone);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// List mode
|
|
309
|
+
const result = await http.post('/sap/bc/z_abapgit_agent/dump', buildData({}), { csrfToken });
|
|
310
|
+
const success = result.SUCCESS || result.success;
|
|
311
|
+
const errMsg = result.ERROR || result.error;
|
|
312
|
+
|
|
313
|
+
if (!success || errMsg) {
|
|
314
|
+
console.error(`\n Error: ${errMsg || 'Failed to query short dumps'}\n`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (jsonOutput) {
|
|
319
|
+
console.log(JSON.stringify(result, null, 2));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const dumps = result.DUMPS || result.dumps || [];
|
|
324
|
+
const total = result.TOTAL || result.total || dumps.length;
|
|
325
|
+
renderList(dumps, total, limit, timezone);
|
|
326
|
+
}
|
|
327
|
+
};
|
package/src/commands/help.js
CHANGED
|
@@ -58,6 +58,9 @@ Commands:
|
|
|
58
58
|
where --objects <obj1>,<obj2>,... [--type <type>] [--limit <n>] [--json]
|
|
59
59
|
Find where-used list for ABAP objects (classes, interfaces, programs)
|
|
60
60
|
|
|
61
|
+
dump [--user <user>] [--date <date>] [--time <HH:MM..HH:MM>] [--timezone <tz>] [--program <prog>] [--error <error>] [--limit <n>] [--detail <n>] [--json]
|
|
62
|
+
Query short dumps (ST22) from ABAP system. Use --detail <n> to view full details.
|
|
63
|
+
|
|
61
64
|
ref <pattern> [--json]
|
|
62
65
|
Search ABAP reference repositories for patterns. Requires referenceFolder in .abapGitAgent.
|
|
63
66
|
|
|
@@ -100,12 +103,14 @@ Examples:
|
|
|
100
103
|
abapgit-agent list --package $MY_PACKAGE --type CLAS,INTF # List classes & interfaces
|
|
101
104
|
abapgit-agent view --objects ZCL_MY_CLASS # View class definition
|
|
102
105
|
abapgit-agent where --objects ZCL_MY_CLASS # Find where class is used
|
|
106
|
+
abapgit-agent dump --date TODAY # Recent short dumps
|
|
107
|
+
abapgit-agent dump --user DEVELOPER --detail 1 # Full detail of first result
|
|
103
108
|
abapgit-agent ref "CORRESPONDING" # Search for pattern
|
|
104
109
|
abapgit-agent ref --topic exceptions # View exceptions topic
|
|
105
110
|
abapgit-agent health # Health check
|
|
106
111
|
abapgit-agent status # Configuration status
|
|
107
112
|
|
|
108
|
-
For more info: https://github.
|
|
113
|
+
For more info: https://github.com/SylvosCai/abapgit-agent
|
|
109
114
|
`);
|
|
110
115
|
}
|
|
111
116
|
};
|
package/src/commands/pull.js
CHANGED
|
@@ -77,7 +77,7 @@ module.exports = {
|
|
|
77
77
|
await this.pull(gitUrl, branch, files, transportRequest, loadConfig, AbapHttp, jsonOutput);
|
|
78
78
|
},
|
|
79
79
|
|
|
80
|
-
async pull(gitUrl, branch = 'main', files = null, transportRequest = null, loadConfig, AbapHttp, jsonOutput = false) {
|
|
80
|
+
async pull(gitUrl, branch = 'main', files = null, transportRequest = null, loadConfig, AbapHttp, jsonOutput = false, gitCredentials = undefined) {
|
|
81
81
|
const TERM_WIDTH = process.stdout.columns || 80;
|
|
82
82
|
|
|
83
83
|
if (!jsonOutput) {
|
|
@@ -99,11 +99,15 @@ module.exports = {
|
|
|
99
99
|
const csrfToken = await http.fetchCsrfToken();
|
|
100
100
|
|
|
101
101
|
// Prepare request data with git credentials
|
|
102
|
+
// gitCredentials=null means no credentials (public repo); undefined means use config defaults
|
|
103
|
+
const resolvedCredentials = gitCredentials === undefined
|
|
104
|
+
? { username: config.gitUsername, password: config.gitPassword }
|
|
105
|
+
: gitCredentials;
|
|
106
|
+
|
|
102
107
|
const data = {
|
|
103
108
|
url: gitUrl,
|
|
104
109
|
branch: branch,
|
|
105
|
-
username:
|
|
106
|
-
password: config.gitPassword
|
|
110
|
+
...(resolvedCredentials ? { username: resolvedCredentials.username, password: resolvedCredentials.password } : {})
|
|
107
111
|
};
|
|
108
112
|
|
|
109
113
|
// Add files array if specified
|
|
@@ -198,9 +202,9 @@ module.exports = {
|
|
|
198
202
|
console.log(` Job ID: ${jobId || 'N/A'}`);
|
|
199
203
|
console.log(` Message: ${message || 'N/A'}`);
|
|
200
204
|
} else {
|
|
201
|
-
console.
|
|
202
|
-
console.
|
|
203
|
-
console.
|
|
205
|
+
console.error(`❌ Pull completed with errors!`);
|
|
206
|
+
console.error(` Job ID: ${jobId || 'N/A'}`);
|
|
207
|
+
console.error(` Message: ${message || 'N/A'}`);
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
// Display error detail if available
|
|
@@ -306,8 +310,18 @@ module.exports = {
|
|
|
306
310
|
console.log(`\n❌ Failed Objects Log (${failedCount})`);
|
|
307
311
|
}
|
|
308
312
|
|
|
313
|
+
// Throw if pull was not successful so callers (e.g. upgrade) can detect failure
|
|
314
|
+
if (success !== 'X' && success !== true) {
|
|
315
|
+
const err = new Error(message || 'Pull completed with errors');
|
|
316
|
+
err._isPullError = true;
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
|
|
309
320
|
return result;
|
|
310
321
|
} catch (error) {
|
|
322
|
+
if (error._isPullError) {
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
311
325
|
console.error(`\n❌ Error: ${error.message}`);
|
|
312
326
|
process.exit(1);
|
|
313
327
|
}
|
package/src/commands/upgrade.js
CHANGED
|
@@ -46,10 +46,29 @@ module.exports = {
|
|
|
46
46
|
const { apiVersion } = await versionCheck.checkCompatibility(config);
|
|
47
47
|
abapVersion = apiVersion;
|
|
48
48
|
} catch (e) {
|
|
49
|
-
console.error(`⚠️ Could not
|
|
49
|
+
console.error(`⚠️ Could not reach ABAP system: ${e.message}`);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// If ABAP is unreachable and we need to upgrade it, warn and ask whether to continue CLI-only
|
|
54
|
+
if (needsAbapConfig && abapVersion === null && !flags.cliOnly && !flags.abapOnly && !flags.match) {
|
|
55
|
+
console.error('');
|
|
56
|
+
console.error('⚠️ ABAP system is unreachable. Cannot upgrade ABAP backend.');
|
|
57
|
+
if (flags.yes) {
|
|
58
|
+
console.log(' --yes flag set: upgrading CLI only.');
|
|
59
|
+
flags.cliOnly = true;
|
|
60
|
+
} else {
|
|
61
|
+
const proceed = await this.confirmCliOnlyFallback();
|
|
62
|
+
if (!proceed) {
|
|
63
|
+
console.log('Upgrade cancelled.');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
flags.cliOnly = true;
|
|
67
|
+
}
|
|
68
|
+
console.log(' Run "abapgit-agent upgrade --abap-only" once the system is back.');
|
|
69
|
+
console.log('');
|
|
70
|
+
}
|
|
71
|
+
|
|
53
72
|
// Get latest version from npm
|
|
54
73
|
const latestVersion = await versionCheck.getLatestNpmVersion();
|
|
55
74
|
if (!latestVersion && !flags.version && !flags.match) {
|
|
@@ -260,8 +279,8 @@ module.exports = {
|
|
|
260
279
|
step++;
|
|
261
280
|
}
|
|
262
281
|
if (targets.abapTarget) {
|
|
263
|
-
console.log(` ${step}. abapgit-agent pull --branch v${targets.abapTarget}`);
|
|
264
|
-
console.log(' (
|
|
282
|
+
console.log(` ${step}. abapgit-agent pull --url <agentRepoUrl> --branch v${targets.abapTarget}`);
|
|
283
|
+
console.log(' (agentRepoUrl from .abapGitAgent config or current git remote)');
|
|
265
284
|
step++;
|
|
266
285
|
}
|
|
267
286
|
console.log(` ${step}. Verify versions match`);
|
|
@@ -379,7 +398,24 @@ module.exports = {
|
|
|
379
398
|
},
|
|
380
399
|
|
|
381
400
|
/**
|
|
382
|
-
*
|
|
401
|
+
* Confirm CLI-only fallback when ABAP system is unreachable
|
|
402
|
+
*/
|
|
403
|
+
async confirmCliOnlyFallback() {
|
|
404
|
+
return new Promise((resolve) => {
|
|
405
|
+
const rl = readline.createInterface({
|
|
406
|
+
input: process.stdin,
|
|
407
|
+
output: process.stdout
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
rl.question(' Continue with CLI upgrade only? [Y/n] ', (answer) => {
|
|
411
|
+
rl.close();
|
|
412
|
+
const normalized = answer.trim().toLowerCase();
|
|
413
|
+
resolve(normalized === '' || normalized === 'y' || normalized === 'yes');
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
/**
|
|
383
419
|
*/
|
|
384
420
|
async upgradeAbapBackend(version, transport, context) {
|
|
385
421
|
console.log(`📦 Upgrading ABAP backend to v${version}...`);
|
|
@@ -387,15 +423,20 @@ module.exports = {
|
|
|
387
423
|
console.log('');
|
|
388
424
|
|
|
389
425
|
try {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
426
|
+
const { loadConfig } = context;
|
|
427
|
+
|
|
428
|
+
// Determine the abapgit-agent repository URL.
|
|
429
|
+
// Priority:
|
|
430
|
+
// 1. agentRepoUrl from .abapGitAgent config (for forks/mirrors)
|
|
431
|
+
// 2. Default canonical repository
|
|
432
|
+
const config = loadConfig();
|
|
433
|
+
const agentRepoUrl = config.agentRepoUrl || 'https://github.com/SylvosCai/abapgit-agent.git';
|
|
395
434
|
|
|
396
|
-
// Execute pull command
|
|
435
|
+
// Execute pull command — pass null credentials so the canonical
|
|
436
|
+
// public GitHub repo is not sent the project's SAP internal git credentials.
|
|
397
437
|
const pullCommand = require('./pull');
|
|
398
|
-
|
|
438
|
+
const { loadConfig: lc, AbapHttp } = context;
|
|
439
|
+
await pullCommand.pull(agentRepoUrl, `v${version}`, null, transport || null, lc, AbapHttp, false, null);
|
|
399
440
|
|
|
400
441
|
console.log('');
|
|
401
442
|
console.log(`✅ ABAP backend upgraded to v${version}`);
|
|
@@ -404,9 +445,13 @@ module.exports = {
|
|
|
404
445
|
console.error(`❌ Failed to upgrade ABAP backend: ${error.message}`);
|
|
405
446
|
console.error('');
|
|
406
447
|
console.error('This may be due to:');
|
|
448
|
+
console.error(' - ABAP system is unavailable');
|
|
407
449
|
console.error(' - Git tag not found in repository');
|
|
408
450
|
console.error(' - ABAP activation errors');
|
|
409
451
|
console.error(' - Connection issues with ABAP system');
|
|
452
|
+
console.error('');
|
|
453
|
+
console.error('To retry once the system is available:');
|
|
454
|
+
console.error(` abapgit-agent upgrade --abap-only --version ${version}`);
|
|
410
455
|
process.exit(1);
|
|
411
456
|
}
|
|
412
457
|
},
|
|
@@ -75,9 +75,9 @@ async function checkCompatibility(config) {
|
|
|
75
75
|
const apiVersion = result.version || '1.0.0';
|
|
76
76
|
|
|
77
77
|
if (cliVersion !== apiVersion) {
|
|
78
|
-
console.
|
|
79
|
-
console.
|
|
80
|
-
console.
|
|
78
|
+
console.error(`\n⚠️ Version mismatch: CLI ${cliVersion}, ABAP API ${apiVersion}`);
|
|
79
|
+
console.error(' Some commands may not work correctly.');
|
|
80
|
+
console.error(' Update ABAP code: abapgit-agent upgrade --match\n');
|
|
81
81
|
}
|
|
82
82
|
resolve({ cliVersion, apiVersion, compatible: cliVersion === apiVersion });
|
|
83
83
|
} catch (e) {
|
|
@@ -242,9 +242,9 @@ async function showNewVersionReminder() {
|
|
|
242
242
|
const { hasNewVersion, latestVersion, currentVersion } = await checkForNewVersion();
|
|
243
243
|
|
|
244
244
|
if (hasNewVersion) {
|
|
245
|
-
console.
|
|
246
|
-
console.
|
|
247
|
-
console.
|
|
245
|
+
console.error('');
|
|
246
|
+
console.error(`💡 New version available: ${latestVersion} (current: ${currentVersion})`);
|
|
247
|
+
console.error(` Run: abapgit-agent upgrade`);
|
|
248
248
|
}
|
|
249
249
|
} catch (e) {
|
|
250
250
|
// Silent - don't interrupt user's workflow
|