abapgit-agent 1.16.1 → 1.16.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.
package/abap/CLAUDE.md CHANGED
@@ -632,6 +632,7 @@ Checked into the repository — applies to all developers. **Read this file at t
632
632
  |---------|--------|---------|--------|
633
633
  | `safeguards.requireFilesForPull` | `true`/`false` | `false` | Requires `--files` on every pull |
634
634
  | `safeguards.disablePull` | `true`/`false` | `false` | Disables pull entirely (CI/CD-only projects) |
635
+ | `safeguards.disableImport` | `true`/`false` | `false` | Disables import entirely (one-time or managed operation) |
635
636
  | `conflictDetection.mode` | `"abort"`/`"ignore"` | `"abort"` | Whether to abort pull on conflict |
636
637
  | `transports.hook.path` | string | `null` | Path to JS module that auto-selects a transport for pull |
637
638
  | `transports.hook.description` | string | `null` | Optional label shown when the hook runs |
@@ -724,6 +725,10 @@ abapgit-agent pull --files src/<name>.clas.abap --sync-xml
724
725
  1. ✗ Do not run `abapgit-agent run` at all
725
726
  2. ✓ Inform the user that run is disabled for this project
726
727
 
728
+ **When `safeguards.disableImport = true`:**
729
+ 1. ✗ Do not run `abapgit-agent import` at all
730
+ 2. ✓ Inform the user that import is disabled for this project
731
+
727
732
  **When `safeguards.disableProbeClasses = true`:**
728
733
  1. ✗ Do not create probe classes in the current project
729
734
  2. ✓ If `scratchWorkspace` is configured → create probe class there (see Rule 10)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.16.1",
3
+ "version": "1.16.2",
4
4
  "description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
5
5
  "files": [
6
6
  "bin/",
@@ -18,7 +18,7 @@ module.exports = {
18
18
 
19
19
  async execute(args, context) {
20
20
  try {
21
- const { loadConfig, gitUtils, AbapHttp } = context;
21
+ const { loadConfig, gitUtils, AbapHttp, getSafeguards } = context;
22
22
 
23
23
  // Show help if requested
24
24
  const helpIndex = args.findIndex(a => a === '--help' || a === '-h');
@@ -48,6 +48,18 @@ Examples:
48
48
  return;
49
49
  }
50
50
 
51
+ // Get parameters from config
52
+ const safeguards = getSafeguards();
53
+ if (safeguards.disableImport) {
54
+ console.error('❌ Error: import command is disabled for this project\n');
55
+ if (safeguards.reason) {
56
+ console.error(`Reason: ${safeguards.reason}\n`);
57
+ }
58
+ console.error('The import command has been disabled in .abapgit-agent.json');
59
+ console.error('Please contact the project maintainer to enable it.');
60
+ process.exit(1);
61
+ }
62
+
51
63
  // Get parameters from config
52
64
  const config = loadConfig();
53
65
  const repoUrl = gitUtils.getRemoteUrl();
@@ -71,6 +71,7 @@ Examples:
71
71
  // Group class files together (main + locals)
72
72
  const classFilesMap = new Map(); // className -> { main, locals_def, locals_imp }
73
73
  const fugrGroupMap = new Map(); // groupName -> { dir, fmFiles: Map<fmName, source> }
74
+ const progIncludeMap = new Map(); // includeName -> { filePath, source }
74
75
  const objects = [];
75
76
 
76
77
  for (const file of syntaxFiles) {
@@ -165,7 +166,7 @@ Examples:
165
166
  source: source
166
167
  };
167
168
 
168
- // Read FIXPT from XML metadata for INTF and PROG
169
+ // Read FIXPT and SUBC from XML metadata for INTF and PROG
169
170
  if (objType === 'INTF' || objType === 'PROG') {
170
171
  const dir = pathModule.dirname(filePath);
171
172
  let xmlFile;
@@ -176,14 +177,22 @@ Examples:
176
177
  }
177
178
  if (xmlFile && fs.existsSync(xmlFile)) {
178
179
  const xmlContent = fs.readFileSync(xmlFile, 'utf8');
179
- // Simple regex to extract FIXPT value
180
180
  const fixptMatch = xmlContent.match(/<FIXPT>([^<]+)<\/FIXPT>/);
181
181
  if (fixptMatch && fixptMatch[1] === 'X') {
182
182
  obj.fixpt = 'X';
183
183
  } else {
184
- // No FIXPT tag means FIXPT=false (blank)
185
184
  obj.fixpt = '';
186
185
  }
186
+ if (objType === 'PROG') {
187
+ const subcMatch = xmlContent.match(/<SUBC>([^<]+)<\/SUBC>/);
188
+ obj.subc = subcMatch ? subcMatch[1] : '';
189
+
190
+ // INCLUDE programs: stash for parent auto-detection instead of pushing directly
191
+ if (obj.subc === 'I') {
192
+ progIncludeMap.set(objName, { filePath, source });
193
+ continue; // skip objects.push — handled after auto-detection below
194
+ }
195
+ }
187
196
  }
188
197
  }
189
198
 
@@ -352,12 +361,102 @@ Examples:
352
361
  }
353
362
  }
