abapgit-agent 1.11.1 → 1.11.2

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.
Files changed (31) hide show
  1. package/abap/.github/copilot-instructions.md +9 -9
  2. package/abap/CLAUDE.md +48 -539
  3. package/abap/guidelines/{08_abapgit.md → abapgit.md} +1 -1
  4. package/abap/guidelines/branch-workflow.md +137 -0
  5. package/abap/guidelines/cds-testing.md +25 -0
  6. package/abap/guidelines/{04_cds.md → cds.md} +4 -4
  7. package/abap/guidelines/{10_common_errors.md → common-errors.md} +3 -3
  8. package/abap/guidelines/debug-dump.md +33 -0
  9. package/abap/guidelines/debug-session.md +280 -0
  10. package/abap/guidelines/index.md +50 -0
  11. package/abap/guidelines/object-creation.md +51 -0
  12. package/abap/guidelines/{06_objects.md → objects.md} +2 -2
  13. package/abap/guidelines/{01_sql.md → sql.md} +2 -2
  14. package/abap/guidelines/{03_testing.md → testing.md} +3 -3
  15. package/abap/guidelines/workflow-detailed.md +255 -0
  16. package/package.json +1 -1
  17. package/src/commands/debug.js +54 -20
  18. package/src/commands/inspect.js +5 -3
  19. package/src/commands/pull.js +4 -1
  20. package/src/commands/transport.js +3 -1
  21. package/src/commands/unit.js +10 -10
  22. package/src/commands/view.js +238 -1
  23. package/src/utils/abap-http.js +6 -1
  24. package/src/utils/abap-reference.js +4 -4
  25. package/src/utils/adt-http.js +6 -1
  26. package/src/utils/format-error.js +89 -0
  27. package/abap/guidelines/00_index.md +0 -44
  28. /package/abap/guidelines/{05_classes.md → classes.md} +0 -0
  29. /package/abap/guidelines/{02_exceptions.md → exceptions.md} +0 -0
  30. /package/abap/guidelines/{07_json.md → json.md} +0 -0
  31. /package/abap/guidelines/{09_unit_testable_code.md → unit-testable-code.md} +0 -0
@@ -2,6 +2,166 @@
2
2
  * View command - View ABAP object definitions
3
3
  */
