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 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 (FUGR, TABL, etc.)**: Skip syntax, proceed to commit/push/pull.
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. Probe and PoC Objects Always Z/Y, Never in SAP Packages
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
- ### 11. Troubleshooting ABAP Issues
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 (FUGR, TABL, STRU, DTEL, TTYP, etc.)?
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
- └─ FUGR and other complex objects?
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.15.3",
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
+ };
@@ -30,5 +30,6 @@ module.exports = {
30
30
  pull: require('./pull'),
31
31
  upgrade: require('./upgrade'),
32
32
  transport: require('./transport'),
33
- lint: require('./lint')
33
+ lint: require('./lint'),
34
+ drop: require('./drop')
34
35
  };
@@ -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 (const res of results) {
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
- const includeMap = {
291
- 'main': { display: 'Main class', suffix: '.clas.abap' },
292
- 'locals_def': { display: 'Local definitions', suffix: '.clas.locals_def.abap' },
293
- 'locals_imp': { display: 'Local implementations', suffix: '.clas.locals_imp.abap' },
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
- console.log(` In: ${includeInfo.display}`);
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) {