354
363
 
364
+ // Helper: read SUBC from a .prog.xml file
365
+ function readProgSubc(xmlPath) {
366
+ if (!fs.existsSync(xmlPath)) return '';
367
+ const xml = fs.readFileSync(xmlPath, 'utf8');
368
+ const m = xml.match(/<SUBC>([^<]+)<\/SUBC>/);
369
+ return m ? m[1] : '';
370
+ }
371
+
372
+ // Helper: read FIXPT from a .prog.xml file
373
+ function readProgFixpt(xmlPath) {
374
+ if (!fs.existsSync(xmlPath)) return '';
375
+ const xml = fs.readFileSync(xmlPath, 'utf8');
376
+ const m = xml.match(/<FIXPT>([^<]+)<\/FIXPT>/);
377
+ return (m && m[1] === 'X') ? 'X' : '';
378
+ }
379
+
380
+ // Auto-detect parent program for INCLUDE files
381
+ // Scan the same directory for .prog.abap files that contain INCLUDE <name>.
382
+ // Assemble the parent source with the INCLUDE source substituted in-place.
383
+ for (const [includeName, includeData] of progIncludeMap) {
384
+ const includeDir = pathModule.dirname(includeData.filePath);
385
+ let parentFound = false;
386
+
387
+ let dirEntries;
388
+ try {
389
+ dirEntries = fs.readdirSync(includeDir);
390
+ } catch (e) {
391
+ dirEntries = [];
392
+ }
393
+
394
+ for (const entry of dirEntries) {
395
+ if (!entry.toLowerCase().endsWith('.prog.abap')) continue;
396
+ const candidatePath = pathModule.join(includeDir, entry);
397
+ if (candidatePath === includeData.filePath) continue; // skip itself
398
+
399
+ // Only consider executable/non-include programs as parents
400
+ const candidateName = entry.split('.')[0].toUpperCase();
401
+ const candidateXml = pathModule.join(includeDir, `${candidateName.toLowerCase()}.prog.xml`);
402
+ const candidateSubc = readProgSubc(candidateXml);
403
+ if (candidateSubc === 'I') continue; // another INCLUDE — skip
404
+
405
+ const parentSource = fs.readFileSync(candidatePath, 'utf8');
406
+ const parentLines = parentSource.split('\n');
407
+
408
+ // Find `INCLUDE <includeName>.` line (case-insensitive)
409
+ const includeRegex = new RegExp(`^\\s*INCLUDE\\s+${includeName}\\s*\\.`, 'i');
410
+ const includeLineIdx = parentLines.findIndex(l => includeRegex.test(l));
411
+ if (includeLineIdx === -1) continue;
412
+
413
+ // Found parent — assemble: replace INCLUDE statement with include's source lines
414
+ const includeLines = includeData.source.split('\n');
415
+ const assembled = [
416
+ ...parentLines.slice(0, includeLineIdx),
417
+ ...includeLines,
418
+ ...parentLines.slice(includeLineIdx + 1)
419
+ ];
420
+ const assembledSource = assembled.join('\n');
421
+
422
+ if (!jsonOutput) {
423
+ console.log(` Auto-detected parent: ${entry} (contains INCLUDE ${includeName}.)`);
424
+ }
425
+
426
+ objects.push({
427
+ type: 'PROG',
428
+ name: candidateName,
429
+ source: assembledSource,
430
+ subc: candidateSubc || '1',
431
+ fixpt: readProgFixpt(candidateXml),
432
+ // offset into assembled source where include lines start (1-based)
433
+ include_offset: includeLineIdx + 1,
434
+ include_line_count: includeLines.length,
435
+ // carry include name for error line mapping
436
+ include_name: includeName
437
+ });
438
+
439
+ parentFound = true;
440
+ break; // use first matching parent
441
+ }
442
+
443
+ if (!parentFound) {
444
+ if (!jsonOutput) {
445
+ console.error(` Warning: No parent program found for INCLUDE ${includeName} — cannot syntax-check standalone`);
446
+ }
447
+ }
448
+ }
449
+
355
450
  if (objects.length === 0) {
356
451
  console.error(' No valid files to check');
357
452
  process.exit(1);
358
453
  }
