abapgit-agent 1.11.0 → 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 (34) hide show
  1. package/.abapGitAgent.example +1 -0
  2. package/abap/.github/copilot-instructions.md +9 -9
  3. package/abap/CLAUDE.md +48 -539
  4. package/abap/guidelines/{08_abapgit.md → abapgit.md} +1 -1
  5. package/abap/guidelines/branch-workflow.md +137 -0
  6. package/abap/guidelines/cds-testing.md +25 -0
  7. package/abap/guidelines/{04_cds.md → cds.md} +4 -4
  8. package/abap/guidelines/{10_common_errors.md → common-errors.md} +3 -3
  9. package/abap/guidelines/debug-dump.md +33 -0
  10. package/abap/guidelines/debug-session.md +280 -0
  11. package/abap/guidelines/index.md +50 -0
  12. package/abap/guidelines/object-creation.md +51 -0
  13. package/abap/guidelines/{06_objects.md → objects.md} +2 -2
  14. package/abap/guidelines/{01_sql.md → sql.md} +2 -2
  15. package/abap/guidelines/{03_testing.md → testing.md} +3 -3
  16. package/abap/guidelines/workflow-detailed.md +255 -0
  17. package/package.json +1 -1
  18. package/src/commands/debug.js +54 -20
  19. package/src/commands/inspect.js +5 -3
  20. package/src/commands/pull.js +4 -1
  21. package/src/commands/transport.js +3 -1
  22. package/src/commands/unit.js +10 -10
  23. package/src/commands/view.js +238 -1
  24. package/src/config.js +4 -2
  25. package/src/utils/abap-http.js +11 -6
  26. package/src/utils/abap-reference.js +4 -4
  27. package/src/utils/adt-http.js +13 -8
  28. package/src/utils/format-error.js +89 -0
  29. package/src/utils/version-check.js +4 -3
  30. package/abap/guidelines/00_index.md +0 -44
  31. /package/abap/guidelines/{05_classes.md → classes.md} +0 -0
  32. /package/abap/guidelines/{02_exceptions.md → exceptions.md} +0 -0
  33. /package/abap/guidelines/{07_json.md → json.md} +0 -0
  34. /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');
package/src/config.js CHANGED
@@ -34,7 +34,8 @@ function loadConfig() {
34
34
  language: process.env.ABAP_LANGUAGE || 'EN',
35
35
  gitUsername: process.env.GIT_USERNAME,
36
36
  gitPassword: process.env.GIT_PASSWORD,
37
- transport: process.env.ABAP_TRANSPORT
37
+ transport: process.env.ABAP_TRANSPORT,
38
+ protocol: process.env.ABAP_PROTOCOL || 'https'
38
39
  };
39
40
  }
40
41
 
@@ -51,7 +52,8 @@ function getAbapConfig() {
51
52
  password: cfg.password,
52
53
  language: cfg.language || 'EN',
53
54
  gitUsername: cfg.gitUsername || process.env.GIT_USERNAME,
54
- gitPassword: cfg.gitPassword || process.env.GIT_PASSWORD
55
+ gitPassword: cfg.gitPassword || process.env.GIT_PASSWORD,
56
+ protocol: cfg.protocol || process.env.ABAP_PROTOCOL || 'https'
55
57
  };
56
58
  }
57
59
 