4
4
 
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ /**
9
+ * Find the local .clas.abap file for an object by scanning the configured
10
+ * source folder (from the nearest .abapGitAgent config file).
11
+ * Returns the file path if found, null otherwise.
12
+ */
13
+ function findLocalClassFile(objName) {
14
+ try {
15
+ // Try to read .abapGitAgent to get configured folder
16
+ let folder = null;
17
+ let dir = process.cwd();
18
+ for (let i = 0; i < 5; i++) {
19
+ const cfgPath = path.join(dir, '.abapGitAgent');
20
+ if (fs.existsSync(cfgPath)) {
21
+ try {
22
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
23
+ folder = cfg.folder;
24
+ } catch (e) { /* ignore */ }
25
+ break;
26
+ }
27
+ const parent = path.dirname(dir);
28
+ if (parent === dir) break;
29
+ dir = parent;
30
+ }
31
+
32
+ // Normalise folder to a relative path segment (strip leading/trailing slashes)
33
+ const folderSeg = folder ? folder.replace(/^\/|\/$/g, '') : null;
34
+ const lowerName = objName.toLowerCase();
35
+ const fileName = `${lowerName}.clas.abap`;
36
+
37
+ const candidates = [];
38
+ if (folderSeg) {
39
+ candidates.push(path.join(process.cwd(), folderSeg, fileName));
40
+ }
41
+ candidates.push(path.join(process.cwd(), 'src', fileName));
42
+ candidates.push(path.join(process.cwd(), 'abap', fileName));
43
+ candidates.push(path.join(process.cwd(), fileName));
44
+
45
+ for (const candidate of candidates) {
46
+ if (fs.existsSync(candidate)) return candidate;
47
+ }
48
+ } catch (e) { /* ignore */ }
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Given a fully assembled class source (array of lines, 1-indexed positions),
54
+ * return a map of { METHODNAME_UPPER: globalLineNumber } where globalLineNumber
55
+ * is the line on which `METHOD <name>.` appears.
56
+ *
57
+ * Matches lines where the first non-blank token is exactly "METHOD" (case-insensitive)
58
+ * to avoid false matches on comments or string literals.
59
+ */
60
+ function buildMethodLineMap(sourceLines) {
61
+ const map = {};
62
+ for (let i = 0; i < sourceLines.length; i++) {
63
+ const condensed = sourceLines[i].trimStart();
64
+ if (/^method\s+/i.test(condensed)) {
65
+ // Extract method name: everything between "METHOD " and the next space/period/paren
66
+ const m = condensed.match(/^method\s+([\w~]+)/i);
67
+ if (m) {
68
+ map[m[1].toUpperCase()] = i + 1; // 1-based line number
69
+ }
70
+ }
71
+ }
72
+ return map;
73
+ }
74
+
75
+ /**
76
+ * Fetch the assembled class source from ADT.
77
+ * Returns an array of source lines, or null on failure.
78
+ */
79
+ async function fetchAdtSource(objName, config) {
80
+ try {
81
+ const { AdtHttp } = require('../utils/adt-http');
82
+ const adt = new AdtHttp(config);
83
+ await adt.fetchCsrfToken();
84
+ const lower = objName.toLowerCase();
85
+ const resp = await adt.get(
86
+ `/sap/bc/adt/oo/classes/${lower}/source/main`,
87
+ { accept: 'text/plain' }
88
+ );
89
+ if (resp && resp.body) {
90
+ return resp.body.split('\n');
91
+ }
92
+ } catch (e) { /* ignore — fall through */ }
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * Compute global_start for each CM section in a sections array.
98
+ * Mutates sections in-place, adding a globalStart property.
99
+ *
100
+ * Strategy:
101
+ * 1. Try local .clas.abap file → build method line map
102
+ * 2. Fall back to ADT source fetch → build method line map
103
+ * 3. If neither works, leave globalStart = 0 (unknown)
104
+ *
105
+ * For non-CM sections (CU, CO, CP, CCDEF, CCIMP, CCAU) globalStart is also
106
+ * set from the source map for sections that have a unique recognisable first line,
107
+ * but for simplicity we set it to 0 for non-CM sections (they use section-local
108
+ * line numbers already).
109
+ */
110
+ async function computeGlobalStarts(objName, sections, config) {
111
+ // Only CM sections need global_start for breakpoints
112
+ const cmSections = sections.filter(s => {
113
+ const suffix = s.SUFFIX || s.suffix || '';
114
+ const methodName = s.METHOD_NAME || s.method_name || '';
115
+ return suffix.startsWith('CM') && methodName;
116
+ });
117
+ if (cmSections.length === 0) return;
118
+
119
+ let sourceLines = null;
120
+
121
+ // Try local file first
122
+ const localFile = findLocalClassFile(objName);
123
+ if (localFile) {
124
+ try {
125
+ sourceLines = fs.readFileSync(localFile, 'utf8').split('\n');
126
+ } catch (e) { /* ignore */ }
127
+ }
128
+
129
+ // Fall back to ADT source fetch
130
+ if (!sourceLines) {
131
+ sourceLines = await fetchAdtSource(objName, config);
132
+ }
133
+
134
+ if (!sourceLines) return;
135
+
136
+ const methodLineMap = buildMethodLineMap(sourceLines);
137
+
138
+ for (const section of cmSections) {
139
+ const methodName = (section.METHOD_NAME || section.method_name || '').toUpperCase();
140
+ if (methodLineMap[methodName] !== undefined) {
141
+ section.globalStart = methodLineMap[methodName];
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Given the lines of a CM method section, return the 0-based index of the
148
+ * first "executable" line — i.e. skip METHOD, blank lines, and pure
149
+ * declaration lines (DATA/FINAL/TYPES/CONSTANTS/CLASS-DATA).
150
+ * Returns 0 if no better line is found (falls back to METHOD statement).
151
+ */
152
+ function findFirstExecutableLine(lines) {
153
+ const declPattern = /^\s*(data|final|types|constants|class-data)[\s:]/i;
154
+ const methodPattern = /^\s*method\s+/i;
155
+ for (let i = 0; i < lines.length; i++) {
156
+ const trimmed = lines[i].trim();
157
+ if (!trimmed) continue; // blank line
158
+ if (methodPattern.test(trimmed)) continue; // METHOD statement itself
159
+ if (declPattern.test(trimmed)) continue; // declaration
160
+ return i;
161
+ }
162
+ return 0;
163
+ }
164
+
5
165
  module.exports = {
6
166
  name: 'view',
7
167
  description: 'View ABAP object definitions from ABAP system',
@@ -24,6 +184,8 @@ module.exports = {
24
184
  const typeArg = args.indexOf('--type');
25
185
  const type = typeArg !== -1 ? args[typeArg + 1].toUpperCase() : null;
26
186
  const jsonOutput = args.includes('--json');
187
+ const fullMode = args.includes('--full');
188
+ const linesMode = args.includes('--lines');
27
189
 
28
190
  console.log(`\n Viewing ${objects.length} object(s)`);
29
191
 
@@ -39,6 +201,10 @@ module.exports = {
39
201
  data.type = type;
40
202
  }
41
203
 
204
+ if (fullMode) {
205
+ data.full = true;
206
+ }
207
+
42
208
  const result = await http.post('/sap/bc/z_abapgit_agent/view', data, { csrfToken });
43
209
 
44
210
  // Handle uppercase keys from ABAP
@@ -52,6 +218,17 @@ module.exports = {
52
218
  return;
53
219
  }
54
220
 
221
+ // In full+lines mode, compute global line numbers client-side before rendering
222
+ if (fullMode && linesMode) {
223
+ for (const obj of viewObjects) {
224
+ const objName = obj.NAME || obj.name || '';
225
+ const sections = obj.SECTIONS || obj.sections || [];
226
+ if (sections.length > 0 && objName) {
227
+ await computeGlobalStarts(objName, sections, config);
228
+ }
229
+ }
230
+ }
231
+
55
232
  if (jsonOutput) {
56
233
  console.log(JSON.stringify(result, null, 2));
57
234
  } else {
@@ -86,7 +263,67 @@ module.exports = {
86
263
 
87
264
  // Display source code for classes, interfaces, CDS views, programs/source includes, and STOB
88
265
  const source = obj.SOURCE || obj.source || '';
89
- if (source && (objType === 'INTF' || objType === 'Interface' || objType === 'CLAS' || objType === 'Class' || objType === 'DDLS' || objType === 'CDS View' || objType === 'PROG' || objType === 'Program' || objType === 'STOB' || objType === 'Structured Object')) {
266
+ const sections = obj.SECTIONS || obj.sections || [];
267
+
268
+ if (sections.length > 0) {
269
+ // --full mode: render all sections.
270
+ // --full --lines adds dual line numbers per line for debugging.
271
+ console.log('');
272
+ for (const section of sections) {
273
+ const suffix = section.SUFFIX || section.suffix || '';
274
+ const methodName = section.METHOD_NAME || section.method_name || '';
275
+ const file = section.FILE || section.file || '';
276
+ const lines = section.LINES || section.lines || [];
277
+ const isCmSection = suffix.startsWith('CM') && methodName;
278
+
279
+ if (linesMode) {
280
+ // --full --lines: dual line numbers (G [N]) for debugging
281
+ const globalStart = section.globalStart || 0;
282
+
283
+ if (isCmSection) {
284
+ let bpHint;
285
+ if (globalStart) {
286
+ const execOffset = findFirstExecutableLine(lines);
287
+ const execLine = globalStart + execOffset;
288
+ bpHint = `debug set --objects ${objName}:${execLine}`;
289
+ } else {
290
+ bpHint = `debug set --objects ${objName}:<global_line>`;
291
+ }
292
+ console.log(` * ---- Method: ${methodName} (${suffix}) — breakpoint: ${bpHint} ----`);
293
+ } else if (file) {
294
+ console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (from .clas.${file}.abap) ----`);
295
+ } else if (suffix) {
296
+ console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (${suffix}) ----`);
297
+ }
298
+
299
+ let includeRelLine = 0;
300
+ for (const codeLine of lines) {
301
+ includeRelLine++;
302
+ const globalLine = globalStart ? globalStart + includeRelLine - 1 : 0;
303
+ if (isCmSection) {
304
+ const gStr = globalLine ? String(globalLine).padStart(4) : ' ';
305
+ const iStr = String(includeRelLine).padStart(3);
306
+ console.log(` ${gStr} [${iStr}] ${codeLine}`);
307
+ } else {
308
+ const lStr = globalLine ? String(globalLine).padStart(4) : String(includeRelLine).padStart(4);
309
+ console.log(` ${lStr} ${codeLine}`);
310
+ }
311
+ }
312
+ } else {
313
+ // --full (no --lines): clean source, no line numbers
314
+ if (isCmSection) {
315
+ console.log(` * ---- Method: ${methodName} (${suffix}) ----`);
316
+ } else if (file) {
317
+ console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (from .clas.${file}.abap) ----`);
318
+ } else if (suffix) {
319
+ console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (${suffix}) ----`);
320
+ }
321
+ for (const codeLine of lines) {
322
+ console.log(` ${codeLine}`);
323
+ }
324
+ }
325
+ }
326
+ } else if (source && (objType === 'INTF' || objType === 'Interface' || objType === 'CLAS' || objType === 'Class' || objType === 'DDLS' || objType === 'CDS View' || objType === 'PROG' || objType === 'Program' || objType === 'STOB' || objType === 'Structured Object')) {
90
327
  console.log('');
91
328
  // Replace escaped newlines with actual newlines and display
92
329
  const displaySource = source.replace(/\\n/g, '\n');
@@ -3,6 +3,7 @@
3
3
  */
4
4
  const https = require('https');
5
5
  const http = require('http');
6
+ const { extractBodyDetail } = require('./format-error');
6
7
  const fs = require('fs');
7
8
  const path = require('path');
8
9
  const os = require('os');
@@ -269,9 +270,13 @@ class AbapHttp {
269
270
  let body = '';
270
271
  res.on('data', chunk => body += chunk);
271
272
  res.on('end', () => {
273
+ const detail = extractBodyDetail(body);
274
+ const message = detail
275
+ ? `(HTTP ${res.statusCode}) ${detail}`
276
+ : `(HTTP ${res.statusCode}) ${res.statusMessage || 'Internal Server Error'}`;
272
277
  reject({
273
278
  statusCode: res.statusCode,
274
- message: `HTTP ${res.statusCode} error`,
279
+ message,
275
280
  body: body
276
281
  });
277
282
  });
@@ -508,10 +508,10 @@ async function getTopic(topic) {
508
508
  if (guidelinesDir) {
509
509
  // Map topic to local guideline file
510
510
  const guidelineMap = {
511
- 'abapgit': '08_abapgit.md',
512
- 'xml': '06_objects.md',
513
- 'objects': '06_objects.md',
514
- 'naming': '06_objects.md'
511
+ 'abapgit': 'abapgit.md',
512
+ 'xml': 'objects.md',
513
+ 'objects': 'objects.md',
514
+ 'naming': 'objects.md'
515
515
  };
516
516
 
517
517
  const guidelineFile = guidelineMap[topicLower];
@@ -8,6 +8,7 @@ const https = require('https');
8
8
  const http = require('http');
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
+ const { extractBodyDetail } = require('./format-error');
11
12
  const os = require('os');
12
13
  const crypto = require('crypto');
13
14
 
@@ -194,7 +195,11 @@ class AdtHttp {
194
195
  let respBody = '';
195
196
  res.on('data', chunk => respBody += chunk);
196
197
  res.on('end', () => {
197
- reject({ statusCode: res.statusCode, message: `HTTP ${res.statusCode} error`, body: respBody });
198
+ const detail = extractBodyDetail(respBody);
199
+ const message = detail
200
+ ? `(HTTP ${res.statusCode}) ${detail}`
201
+ : `(HTTP ${res.statusCode}) ${res.statusMessage || 'Internal Server Error'}`;
202
+ reject({ statusCode: res.statusCode, message, body: respBody });
198
203
  });
199
204
  return;
200
205
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Shared HTTP error formatting utilities.
3
+ *
4
+ * When ABAP returns HTTP 4xx/5xx the abap-http.js layer captures the raw
5
+ * response body in `error.body`. This module extracts a human-readable
6
+ * detail line from that body and provides a consistent display helper used
7
+ * by all command modules.
8
+ */
9
+
10
+ /**
11
+ * Extract a short, human-readable detail string from an HTTP error's response
12
+ * body. Handles the three common SAP response shapes:
13
+ * - JSON with a `message` or `error` field (our own ABAP handlers)
14
+ * - SAP ICF XML/HTML error pages (raw ICM 500 page)
15
+ * - Plain text
16
+ *
17
+ * @param {string|object} body - raw response body (string or already-parsed object)
18
+ * @returns {string|null} - detail text, or null if nothing useful found
19
+ */
20
+ function extractBodyDetail(body) {
21
+ if (!body) return null;
22
+
23
+ // Already a parsed object (e.g. CSRF error shape)
24
+ if (typeof body === 'object') {
25
+ return body.message || body.error || body.MESSAGE || body.ERROR || null;
26
+ }
27
+
28
+ // Try to parse as JSON first
29
+ try {
30
+ const parsed = JSON.parse(body);
31
+ return parsed.message || parsed.error || parsed.MESSAGE || parsed.ERROR || null;
32
+ } catch (_) {
33
+ // Not JSON — fall through
34
+ }
35
+
36
+ // SAP ICF HTML/XML error page: grab the first <p> or <title> content
37
+ const titleMatch = body.match(/<title[^>]*>([^<]+)<\/title>/i);
38
+ if (titleMatch) return titleMatch[1].trim();
39
+
40
+ const pMatch = body.match(/<p[^>]*>([^<]{10,})<\/p>/i);
41
+ if (pMatch) return pMatch[1].trim();
42
+
43
+ // Plain text — return first non-empty line (up to 200 chars)
44
+ const firstLine = body.split('\n').map(l => l.trim()).find(l => l.length > 0);
45
+ if (firstLine) return firstLine.substring(0, 200);
46
+
47
+ return null;
48
+ }
49
+
50
+ /**
51
+ * Build a display message for an HTTP error, including body detail when
52
+ * available.
53
+ *
54
+ * @param {Error|object} error - error object from abap-http.js
55
+ * @returns {string} - formatted message for console output
56
+ */
57
+ function formatHttpError(error) {
58
+ const base = error.message || String(error);
59
+ const detail = extractBodyDetail(error.body);
60
+ if (detail && detail !== base) {
61
+ return `${base}\n Detail: ${detail}`;
62
+ }
63
+ return base;
64
+ }
65
+
66
+ /**
67
+ * Print a formatted HTTP error to stderr.
68
+ * Optionally dump the full raw body when verbose mode is active.
69
+ *
70
+ * @param {Error|object} error - error from abap-http.js
71
+ * @param {object} [opts] - options
72
+ * @param {boolean} [opts.verbose=false] - dump full raw body
73
+ * @param {string} [opts.prefix='❌ Error'] - prefix for first line
74
+ */
75
+ function printHttpError(error, { verbose = false, prefix = '❌ Error' } = {}) {
76
+ const msg = formatHttpError(error);
77
+ console.error(`\n${prefix}: ${msg}`);
78
+
79
+ if (verbose && error.body) {
80
+ console.error('\n--- Raw response body ---');
81
+ const raw = typeof error.body === 'object'
82
+ ? JSON.stringify(error.body, null, 2)
83
+ : String(error.body);
84
+ console.error(raw);
85
+ console.error('--- End of response body ---');
86
+ }
87
+ }
88
+
89
+ module.exports = { formatHttpError, printHttpError, extractBodyDetail };
@@ -1,44 +0,0 @@
1
- ---
2
- layout: default
3
- title: Overview
4
- nav_order: 1
5
- parent: ABAP Coding Guidelines
6
- grand_parent: ABAP Development
7
- ---
8
-
9
- # ABAP Coding Guidelines Index
10
-
11
- This folder contains detailed ABAP coding guidelines that can be searched using the `ref` command.
12
-
13
- ## Guidelines Available
14
-
15
- | File | Topic |
16
- |------|-------|
17
- | `01_sql.md` | ABAP SQL Best Practices |
18
- | `02_exceptions.md` | Exception Handling |
19
- | `03_testing.md` | Unit Testing (including CDS) |
20
- | `04_cds.md` | CDS Views |
21
- | `05_classes.md` | ABAP Classes and Objects |
22
- | `06_objects.md` | Object Naming Conventions |
23
- | `07_json.md` | JSON Handling |
24
- | `08_abapgit.md` | abapGit XML Metadata Templates |
25
- | `09_unit_testable_code.md` | Unit Testable Code Guidelines (Dependency Injection) |
26
-
27
- ## Usage
28
-
29
- These guidelines are automatically searched by the `ref` command:
30
-
31
- ```bash
32
- # Search across all guidelines
33
- abapgit-agent ref "CORRESPONDING"
34
-
35
- # List all topics
36
- abapgit-agent ref --list-topics
37
- ```
38
-
39
- ## Adding Custom Guidelines
40
-
41
- To add your own guidelines:
42
- 1. Create a new `.md` file in this folder
43
- 2. Follow the naming convention: `XX_name.md`
44
- 3. Export to reference folder: `abapgit-agent ref export`
File without changes
File without changes