359
454
  const data = {
360
- objects: objects,
455
+ objects: objects.map(o => {
456
+ // Strip client-side INCLUDE tracking fields before sending to ABAP
457
+ const { include_offset, include_line_count, include_name, ...abapObj } = o; // eslint-disable-line no-unused-vars
458
+ return abapObj;
459
+ }),
361
460
  uccheck: cloudMode ? '5' : 'X'
362
461
  };
363
462
 
@@ -387,26 +486,56 @@ Examples:
387
486
  const fugrFmLabel = (objType === 'FUGR' && sentObj.fugr_include_name)
388
487
  ? ` (${sentObj.fugr_include_name})` : '';
389
488
 
489
+ // For INCLUDE-backed checks: determine display label and line remapping
490
+ const includeLabel = sentObj.include_name
491
+ ? ` (checking via parent ${objName})` : '';
492
+
390
493
  if (objSuccess) {
391
- console.log(`✅ ${objType} ${objName}${fugrFmLabel} - Syntax check passed`);
494
+ if (sentObj.include_name) {
495
+ console.log(`✅ PROG ${sentObj.include_name} - Syntax check passed${includeLabel}`);
496
+ } else {
497
+ console.log(`✅ ${objType} ${objName}${fugrFmLabel} - Syntax check passed`);
498
+ }
392
499
  if (warnings.length > 0) {
393
500
  console.log(` (${warnings.length} warning(s))`);
394
501
  }
395
502
  } else {
396
- console.log(`❌ ${objType} ${objName}${fugrFmLabel} - Syntax check failed (${errorCount} error(s))`);
503
+ if (sentObj.include_name) {
504
+ console.log(`❌ PROG ${sentObj.include_name} - Syntax check failed (${errorCount} error(s))${includeLabel}`);
505
+ } else {
506
+ console.log(`❌ ${objType} ${objName}${fugrFmLabel} - Syntax check failed (${errorCount} error(s))`);
507
+ }
397
508
  console.log('');
398
509
  console.log('Errors:');
399
510
  console.log('─'.repeat(60));
400
511
 
401
512
  for (const err of errors) {
402
- const line = err.LINE || err.line || '?';
513
+ let line = err.LINE || err.line || '?';
403
514
  const column = err.COLUMN || err.column || '';
404
515
  const text = err.TEXT || err.text || 'Unknown error';
405
516
  const methodName = err.METHOD_NAME || err.method_name || '';
406
517
  const include = err.INCLUDE || err.include || '';
407
518
 
519
+ // Remap line numbers for INCLUDE-backed checks
520
+ let remappedFile = null;
521
+ if (sentObj.include_name && typeof line === 'number') {
522
+ const offset = sentObj.include_offset || 0;
523
+ const count = sentObj.include_line_count || 0;
524
+ if (line >= offset && line < offset + count) {
525
+ // Error is inside the INCLUDE — remap to include-relative line
526
+ line = line - offset + 1;
527
+ remappedFile = `${sentObj.include_name.toLowerCase()}.prog.abap`;
528
+ } else {
529
+ // Error is in the parent program context (e.g. header line)
530
+ remappedFile = `${objName.toLowerCase()}.prog.abap (parent)`;
531
+ }
532
+ }
533
+
408
534
  // Display which file/include the error is in
409
- if (include) {
535
+ if (remappedFile) {
536
+ // INCLUDE-backed check: show the actual source file the error maps to
537
+ console.log(` In: ${remappedFile}`);
538
+ } else if (include) {
410
539
  // For FUGR: include = lowercase FM name → display as '<group>.fugr.<fm_name>.abap'
411
540
  if (objType === 'FUGR') {
412
541
  const fugrFile = `${objName.toLowerCase()}.fugr.${include}.abap`;
package/src/config.js CHANGED
@@ -123,6 +123,7 @@ function getSafeguards() {
123
123
  requireFilesForPull: projectConfig.safeguards.requireFilesForPull === true,
124
124
  disablePull: projectConfig.safeguards.disablePull === true,
125
125
  disableRun: projectConfig.safeguards.disableRun === true,
126
+ disableImport: projectConfig.safeguards.disableImport === true,
126
127
  disableProbeClasses: projectConfig.safeguards.disableProbeClasses === true,
127
128
  reason: projectConfig.safeguards.reason || null
128
129
  };
@@ -133,6 +134,7 @@ function getSafeguards() {
133
134
  requireFilesForPull: false,
134
135
  disablePull: false,
135
136
  disableRun: false,
137
+ disableImport: false,
136
138
  disableProbeClasses: false,
137
139
  reason: null
138
140
  };