@@ -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');
@@ -129,7 +130,7 @@ class AbapHttp {
129
130
  * @returns {Promise<string>} CSRF token
130
131
  */
131
132
  async fetchCsrfToken() {
132
- const url = new URL(`/sap/bc/z_abapgit_agent/health`, `https://${this.config.host}:${this.config.sapport}`);
133
+ const url = new URL(`/sap/bc/z_abapgit_agent/health`, `${this.config.protocol || 'https'}://${this.config.host}:${this.config.sapport}`);
133
134
 
134
135
  return new Promise((resolve, reject) => {
135
136
  const options = {
@@ -144,10 +145,10 @@ class AbapHttp {
144
145
  'X-CSRF-Token': 'fetch',
145
146
  'Content-Type': 'application/json'
146
147
  },
147
- agent: new https.Agent({ rejectUnauthorized: false })
148
+ agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
148
149
  };
149
150
 
150
- const req = https.request(options, (res) => {
151
+ const req = (this.config.protocol === 'http' ? http : https).request(options, (res) => {
151
152
  const csrfToken = res.headers['x-csrf-token'];
152
153
 
153
154
  // Save cookies from response
@@ -222,7 +223,7 @@ class AbapHttp {
222
223
  */
223
224
  async _makeRequest(method, urlPath, data = null, options = {}) {
224
225
  return new Promise((resolve, reject) => {
225
- const url = new URL(urlPath, `https://${this.config.host}:${this.config.sapport}`);
226
+ const url = new URL(urlPath, `${this.config.protocol || 'https'}://${this.config.host}:${this.config.sapport}`);
226
227
 
227
228
  const headers = {
228
229
  'Content-Type': 'application/json',
@@ -250,7 +251,7 @@ class AbapHttp {
250
251
  path: url.pathname + url.search,
251
252
  method,
252
253
  headers,
253
- agent: new https.Agent({ rejectUnauthorized: false })
254
+ agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
254
255
  };
255
256
 
256
257
  const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (res) => {
@@ -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
 
@@ -75,7 +76,7 @@ class AdtHttp {
75
76
  */
76
77
  async fetchCsrfToken() {
77
78
  return new Promise((resolve, reject) => {
78
- const url = new URL('/sap/bc/adt/discovery', `https://${this.config.host}:${this.config.sapport}`);
79
+ const url = new URL('/sap/bc/adt/discovery', `${this.config.protocol || 'https'}://${this.config.host}:${this.config.sapport}`);
79
80
  const options = {
80
81
  hostname: url.hostname,
81
82
  port: url.port,
@@ -88,10 +89,10 @@ class AdtHttp {
88
89
  'X-CSRF-Token': 'fetch',
89
90
  'Accept': 'application/atomsvc+xml'
90
91
  },
91
- agent: new https.Agent({ rejectUnauthorized: false })
92
+ agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
92
93
  };
93
94
 
94
- const req = https.request(options, (res) => {
95
+ const req = (this.config.protocol === 'http' ? http : https).request(options, (res) => {
95
96
  const csrfToken = res.headers['x-csrf-token'];
96
97
  const setCookie = res.headers['set-cookie'];
97
98
  if (setCookie) {
@@ -140,7 +141,7 @@ class AdtHttp {
140
141
 
141
142
  async _makeRequest(method, urlPath, body = null, options = {}) {
142
143
  return new Promise((resolve, reject) => {
143
- const url = new URL(urlPath, `https://${this.config.host}:${this.config.sapport}`);
144
+ const url = new URL(urlPath, `${this.config.protocol || 'https'}://${this.config.host}:${this.config.sapport}`);
144
145
 
145
146
  const headers = {
146
147
  'Content-Type': options.contentType || 'application/atom+xml',
@@ -169,7 +170,7 @@ class AdtHttp {
169
170
  path: url.pathname + url.search,
170
171
  method,
171
172
  headers,
172
- agent: new https.Agent({ rejectUnauthorized: false })
173
+ agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
173
174
  };
174
175
 
175
176
  const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (res) => {
@@ -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
  }
@@ -272,7 +277,7 @@ class AdtHttp {
272
277
  */
273
278
  async postFire(urlPath, body = null, options = {}) {
274
279
  return new Promise((resolve, reject) => {
275
- const url = new URL(urlPath, `https://${this.config.host}:${this.config.sapport}`);
280
+ const url = new URL(urlPath, `${this.config.protocol || 'https'}://${this.config.host}:${this.config.sapport}`);
276
281
 
277
282
  const headers = {
278
283
  'Content-Type': options.contentType || 'application/atom+xml',
@@ -301,7 +306,7 @@ class AdtHttp {
301
306
  path: url.pathname + url.search,
302
307
  method: 'POST',
303
308
  headers,
304
- agent: new https.Agent({ rejectUnauthorized: false })
309
+ agent: this.config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
305
310
  };
306
311
 
307
312
  const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (_res) => {
@@ -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 };
@@ -5,6 +5,7 @@ const pathModule = require('path');
5
5
  const fs = require('fs');
6
6
  const os = require('os');
7
7
  const https = require('https');
8
+ const http = require('http');
8
9
 
9
10
  /**
10
11
  * Get cache directory for abapgit-agent
@@ -49,7 +50,7 @@ async function checkCompatibility(config) {
49
50
  const cliVersion = getCliVersion();
50
51
 
51
52
  try {
52
- const url = new URL(`/sap/bc/z_abapgit_agent/health`, `https://${config.host}:${config.sapport}`);
53
+ const url = new URL(`/sap/bc/z_abapgit_agent/health`, `${config.protocol || 'https'}://${config.host}:${config.sapport}`);
53
54
 
54
55
  return new Promise((resolve) => {
55
56
  const options = {
@@ -63,10 +64,10 @@ async function checkCompatibility(config) {
63
64
  'sap-language': config.language || 'EN',
64
65
  'Content-Type': 'application/json'
65
66
  },
66
- agent: new https.Agent({ rejectUnauthorized: false })
67
+ agent: config.protocol === 'http' ? undefined : new https.Agent({ rejectUnauthorized: false })
67
68
  };
68
69
 
69
- const req = https.request(options, (res) => {
70
+ const req = (config.protocol === 'http' ? http : https).request(options, (res) => {
70
71
  let body = '';
71
72
  res.on('data', chunk => body += chunk);
72
73
  res.on('end', () => {
@@ -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