abapgit-agent 1.15.3 → 1.16.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/abap/CLAUDE.md +42 -8
- package/abap/guidelines/abaplint.md +35 -0
- package/abap/guidelines/common-errors.md +78 -1
- package/abap/guidelines/string-template.md +84 -0
- package/package.json +2 -1
- package/src/commands/drop.js +171 -0
- package/src/commands/index.js +2 -1
- package/src/commands/syntax.js +121 -17
package/abap/CLAUDE.md
CHANGED
|
@@ -179,26 +179,27 @@ Anything else (SAP namespace object, or SAP-delivered package)?
|
|
|
179
179
|
|
|
180
180
|
---
|
|
181
181
|
|
|
182
|
-
### 4. Use Syntax Command Before Commit (for CLAS, INTF, PROG, DDLS)
|
|
182
|
+
### 4. Use Syntax Command Before Commit (for CLAS, INTF, PROG, DDLS, FUGR)
|
|
183
183
|
|
|
184
184
|
```
|
|
185
185
|
❌ WRONG: Make changes → Commit → Push → Pull → Find errors → Fix → Repeat
|
|
186
186
|
✅ CORRECT: Make changes → Run syntax → Fix locally → Commit → Push → Pull → Done
|
|
187
187
|
```
|
|
188
188
|
|
|
189
|
-
**For CLAS, INTF, PROG, DDLS files**: Run `syntax` command BEFORE commit to catch errors early.
|
|
189
|
+
**For CLAS, INTF, PROG, DDLS, FUGR files**: Run `syntax` command BEFORE commit to catch errors early.
|
|
190
190
|
|
|
191
191
|
```bash
|
|
192
192
|
# Check syntax of local code (no commit/push needed)
|
|
193
193
|
# Use the actual filename from your project (name comes from objects.local.md)
|
|
194
194
|
abapgit-agent syntax --files src/<name>.clas.abap
|
|
195
195
|
abapgit-agent syntax --files src/<name>.ddls.asddls
|
|
196
|
+
abapgit-agent syntax --files src/<name>.fugr.<fm_name>.abap
|
|
196
197
|
|
|
197
198
|
# Check multiple INDEPENDENT files
|
|
198
199
|
abapgit-agent syntax --files src/<name1>.clas.abap,src/<name2>.clas.abap
|
|
199
200
|
```
|
|
200
201
|
|
|
201
|
-
**For other types (
|
|
202
|
+
**For other types (TABL, STRU, DCLS, etc.)**: Skip syntax, proceed to commit/push/pull.
|
|
202
203
|
|
|
203
204
|
**Why use syntax command?**
|
|
204
205
|
- Catches syntax errors BEFORE polluting git history with fix commits
|
|
@@ -424,7 +425,39 @@ After activating a class, stop and tell the user: `"Class is activated. Run with
|
|
|
424
425
|
|
|
425
426
|
---
|
|
426
427
|
|
|
427
|
-
### 10.
|
|
428
|
+
### 10. Never Run `drop` Command Without Explicit Permission
|
|
429
|
+
|
|
430
|
+
**Never call `abapgit-agent drop` unless the user explicitly confirms.** Dropping an object physically deletes it from the ABAP system — this is irreversible without a subsequent pull.
|
|
431
|
+
|
|
432
|
+
**When to SUGGEST `drop --pull`:**
|
|
433
|
+
|
|
434
|
+
When an object is in a broken or inconsistent state that cannot be resolved by re-pulling:
|
|
435
|
+
- `inspect` reports **"INCLUDE report ... not found"** (stale include in ABAP not present in git)
|
|
436
|
+
- Pull repeatedly fails with **"Activation cancelled"** and re-pulling doesn't fix it
|
|
437
|
+
- Object has a corrupt inactive version that permanently blocks activation
|
|
438
|
+
|
|
439
|
+
In these cases, suggest the fix and wait for confirmation:
|
|
440
|
+
|
|
441
|
+
```
|
|
442
|
+
"The object ZCL_FOO has an inconsistent state (stale include in ABAP).
|
|
443
|
+
This can be fixed by dropping and re-pulling it:
|
|
444
|
+
|
|
445
|
+
abapgit-agent drop --files abap/zcl_foo.clas.abap --pull
|
|
446
|
+
|
|
447
|
+
This will delete ZCL_FOO from ABAP and immediately re-activate it from git.
|
|
448
|
+
Confirm when ready."
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**What `drop --pull` does:**
|
|
452
|
+
1. Physically deletes the object from the ABAP system
|
|
453
|
+
2. Immediately re-pulls and re-activates it from git (clean state)
|
|
454
|
+
3. Does NOT touch the git repository file
|
|
455
|
+
|
|
456
|
+
**Constraint:** Only CLAS, INTF, PROG, TABL, TTYP are supported. DTEL (data elements) are rejected — edit the XML and use `pull` instead.
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
### 11. Probe and PoC Objects — Always Z/Y, Never in SAP Packages
|
|
428
461
|
|
|
429
462
|
```
|
|
430
463
|
❌ WRONG: Create a probe/PoC object with the project's SAP namespace prefix
|
|
@@ -450,7 +483,7 @@ Never assume — wait for the user's answer before proceeding.
|
|
|
450
483
|
|
|
451
484
|
---
|
|
452
485
|
|
|
453
|
-
###
|
|
486
|
+
### 12. Troubleshooting ABAP Issues
|
|
454
487
|
|
|
455
488
|
| Symptom | Tool | When |
|
|
456
489
|
|---|---|---|
|
|
@@ -722,18 +755,18 @@ abapgit-agent pull --files src/<name>.clas.abap --sync-xml
|
|
|
722
755
|
|
|
723
756
|
```
|
|
724
757
|
Modified ABAP files?
|
|
725
|
-
├─ CLAS/INTF/PROG/DDLS files?
|
|
758
|
+
├─ CLAS/INTF/PROG/DDLS/FUGR files?
|
|
726
759
|
│ ├─ Independent files (no cross-dependencies)?
|
|
727
760
|
│ │ └─ ✅ Use: syntax → [abaplint] → commit → push → pull --sync-xml
|
|
728
761
|
│ └─ Dependent files (interface + class, class uses class)?
|
|
729
762
|
│ └─ ✅ Use: skip syntax → [abaplint] → commit → push → pull --sync-xml
|
|
730
|
-
└─ Other types (
|
|
763
|
+
└─ Other types (TABL, STRU, DTEL, TTYP, etc.)?
|
|
731
764
|
├─ XML-only objects (TABL, STRU, DTEL, TTYP, DOMA, MSAG)?
|
|
732
765
|
│ └─ ✅ Use: skip syntax → [abaplint] → commit → push → pull --files abap/ztable.tabl.xml --sync-xml
|
|
733
766
|
├─ DCLS (CDS access control)?
|
|
734
767
|
│ └─ ✅ Use: skip syntax → [abaplint] → commit → push → pull --files abap/zc_view.dcls.xml --sync-xml
|
|
735
768
|
│ ⚠️ Pass the .xml file — pull --files does NOT accept .asdcls extensions
|
|
736
|
-
└─
|
|
769
|
+
└─ Other complex objects?
|
|
737
770
|
└─ ✅ Use: skip syntax → [abaplint] → commit → push → pull --sync-xml → (if errors: inspect)
|
|
738
771
|
|
|
739
772
|
[abaplint] = run abapgit-agent lint only if .abaplint.json exists in repo root
|
|
@@ -761,6 +794,7 @@ Modified ABAP files?
|
|
|
761
794
|
| `ref --topic cds-testing` | CDS Testing (Test Double Framework) |
|
|
762
795
|
| `ref --topic json` | JSON Handling |
|
|
763
796
|
| `ref --topic common-errors` | Common ABAP Errors - Quick Fixes |
|
|
797
|
+
| `ref --topic string-template` | String Templates — syntax, escaping `\{` `\}`, JSON payloads |
|
|
764
798
|
| `ref --topic abapgit` | abapGit XML Metadata — **use for CDS/DDLS XML**, also CLAS, INTF, PROG, DCLS, FUGR |
|
|
765
799
|
| `ref --topic abapgit-xml-only` | abapGit XML Metadata — XML-only objects (TABL, STRU, DTEL, TTYP, DOMA, MSAG) |
|
|
766
800
|
| `ref --topic abapgit-fugr` | abapGit XML Metadata — Function Group (FUGR) details |
|
|
@@ -80,6 +80,41 @@ lv_response = lv_response && '"key":"val"}'.
|
|
|
80
80
|
| `DATA(lv) = 'literal'.` followed by `&&` | ❌ | Infers `C LENGTH N`, truncates |
|
|
81
81
|
| `DATA(lv) = 'literal'.` used only in `\|{ lv }\|` | ⚠️ | Technically works but misleading — prefer explicit type |
|
|
82
82
|
| `DATA(lv) = 'X'.` used as abap_bool flag | ✅ | `C LENGTH 1` is correct for flags |
|
|
83
|
+
| `DATA(lv) = COND string( WHEN ... THEN str+off ELSE str ).` | ❌ | Offset notation on `string` inside COND raises `CX_SY_RANGE_OUT_OF_BOUNDS` at runtime |
|
|
84
|
+
|
|
85
|
+
### The offset-notation-in-COND trap
|
|
86
|
+
|
|
87
|
+
Offset/length notation (`str+off` or `str(len)`) on a `string`-typed variable **cannot appear
|
|
88
|
+
as a result expression inside `COND`** — even when the `COND` is explicitly typed as `string`.
|
|
89
|
+
ABAP evaluates both branch types at generation time and the offset expression on a dynamic
|
|
90
|
+
string is invalid, causing a `CX_SY_RANGE_OUT_OF_BOUNDS` crash at runtime.
|
|
91
|
+
|
|
92
|
+
```abap
|
|
93
|
+
DATA lv_file TYPE string VALUE 'src/foo.clas.abap'.
|
|
94
|
+
DATA(lv_pos) = find( val = lv_file sub = '/' occ = -1 ).
|
|
95
|
+
|
|
96
|
+
* WRONG — CX_SY_RANGE_OUT_OF_BOUNDS at runtime
|
|
97
|
+
DATA(lv_path) = COND string(
|
|
98
|
+
WHEN lv_pos > 0 THEN '/' && lv_file(lv_pos + 1) " ← offset on string = crash
|
|
99
|
+
ELSE '/' ).
|
|
100
|
+
|
|
101
|
+
* CORRECT — use substring() which returns string and is safe in COND
|
|
102
|
+
DATA(lv_path) = COND string(
|
|
103
|
+
WHEN lv_pos > 0 THEN '/' && substring( val = lv_file len = lv_pos + 1 )
|
|
104
|
+
ELSE '/' ).
|
|
105
|
+
|
|
106
|
+
* ALSO CORRECT — keep IF/ELSE with DATA x TYPE string
|
|
107
|
+
DATA lv_path TYPE string.
|
|
108
|
+
IF lv_pos > 0.
|
|
109
|
+
DATA(lv_len) = lv_pos + 1.
|
|
110
|
+
lv_path = '/' && lv_file(lv_len).
|
|
111
|
+
ELSE.
|
|
112
|
+
lv_path = '/'.
|
|
113
|
+
ENDIF.
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Rule**: whenever the prefer_inline quickfix would produce `COND ... THEN str+off` or
|
|
117
|
+
`COND ... THEN str(len)`, use `substring()` instead, or keep the `IF/ELSE` form.
|
|
83
118
|
|
|
84
119
|
### Rule of thumb
|
|
85
120
|
|
|
@@ -8,7 +8,7 @@ grand_parent: ABAP Development
|
|
|
8
8
|
|
|
9
9
|
# Common ABAP Errors - Quick Fixes
|
|
10
10
|
|
|
11
|
-
**Searchable keywords**: error, syntax error, field mismatch, fixed point, comma, host variable, @ prefix, type incompatible
|
|
11
|
+
**Searchable keywords**: error, syntax error, field mismatch, fixed point, comma, host variable, @ prefix, type incompatible, string template, expression limiter, literal brace, http 500, xml metadata mismatch, interface description, descript, recompilation, offset notation, cond trap, cx_sy_range_out_of_bounds
|
|
12
12
|
|
|
13
13
|
## Fixed Point Arithmetic Error
|
|
14
14
|
|
|
@@ -119,6 +119,83 @@ or declare the variable explicitly with `TYPE string`.
|
|
|
119
119
|
|
|
120
120
|
---
|
|
121
121
|
|
|
122
|
+
## Offset Notation on String in COND — CX_SY_RANGE_OUT_OF_BOUNDS
|
|
123
|
+
|
|
124
|
+
**Symptom**: Pull succeeds but ABAP crashes at runtime with `CX_SY_RANGE_OUT_OF_BOUNDS`
|
|
125
|
+
inside a method using `COND`.
|
|
126
|
+
|
|
127
|
+
**Root cause**: Offset/length notation on a `string` variable (`str+off`, `str(len)`)
|
|
128
|
+
**cannot be used as a result expression inside `COND`** — even with an explicit `COND string(...)` type.
|
|
129
|
+
The abaplint `prefer_inline` quickfix can introduce this automatically when converting
|
|
130
|
+
an `IF/ELSE` block that assigns from a string-variable offset.
|
|
131
|
+
|
|
132
|
+
```abap
|
|
133
|
+
* WRONG — crashes at runtime with CX_SY_RANGE_OUT_OF_BOUNDS
|
|
134
|
+
DATA(lv_path) = COND string(
|
|
135
|
+
WHEN lv_pos > 0 THEN '/' && lv_file(lv_pos + 1) " ← string offset in COND
|
|
136
|
+
ELSE '/' ).
|
|
137
|
+
|
|
138
|
+
* CORRECT — use substring() instead
|
|
139
|
+
DATA(lv_path) = COND string(
|
|
140
|
+
WHEN lv_pos > 0 THEN '/' && substring( val = lv_file len = lv_pos + 1 )
|
|
141
|
+
ELSE '/' ).
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Fix**: Replace `str+off` / `str(len)` inside `COND` results with `substring( val = str off = off )` or
|
|
145
|
+
`substring( val = str len = len )`, or revert to the `IF/ELSE` form with `DATA x TYPE string`.
|
|
146
|
+
|
|
147
|
+
→ See `abaplint.md` for the full safe/unsafe table for `prefer_inline`.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## XML Metadata Mismatch — HTTP 500 on Pull
|
|
152
|
+
|
|
153
|
+
**Symptom**: `pull` returns HTTP 500 with no short dump (ST22) for the request user.
|
|
154
|
+
Other objects pull fine; only a specific interface or class fails.
|
|
155
|
+
|
|
156
|
+
**Root cause**: The `.intf.xml` or `.clas.xml` file in git has a metadata field (e.g. `DESCRIPT`,
|
|
157
|
+
`UNICODE`, `STATE`) that differs from the active ABAP system value. abapGit writes the git
|
|
158
|
+
value back to VSEOINTERF/VSEOCLASS, which forces the SAP class framework to recompile all
|
|
159
|
+
implementing/inheriting objects. If any implementing class has a syntax or generation
|
|
160
|
+
issue with the current active source, the recompilation crashes outside the normal exception
|
|
161
|
+
handler — producing HTTP 500 with no dump.
|
|
162
|
+
|
|
163
|
+
**Diagnosis**:
|
|
164
|
+
1. No dump in ST22 for your user → not a user-code crash
|
|
165
|
+
2. Only one specific INTF/CLAS fails → metadata delta in its XML triggers recompilation
|
|
166
|
+
3. Check what differs: `abapgit-agent view --objects ZIF_MY_INTF --type INTF` and compare
|
|
167
|
+
`DESCRIPT` / other fields with the XML file in git
|
|
168
|
+
|
|
169
|
+
**Fix**: Sync the XML to match the ABAP system value. The easiest way:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Pull without --sync-xml first to let abapGit rewrite the XML from the system
|
|
173
|
+
abapgit-agent pull --files abap/zif_my_intf.intf.abap --sync-xml
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Or manually correct the field in the XML file to match the system value, then push and pull.
|
|
177
|
+
|
|
178
|
+
**Prevention**: Always use `--sync-xml` after pulling new objects so the XML stays in sync
|
|
179
|
+
with what the ABAP serializer produces.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
**Error**: `Expression limiter '{' in string template not followed by space`
|
|
184
|
+
|
|
185
|
+
Literal `{` and `}` (e.g. in JSON payloads) must be escaped as `\{` and `\}` inside `|...|`.
|
|
186
|
+
|
|
187
|
+
```abap
|
|
188
|
+
" WRONG — unescaped { in JSON triggers parse error
|
|
189
|
+
rv_result = |{"success":"X","name":"{ lv_name }"}|.
|
|
190
|
+
|
|
191
|
+
" CORRECT
|
|
192
|
+
rv_result = |\{"success":"X","name":"{ lv_name }"\}|.
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
→ Full escaping rules and examples: `abapgit-agent ref --topic string-template`
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
122
199
|
## See Also
|
|
123
200
|
- **ABAP SQL** (sql.md) - for SQL syntax rules
|
|
124
201
|
- **CDS Views** (cds.md) - for CDS selection patterns
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: String Templates
|
|
4
|
+
nav_order: 25
|
|
5
|
+
parent: ABAP Coding Guidelines
|
|
6
|
+
grand_parent: ABAP Development
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# String Templates
|
|
10
|
+
|
|
11
|
+
**Searchable keywords**: string template, pipe, `|...|`, literal brace, escape, expression limiter, JSON, `\{`, `\}`
|
|
12
|
+
|
|
13
|
+
## Syntax
|
|
14
|
+
|
|
15
|
+
String templates are delimited by `|...|`. Embedded expressions use `{ expr }`:
|
|
16
|
+
|
|
17
|
+
```abap
|
|
18
|
+
DATA(s) = |Hello { lv_name }!|.
|
|
19
|
+
DATA(s) = |User: { cl_abap_context_info=>get_user_technical_name( ) }|.
|
|
20
|
+
DATA(s) = |Length: { strlen( lv_text ) }|.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Escaping Special Characters
|
|
24
|
+
|
|
25
|
+
Inside `|...|`, four characters have special meaning and must be escaped with `\`:
|
|
26
|
+
|
|
27
|
+
| Character | Escape as | Produces |
|
|
28
|
+
|-----------|-----------|---------|
|
|
29
|
+
| `{` | `\{` | literal `{` |
|
|
30
|
+
| `}` | `\}` | literal `}` |
|
|
31
|
+
| `\` | `\\` | literal `\` |
|
|
32
|
+
| `\|` | `\|` | literal `\|` |
|
|
33
|
+
|
|
34
|
+
```abap
|
|
35
|
+
" Produces: \ | { }
|
|
36
|
+
DATA(s) = |\\ \| \{ \}|.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## JSON Payloads — The Most Common Mistake
|
|
40
|
+
|
|
41
|
+
JSON objects start with `{` — which ABAP treats as an expression delimiter.
|
|
42
|
+
**Always escape outer JSON braces as `\{` and `\}`.**
|
|
43
|
+
|
|
44
|
+
```abap
|
|
45
|
+
" WRONG — "Expression limiter '{' in string template not followed by space"
|
|
46
|
+
rv_result = |{"success":"X","name":"{ lv_name }"}|.
|
|
47
|
+
" ^ unescaped { triggers parse error
|
|
48
|
+
|
|
49
|
+
" CORRECT — outer JSON braces escaped, expression { } left as-is
|
|
50
|
+
rv_result = |\{"success":"X","name":"{ lv_name }"\}|.
|
|
51
|
+
|
|
52
|
+
" CORRECT — error with method call
|
|
53
|
+
rv_result = |\{"success":"","error":"{ lx_error->get_text( ) }"\}|.
|
|
54
|
+
|
|
55
|
+
" CORRECT — multiple embedded fields
|
|
56
|
+
rv_result = |\{"success":"X","object":"{ lv_obj_name }","type":"{ lv_obj_type }"\}|.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Control Characters
|
|
60
|
+
|
|
61
|
+
```abap
|
|
62
|
+
DATA(s) = |line1\nline2|. " newline
|
|
63
|
+
DATA(s) = |col1\tcol2|. " tab
|
|
64
|
+
DATA(s) = |line1\r\nline2|. " CR+LF
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Chaining with `&&`
|
|
68
|
+
|
|
69
|
+
```abap
|
|
70
|
+
DATA(s) = |{ lv_a }| && ` separator ` && |{ lv_b }|.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Performance Note
|
|
74
|
+
|
|
75
|
+
A string template containing only literal text (no `{ }` expressions) is evaluated at
|
|
76
|
+
runtime. Prefer backquote literals for pure text:
|
|
77
|
+
|
|
78
|
+
```abap
|
|
79
|
+
" Prefer this for literal-only strings
|
|
80
|
+
DATA(s) = `Hello World`.
|
|
81
|
+
|
|
82
|
+
" Not this (runtime overhead with no benefit)
|
|
83
|
+
DATA(s) = |Hello World|.
|
|
84
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abapgit-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
|
|
5
5
|
"files": [
|
|
6
6
|
"bin/",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"test:cmd:preview": "node tests/run-all.js --cmd --command=preview",
|
|
37
37
|
"test:cmd:tree": "node tests/run-all.js --cmd --command=tree",
|
|
38
38
|
"test:cmd:dump": "node tests/run-all.js --cmd --command=dump",
|
|
39
|
+
"test:drop": "node tests/run-all.js --drop",
|
|
39
40
|
"test:cmd:debug": "node tests/run-all.js --cmd --command=debug",
|
|
40
41
|
"test:debug": "node tests/run-all.js --debug",
|
|
41
42
|
"test:debug:scenarios": "bash tests/integration/debug-scenarios.sh",
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop command - Physically delete a single ABAP object from the ABAP system
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { printHttpError } = require('../utils/format-error');
|
|
6
|
+
|
|
7
|
+
// Map file extension (second-to-last part) to ABAP object type label
|
|
8
|
+
const EXT_TO_TYPE = {
|
|
9
|
+
clas: 'CLAS', intf: 'INTF', prog: 'PROG', fugr: 'FUGR',
|
|
10
|
+
tabl: 'TABL', dtel: 'DTEL', ttyp: 'TTYP', doma: 'DOMA',
|
|
11
|
+
ddls: 'DDLS', dcls: 'DCLS', msag: 'MSAG', stru: 'STRU'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Derive a display label for the object from the file path.
|
|
16
|
+
* e.g. "abap/zcl_foo.clas.abap" → { name: "ZCL_FOO", type: "CLAS" }
|
|
17
|
+
*/
|
|
18
|
+
function objectFromFile(filePath) {
|
|
19
|
+
const base = filePath.split('/').pop();
|
|
20
|
+
const parts = base.split('.');
|
|
21
|
+
if (parts.length < 3) return null;
|
|
22
|
+
const name = parts[0].toUpperCase();
|
|
23
|
+
const typeExt = parts[1].toLowerCase();
|
|
24
|
+
const type = EXT_TO_TYPE[typeExt] || typeExt.toUpperCase();
|
|
25
|
+
return { name, type };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
name: 'drop',
|
|
30
|
+
description: 'Physically delete a single ABAP object from the ABAP system',
|
|
31
|
+
requiresAbapConfig: true,
|
|
32
|
+
requiresVersionCheck: true,
|
|
33
|
+
|
|
34
|
+
async execute(args, context) {
|
|
35
|
+
const { loadConfig, AbapHttp, gitUtils, getTransport, getConflictSettings } = context;
|
|
36
|
+
|
|
37
|
+
// Show help if requested
|
|
38
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
39
|
+
console.log(`
|
|
40
|
+
Usage:
|
|
41
|
+
abapgit-agent drop --files <file>
|
|
42
|
+
abapgit-agent drop --files <file> --transport <TRANSPORT>
|
|
43
|
+
abapgit-agent drop --files <file> --pull
|
|
44
|
+
|
|
45
|
+
Description:
|
|
46
|
+
Physically deletes a single ABAP object from the ABAP system using abapGit's
|
|
47
|
+
own object serializer. The object type and name are derived from the file path.
|
|
48
|
+
|
|
49
|
+
Use --pull to immediately re-activate the object from git after deletion
|
|
50
|
+
(useful to force a clean re-installation).
|
|
51
|
+
|
|
52
|
+
Parameters:
|
|
53
|
+
--files <file> Path to the ABAP source or XML file (required).
|
|
54
|
+
Accepted extensions: .abap, .asddls, .tabl.xml, etc.
|
|
55
|
+
The file must exist on disk.
|
|
56
|
+
--transport <TRANSPORT> Transport request (e.g. DEVK900001). Optional.
|
|
57
|
+
--pull Re-activate the object via pull after deletion.
|
|
58
|
+
--conflict-mode <mode> Conflict mode for --pull: abort (default) or ignore.
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
abapgit-agent drop --files abap/zcl_foo.clas.abap
|
|
62
|
+
abapgit-agent drop --files abap/zcl_foo.clas.abap --pull
|
|
63
|
+
abapgit-agent drop --files abap/zmy_table.tabl.xml --transport DEVK900001
|
|
64
|
+
`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const filesArgIndex = args.indexOf('--files');
|
|
69
|
+
const transportArgIndex = args.indexOf('--transport');
|
|
70
|
+
const conflictModeArgIndex = args.indexOf('--conflict-mode');
|
|
71
|
+
const doPull = args.includes('--pull');
|
|
72
|
+
|
|
73
|
+
if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
|
|
74
|
+
console.error('❌ Error: --files is required');
|
|
75
|
+
console.error(' Usage: abapgit-agent drop --files <file>');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const filePath = args[filesArgIndex + 1].trim();
|
|
80
|
+
|
|
81
|
+
// Validate file exists on disk
|
|
82
|
+
const fs = require('fs');
|
|
83
|
+
if (!fs.existsSync(filePath)) {
|
|
84
|
+
console.error(`❌ Error: file not found: ${filePath}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Validate file extension (same rules as pull --files)
|
|
89
|
+
const base = filePath.split('/').pop();
|
|
90
|
+
const parts = base.split('.');
|
|
91
|
+
const lastExt = parts[parts.length - 1].toLowerCase();
|
|
92
|
+
const ABAP_SOURCE_EXTS = new Set(['abap', 'asddls']);
|
|
93
|
+
const isXmlOnlyObject = parts.length === 3 && parts[0].length > 0 && lastExt === 'xml';
|
|
94
|
+
if (!ABAP_SOURCE_EXTS.has(lastExt) && !isXmlOnlyObject) {
|
|
95
|
+
console.error('❌ Error: --files must be an ABAP source file (.abap, .asddls) or an XML-only object file (name.type.xml).');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const obj = objectFromFile(filePath);
|
|
100
|
+
|
|
101
|
+
if (obj && obj.type === 'DTEL') {
|
|
102
|
+
console.error(`❌ drop does not support DTEL objects.`);
|
|
103
|
+
console.error(` Data elements cannot be re-activated after deletion due to SAP CBDA`);
|
|
104
|
+
console.error(` activation engine limitations. Edit the XML file and run pull instead.`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const transportRequest = transportArgIndex !== -1 ? args[transportArgIndex + 1] : (getTransport ? getTransport() : null);
|
|
109
|
+
const conflictMode = conflictModeArgIndex !== -1 ? args[conflictModeArgIndex + 1] : (getConflictSettings ? getConflictSettings().mode : 'abort');
|
|
110
|
+
|
|
111
|
+
console.log(`\n🗑️ Dropping ${obj ? obj.type + ' ' + obj.name : filePath} from ABAP system...`);
|
|
112
|
+
if (transportRequest) {
|
|
113
|
+
console.log(` Transport: ${transportRequest}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const config = loadConfig();
|
|
117
|
+
const http = new AbapHttp(config);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
121
|
+
|
|
122
|
+
const data = { file: filePath };
|
|
123
|
+
if (transportRequest) {
|
|
124
|
+
data.transport_request = transportRequest;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = await http.post('/sap/bc/z_abapgit_agent/drop', data, { csrfToken });
|
|
128
|
+
|
|
129
|
+
const success = result.SUCCESS || result.success;
|
|
130
|
+
const objectName = result.OBJECT || result.object;
|
|
131
|
+
const objectType = result.TYPE || result.type;
|
|
132
|
+
const message = result.MESSAGE || result.message;
|
|
133
|
+
const error = result.ERROR || result.error;
|
|
134
|
+
|
|
135
|
+
if (success === 'X' || success === true) {
|
|
136
|
+
console.log(`✅ Object deleted successfully.`);
|
|
137
|
+
if (objectName && objectType) {
|
|
138
|
+
console.log(` Object: ${objectName} (${objectType})`);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
console.error(`❌ Failed to delete object`);
|
|
142
|
+
console.error(` Error: ${error || message || 'Unknown error'}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
printHttpError(error, {});
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --pull: re-activate the object from git
|
|
151
|
+
if (doPull) {
|
|
152
|
+
console.log(`\n↩️ Re-pulling from git...`);
|
|
153
|
+
const pullCommand = require('./pull');
|
|
154
|
+
const gitUrl = gitUtils.getRemoteUrl();
|
|
155
|
+
const branch = gitUtils.getBranch();
|
|
156
|
+
if (!gitUrl) {
|
|
157
|
+
console.error('❌ Cannot re-pull: no git remote configured.');
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await pullCommand.pull(
|
|
162
|
+
gitUrl, branch, [filePath], transportRequest || null,
|
|
163
|
+
loadConfig, AbapHttp, false, undefined, conflictMode, false, false
|
|
164
|
+
);
|
|
165
|
+
} catch (pullError) {
|
|
166
|
+
// pull() already printed the error
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
package/src/commands/index.js
CHANGED
package/src/commands/syntax.js
CHANGED
|
@@ -48,6 +48,7 @@ module.exports = {
|
|
|
48
48
|
// Build objects array from files
|
|
49
49
|
// Group class files together (main + locals)
|
|
50
50
|
const classFilesMap = new Map(); // className -> { main, locals_def, locals_imp }
|
|
51
|
+
const fugrGroupMap = new Map(); // groupName -> { dir, fmFiles: Map<fmName, source> }
|
|
51
52
|
const objects = [];
|
|
52
53
|
|
|
53
54
|
for (const file of syntaxFiles) {
|
|
@@ -83,6 +84,35 @@ module.exports = {
|
|
|
83
84
|
objType = 'DDLS';
|
|
84
85
|
objName = baseName.split('.')[0].toUpperCase();
|
|
85
86
|
fileKind = 'main';
|
|
87
|
+
} else if (baseName.includes('.fugr.')) {
|
|
88
|
+
objType = 'FUGR';
|
|
89
|
+
const parts = baseName.split('.'); // e.g. ['zmy_fugr', 'fugr', 'zmy_my_function', 'abap']
|
|
90
|
+
objName = parts[0].toUpperCase(); // group name e.g. 'ZMY_FUGR'
|
|
91
|
+
const includeFile = parts[2] || ''; // e.g. 'zmy_my_function', 'lzmy_fugrtop', 'saplzmy_fugr'
|
|
92
|
+
const groupLower = parts[0].toLowerCase();
|
|
93
|
+
const isTopInclude = new RegExp(`^l${groupLower}top$`, 'i').test(includeFile);
|
|
94
|
+
const isUInclude = new RegExp(`^l${groupLower}u\\d+$`, 'i').test(includeFile);
|
|
95
|
+
const isSapl = includeFile.toLowerCase().startsWith('sapl');
|
|
96
|
+
const isFm = !isTopInclude && !isUInclude && !isSapl && includeFile !== '';
|
|
97
|
+
|
|
98
|
+
// Read source and store in fugrGroupMap
|
|
99
|
+
const filePath = pathModule.resolve(file);
|
|
100
|
+
if (!fs.existsSync(filePath)) {
|
|
101
|
+
console.error(` Error: File not found: ${file}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
105
|
+
const dir = pathModule.dirname(filePath);
|
|
106
|
+
|
|
107
|
+
if (!fugrGroupMap.has(objName)) {
|
|
108
|
+
fugrGroupMap.set(objName, { dir, fmFiles: new Map() });
|
|
109
|
+
}
|
|
110
|
+
if (isFm) {
|
|
111
|
+
const fmName = includeFile.toUpperCase();
|
|
112
|
+
fugrGroupMap.get(objName).fmFiles.set(fmName, source);
|
|
113
|
+
}
|
|
114
|
+
// Skip adding to objects here — handled after auto-detection below
|
|
115
|
+
continue;
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
// Read source from file
|
|
@@ -236,12 +266,74 @@ module.exports = {
|
|
|
236
266
|
}
|
|
237
267
|
}
|
|
238
268
|
|
|
269
|
+
// Helper: read FIXPT from SAPL XML for a function group
|
|
270
|
+
function readFugrFixpt(dir, groupName) {
|
|
271
|
+
const xmlFile = pathModule.join(dir, `${groupName.toLowerCase()}.fugr.sapl${groupName.toLowerCase()}.xml`);
|
|
272
|
+
if (fs.existsSync(xmlFile)) {
|
|
273
|
+
const xmlContent = fs.readFileSync(xmlFile, 'utf8');
|
|
274
|
+
const fixptMatch = xmlContent.match(/<FIXPT>([^<]+)<\/FIXPT>/);
|
|
275
|
+
if (fixptMatch && fixptMatch[1] === 'X') return 'X';
|
|
276
|
+
}
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Helper: classify a FUGR include filename — returns fm name (uppercase) or null if not an FM file
|
|
281
|
+
function getFugrFmName(includeFile, groupLower) {
|
|
282
|
+
if (!includeFile) return null;
|
|
283
|
+
const isTopInclude = new RegExp(`^l${groupLower}top$`, 'i').test(includeFile);
|
|
284
|
+
const isUInclude = new RegExp(`^l${groupLower}u\\d+$`, 'i').test(includeFile);
|
|
285
|
+
const isSapl = includeFile.toLowerCase().startsWith('sapl');
|
|
286
|
+
if (isTopInclude || isUInclude || isSapl) return null;
|
|
287
|
+
return includeFile.toUpperCase();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Auto-detect all FM files for each function group and build objects
|
|
291
|
+
for (const [groupName, groupData] of fugrGroupMap) {
|
|
292
|
+
const { dir } = groupData;
|
|
293
|
+
const groupLower = groupName.toLowerCase();
|
|
294
|
+
const prefix = `${groupLower}.fugr.`;
|
|
295
|
+
|
|
296
|
+
// Scan directory for all FUGR files belonging to this group
|
|
297
|
+
let allFiles;
|
|
298
|
+
try {
|
|
299
|
+
allFiles = fs.readdirSync(dir);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
allFiles = [];
|
|
302
|
+
}
|
|
303
|
+
for (const f of allFiles) {
|
|
304
|
+
if (!f.toLowerCase().startsWith(prefix) || !f.toLowerCase().endsWith('.abap')) continue;
|
|
305
|
+
const parts = f.split('.');
|
|
306
|
+
const includeFile = parts[2] || '';
|
|
307
|
+
const fmName = getFugrFmName(includeFile, groupLower);
|
|
308
|
+
if (fmName && !groupData.fmFiles.has(fmName)) {
|
|
309
|
+
groupData.fmFiles.set(fmName, fs.readFileSync(pathModule.join(dir, f), 'utf8'));
|
|
310
|
+
if (!jsonOutput) console.log(` Auto-detected: ${f}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (groupData.fmFiles.size === 0) {
|
|
315
|
+
if (!jsonOutput) console.error(` Warning: No FM source files found for FUGR ${groupName}`);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const fixpt = readFugrFixpt(dir, groupName);
|
|
320
|
+
|
|
321
|
+
// Add one object per FM
|
|
322
|
+
for (const [fmName, source] of groupData.fmFiles) {
|
|
323
|
+
objects.push({
|
|
324
|
+
type: 'FUGR',
|
|
325
|
+
name: groupName,
|
|
326
|
+
source: source,
|
|
327
|
+
fugr_include_name: fmName,
|
|
328
|
+
fixpt: fixpt
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
239
333
|
if (objects.length === 0) {
|
|
240
334
|
console.error(' No valid files to check');
|
|
241
335
|
process.exit(1);
|
|
242
336
|
}
|
|
243
|
-
|
|
244
|
-
// Send request
|
|
245
337
|
const data = {
|
|
246
338
|
objects: objects,
|
|
247
339
|
uccheck: cloudMode ? '5' : 'X'
|
|
@@ -258,7 +350,8 @@ module.exports = {
|
|
|
258
350
|
console.log(JSON.stringify(result, null, 2));
|
|
259
351
|
} else {
|
|
260
352
|
// Display results for each object
|
|
261
|
-
for (
|
|
353
|
+
for (let i = 0; i < results.length; i++) {
|
|
354
|
+
const res = results[i];
|
|
262
355
|
const objSuccess = res.SUCCESS !== undefined ? res.SUCCESS : res.success;
|
|
263
356
|
const objType = res.OBJECT_TYPE || res.object_type || 'UNKNOWN';
|
|
264
357
|
const objName = res.OBJECT_NAME || res.object_name || 'UNKNOWN';
|
|
@@ -267,13 +360,18 @@ module.exports = {
|
|
|
267
360
|
const warnings = res.WARNINGS || res.warnings || [];
|
|
268
361
|
const objMessage = res.MESSAGE || res.message || '';
|
|
269
362
|
|
|
363
|
+
// For FUGR: show which FM was checked alongside the group name
|
|
364
|
+
const sentObj = objects[i] || {};
|
|
365
|
+
const fugrFmLabel = (objType === 'FUGR' && sentObj.fugr_include_name)
|
|
366
|
+
? ` (${sentObj.fugr_include_name})` : '';
|
|
367
|
+
|
|
270
368
|
if (objSuccess) {
|
|
271
|
-
console.log(`✅ ${objType} ${objName} - Syntax check passed`);
|
|
369
|
+
console.log(`✅ ${objType} ${objName}${fugrFmLabel} - Syntax check passed`);
|
|
272
370
|
if (warnings.length > 0) {
|
|
273
371
|
console.log(` (${warnings.length} warning(s))`);
|
|
274
372
|
}
|
|
275
373
|
} else {
|
|
276
|
-
console.log(`❌ ${objType} ${objName} - Syntax check failed (${errorCount} error(s))`);
|
|
374
|
+
console.log(`❌ ${objType} ${objName}${fugrFmLabel} - Syntax check failed (${errorCount} error(s))`);
|
|
277
375
|
console.log('');
|
|
278
376
|
console.log('Errors:');
|
|
279
377
|
console.log('─'.repeat(60));
|
|
@@ -287,19 +385,25 @@ module.exports = {
|
|
|
287
385
|
|
|
288
386
|
// Display which file/include the error is in
|
|
289
387
|
if (include) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
'testclasses': { display: 'Test classes', suffix: '.clas.testclasses.abap' }
|
|
295
|
-
};
|
|
296
|
-
const includeInfo = includeMap[include] || { display: include, suffix: '' };
|
|
297
|
-
|
|
298
|
-
// Show both display name and filename
|
|
299
|
-
if (includeInfo.suffix) {
|
|
300
|
-
console.log(` In: ${includeInfo.display} (${objName.toLowerCase()}${includeInfo.suffix})`);
|
|
388
|
+
// For FUGR: include = lowercase FM name → display as '<group>.fugr.<fm_name>.abap'
|
|
389
|
+
if (objType === 'FUGR') {
|
|
390
|
+
const fugrFile = `${objName.toLowerCase()}.fugr.${include}.abap`;
|
|
391
|
+
console.log(` In: Function module ${include.toUpperCase()} (${fugrFile})`);
|
|
301
392
|
} else {
|
|
302
|
-
|
|
393
|
+
const includeMap = {
|
|
394
|
+
'main': { display: 'Main class', suffix: '.clas.abap' },
|
|
395
|
+
'locals_def': { display: 'Local definitions', suffix: '.clas.locals_def.abap' },
|
|
396
|
+
'locals_imp': { display: 'Local implementations', suffix: '.clas.locals_imp.abap' },
|
|
397
|
+
'testclasses': { display: 'Test classes', suffix: '.clas.testclasses.abap' }
|
|
398
|
+
};
|
|
399
|
+
const includeInfo = includeMap[include] || { display: include, suffix: '' };
|
|
400
|
+
|
|
401
|
+
// Show both display name and filename
|
|
402
|
+
if (includeInfo.suffix) {
|
|
403
|
+
console.log(` In: ${includeInfo.display} (${objName.toLowerCase()}${includeInfo.suffix})`);
|
|
404
|
+
} else {
|
|
405
|
+
console.log(` In: ${includeInfo.display}`);
|
|
406
|
+
}
|
|
303
407
|
}
|
|
304
408
|
}
|
|
305
409
|
if (methodName) {
|