abapgit-agent 1.12.1 → 1.13.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.
@@ -17,9 +17,9 @@ const stat = promisify(fs.stat);
17
17
  const TOPIC_MAP = {
18
18
  'internal-tables': '01_Internal_Tables.md',
19
19
  'structures': '02_Structures.md',
20
- 'sql': '03_ABAP_SQL.md',
20
+ 'sql-cheatsheet': '03_ABAP_SQL.md',
21
21
  'oop': '04_ABAP_Object_Orientation.md',
22
- 'objects': '04_ABAP_Object_Orientation.md',
22
+ 'oop-cheatsheet': '04_ABAP_Object_Orientation.md',
23
23
  'constructors': '05_Constructor_Expressions.md',
24
24
  'constructor': '05_Constructor_Expressions.md',
25
25
  'dynamic': '06_Dynamic_Programming.md',
@@ -33,24 +33,23 @@ const TOPIC_MAP = {
33
33
  'flow': '13_Program_Flow_Logic.md',
34
34
  'unit-tests': '14_ABAP_Unit_Tests.md',
35
35
  'unit': '14_ABAP_Unit_Tests.md',
36
- 'testing': '14_ABAP_Unit_Tests.md',
37
- 'cds': '15_CDS_View_Entities.md',
36
+ 'cds-cheatsheet': '15_CDS_View_Entities.md',
38
37
  'datatypes': '16_Data_Types_and_Objects.md',
39
38
  'luw': '17_SAP_LUW.md',
40
39
  'dynpro': '18_Dynpro.md',
41
40
  'cloud': '19_ABAP_for_Cloud_Development.md',
42
41
  'selection-screens': '20_Selection_Screens_Lists.md',
43
42
  'json-xml': '21_XML_JSON.md',
44
- 'json': '21_XML_JSON.md',
45
- 'xml': '21_XML_JSON.md',
43
+ 'json-cheatsheet': '21_XML_JSON.md',
44
+ 'xml-cheatsheet': '21_XML_JSON.md',
46
45
  'released-classes': '22_Released_ABAP_Classes.md',
47
46
  'datetime': '23_Date_and_Time.md',
48
47
  'functions': '24_Builtin_Functions.md',
49
48
  'auth': '25_Authorization_Checks.md',
50
49
  'authorization': '25_Authorization_Checks.md',
51
50
  'dictionary': '26_ABAP_Dictionary.md',
52
- 'exceptions': '27_Exceptions.md',
53
- 'exception': '27_Exceptions.md',
51
+ 'exceptions-cheatsheet': '27_Exceptions.md',
52
+ 'exception-cheatsheet': '27_Exceptions.md',
54
53
  'regex': '28_Regular_Expressions.md',
55
54
  'numeric': '29_Numeric_Operations.md',
56
55
  'ai': '30_Generative_AI.md',
@@ -380,9 +379,10 @@ async function searchPattern(pattern) {
380
379
  const refFolder = detectReferenceFolder();
381
380
  const repos = await getReferenceRepositories();
382
381
  const guidelinesFolder = detectGuidelinesFolder();
382
+ const builtInPath = getBuiltInGuidelinesPath();
383
383
 
384
- // If neither reference folder nor guidelines exist, return error
385
- if (!refFolder && !guidelinesFolder) {
384
+ // If no sources at all, return error
385
+ if (!refFolder && !guidelinesFolder && !builtInPath) {
386
386
  return {
387
387
  error: 'Reference folder not found',
388
388
  hint: 'Configure referenceFolder in .abapGitAgent, clone to ~/abap-reference, or create abap/guidelines/ folder'
@@ -398,6 +398,34 @@ async function searchPattern(pattern) {
398
398
  matches: []
399
399
  };
400
400
 
401
+ /**
402
+ * Helper: search a list of guideline file objects and push results
403
+ */
404
+ function searchGuidelineFiles(guidelineFiles, repoLabel) {
405
+ for (const file of guidelineFiles) {
406
+ if (file.content.toLowerCase().includes(pattern.toLowerCase())) {
407
+ results.files.push({ repo: repoLabel, file: file.relativePath });
408
+
409
+ const lines = file.content.split('\n');
410
+ let matchCount = 0;
411
+ for (let i = 0; i < lines.length; i++) {
412
+ if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
413
+ const start = Math.max(0, i - 1);
414
+ const end = Math.min(lines.length, i + 2);
415
+ results.matches.push({
416
+ repo: repoLabel,
417
+ file: file.relativePath,
418
+ line: i + 1,
419
+ context: lines.slice(start, end).join('\n')
420
+ });
421
+ matchCount++;
422
+ if (matchCount >= 3) break;
423
+ }
424
+ }
425
+ }
426
+ }
427
+ }
428
+
401
429
  try {
402
430
  // Search reference repositories if available
403
431
  if (repos.length > 0) {
@@ -447,44 +475,22 @@ async function searchPattern(pattern) {
447
475
  }
448
476
  }
449
477
 
450
- // Search local guidelines folder if available
478
+ // Tier 1: search local guidelines folder
451
479
  if (guidelinesFolder) {
452
- const guidelineFiles = await getGuidelineFiles();
453
-
454
- for (const file of guidelineFiles) {
455
- if (file.content.toLowerCase().includes(pattern.toLowerCase())) {
456
- results.files.push({
457
- repo: 'guidelines',
458
- file: file.relativePath
459
- });
460
-
461
- // Find matching lines with context
462
- const lines = file.content.split('\n');
463
- let matchCount = 0;
464
-
465
- for (let i = 0; i < lines.length; i++) {
466
- if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
467
- const start = Math.max(0, i - 1);
468
- const end = Math.min(lines.length, i + 2);
469
- const context = lines.slice(start, end).join('\n');
470
-
471
- results.matches.push({
472
- repo: 'guidelines',
473
- file: file.relativePath,
474
- line: i + 1,
475
- context
476
- });
477
-
478
- matchCount++;
480
+ const localFiles = await getGuidelineFilesFromPath(guidelinesFolder, 'guidelines');
481
+ searchGuidelineFiles(localFiles, 'guidelines');
482
+ }
479
483
 
480
- // Limit matches per file to avoid overwhelming output
481
- if (matchCount >= 3) {
482
- break;
483
- }
484
- }
485
- }
486
- }
487
- }
484
+ // Tier 2: search built-in guidelines for files not shadowed by local ones
485
+ if (builtInPath) {
486
+ const localFileNames = guidelinesFolder
487
+ ? new Set(fs.readdirSync(guidelinesFolder).filter(f => f.endsWith('.md')))
488
+ : new Set();
489
+ const builtInFiles = await getGuidelineFilesFromPath(builtInPath, 'guidelines');
490
+ const filesToSearch = builtInFiles.filter(f => !localFileNames.has(f.name));
491
+ // Override relativePath to signal built-in source
492
+ filesToSearch.forEach(f => { f.relativePath = path.join('guidelines', f.name); });
493
+ searchGuidelineFiles(filesToSearch, '[built-in]');
488
494
  }
489
495
 
490
496
  return results;
@@ -523,8 +529,8 @@ async function getTopic(topic) {
523
529
  return {
524
530
  topic,
525
531
  file: guidelineFile,
526
- content: content.slice(0, 5000),
527
- truncated: content.length > 5000,
532
+ content: content.slice(0, 15000),
533
+ truncated: content.length > 15000,
528
534
  totalLength: content.length,
529
535
  source: 'guidelines'
530
536
  };
@@ -535,46 +541,113 @@ async function getTopic(topic) {
535
541
  }
536
542
  }
537
543
 
538
- // Fall back to external reference folder
544
+ // Fall back to cheat sheets TOPIC_MAP
539
545
  const cheatSheetsDir = getCheatSheetsDir();
540
546
 
541
- if (!cheatSheetsDir) {
542
- return {
543
- error: 'Reference folder not found',
544
- hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
545
- };
546
- }
547
-
548
547
  const fileName = TOPIC_MAP[topicLower];
549
- if (!fileName) {
550
- return {
551
- error: `Unknown topic: ${topic}`,
552
- availableTopics: Object.keys(TOPIC_MAP).filter((v, i, a) => a.indexOf(v) === i).slice(0, 20)
553
- };
548
+ if (fileName && cheatSheetsDir) {
549
+ const filePath = path.join(cheatSheetsDir, fileName);
550
+ if (fs.existsSync(filePath)) {
551
+ try {
552
+ const content = await readFile(filePath, 'utf8');
553
+ return {
554
+ topic,
555
+ file: fileName,
556
+ content: content.slice(0, 5000),
557
+ truncated: content.length > 5000,
558
+ totalLength: content.length,
559
+ source: 'cheat-sheets'
560
+ };
561
+ } catch (error) {
562
+ // fall through
563
+ }
564
+ }
554
565
  }
555
566
 
556
- const filePath = path.join(cheatSheetsDir, fileName);
567
+ // TOPIC_MAP matched but cheat sheets not available — give a helpful setup hint
568
+ if (fileName && !cheatSheetsDir) {
569
+ // Distinguish between "not configured" and "configured but folder missing"
570
+ let configuredRefFolder = null;
571
+ try {
572
+ const configPath = path.join(process.cwd(), '.abapGitAgent');
573
+ if (fs.existsSync(configPath)) {
574
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
575
+ if (config.referenceFolder) configuredRefFolder = config.referenceFolder;
576
+ }
577
+ } catch (e) { /* ignore */ }
578
+
579
+ if (configuredRefFolder) {
580
+ return {
581
+ error: `Topic '${topic}' requires SAP ABAP cheat sheets (folder not found: ${configuredRefFolder})`,
582
+ hint: [
583
+ 'The referenceFolder is configured but does not exist on disk.',
584
+ `Clone the cheat sheets into it: abapgit-agent ref --clone SAP-samples/abap-cheat-sheets`,
585
+ '',
586
+ 'Bundled topics (no setup needed): run abapgit-agent ref --list-topics'
587
+ ].join('\n')
588
+ };
589
+ }
557
590
 
558
- if (!fs.existsSync(filePath)) {
559
591
  return {
560
- error: `File not found: ${fileName}`
592
+ error: `Topic '${topic}' requires SAP ABAP cheat sheets (not configured)`,
593
+ hint: [
594
+ 'To access SAP cheat sheet topics:',
595
+ ' 1. Add to .abapGitAgent: { "referenceFolder": "/path/to/abap-reference" }',
596
+ ' 2. Clone: abapgit-agent ref --clone SAP-samples/abap-cheat-sheets',
597
+ '',
598
+ 'Bundled topics (no setup needed): run abapgit-agent ref --list-topics'
599
+ ].join('\n')
561
600
  };
562
601
  }
563
602
 
564
- try {
565
- const content = await readFile(filePath, 'utf8');
566
- return {
567
- topic,
568
- file: fileName,
569
- content: content.slice(0, 5000), // First 5000 chars
570
- truncated: content.length > 5000,
571
- totalLength: content.length
572
- };
573
- } catch (error) {
574
- return {
575
- error: `Failed to read topic: ${error.message}`
576
- };
603
+ // Fall back to bundled guidelines by filename stem
604
+ // e.g. 'debug-session' 'debug-session.md', 'debug' → 'debug-session.md' (if unambiguous)
605
+ const builtInPath = getBuiltInGuidelinesPath();
606
+ if (builtInPath) {
607
+ try {
608
+ const builtInFiles = fs.readdirSync(builtInPath).filter(f => f.endsWith('.md'));
609
+
610
+ // Exact stem match first
611
+ const exactMatch = builtInFiles.find(f => f.replace(/\.md$/, '') === topicLower);
612
+ if (exactMatch) {
613
+ const content = await readFile(path.join(builtInPath, exactMatch), 'utf8');
614
+ return {
615
+ topic,
616
+ file: exactMatch,
617
+ content: content.slice(0, 15000),
618
+ truncated: content.length > 15000,
619
+ totalLength: content.length,
620
+ source: 'built-in guidelines'
621
+ };
622
+ }
623
+
624
+ // Partial stem match (unambiguous only)
625
+ const partialMatches = builtInFiles.filter(f => f.replace(/\.md$/, '').includes(topicLower));
626
+ if (partialMatches.length === 1) {
627
+ const content = await readFile(path.join(builtInPath, partialMatches[0]), 'utf8');
628
+ return {
629
+ topic,
630
+ file: partialMatches[0],
631
+ content: content.slice(0, 15000),
632
+ truncated: content.length > 15000,
633
+ totalLength: content.length,
634
+ source: 'built-in guidelines'
635
+ };
636
+ } else if (partialMatches.length > 1) {
637
+ return {
638
+ error: `Ambiguous topic: '${topic}' matches multiple guideline files`,
639
+ hint: `Be more specific. Matches: ${partialMatches.map(f => f.replace(/\.md$/, '')).join(', ')}`
640
+ };
641
+ }
642
+ } catch (error) {
643
+ // fall through to final error
644
+ }
577
645
  }
646
+
647
+ return {
648
+ error: `Unknown topic: ${topic}`,
649
+ hint: 'For project guidelines, use the filename stem (e.g. --topic debug-session, --topic workflow-detailed)\nRun: abapgit-agent ref --list-topics'
650
+ };
578
651
  }
579
652
 
580
653
  /**
@@ -584,27 +657,50 @@ async function getTopic(topic) {
584
657
  async function listTopics() {
585
658
  const cheatSheetsDir = getCheatSheetsDir();
586
659
 
587
- if (!cheatSheetsDir) {
588
- return {
589
- error: 'Reference folder not found',
590
- hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
591
- };
592
- }
593
-
594
- // Build topic list from files that exist
660
+ // Build topic list from cheat sheet files that exist
595
661
  const topics = [];
596
- const seenFiles = new Set();
597
662
 
598
- for (const [topic, file] of Object.entries(TOPIC_MAP)) {
599
- if (!seenFiles.has(file) && fs.existsSync(path.join(cheatSheetsDir, file))) {
600
- topics.push({ topic, file });
601
- seenFiles.add(file);
663
+ if (cheatSheetsDir) {
664
+ const seenFiles = new Set();
665
+ for (const [topic, file] of Object.entries(TOPIC_MAP)) {
666
+ if (!seenFiles.has(file) && fs.existsSync(path.join(cheatSheetsDir, file))) {
667
+ topics.push({ topic, file });
668
+ seenFiles.add(file);
669
+ }
602
670
  }
603
671
  }
604
672
 
673
+ // Build guideline topic list from bundled guidelines (filename stem → filename)
674
+ const builtInPath = getBuiltInGuidelinesPath();
675
+ const guidelineTopics = [];
676
+ if (builtInPath) {
677
+ try {
678
+ const entries = fs.readdirSync(builtInPath).filter(f => f.endsWith('.md')).sort();
679
+ for (const file of entries) {
680
+ const stem = file.replace(/\.md$/, '');
681
+ guidelineTopics.push({ topic: stem, file });
682
+ }
683
+ } catch (e) { /* ignore */ }
684
+ }
685
+
686
+ // Also include local guidelines/ topics (if present)
687
+ const localGuidelinesDir = detectGuidelinesFolder();
688
+ const localGuidelineTopics = [];
689
+ if (localGuidelinesDir) {
690
+ try {
691
+ const entries = fs.readdirSync(localGuidelinesDir).filter(f => f.endsWith('.md')).sort();
692
+ for (const file of entries) {
693
+ const stem = file.replace(/\.md$/, '');
694
+ localGuidelineTopics.push({ topic: stem, file });
695
+ }
696
+ } catch (e) { /* ignore */ }
697
+ }
698
+
605
699
  return {
606
- referenceFolder: path.dirname(cheatSheetsDir),
607
- topics: topics.sort((a, b) => a.file.localeCompare(b.file))
700
+ referenceFolder: cheatSheetsDir ? path.dirname(cheatSheetsDir) : null,
701
+ topics: topics.sort((a, b) => a.file.localeCompare(b.file)),
702
+ guidelineTopics,
703
+ localGuidelineTopics
608
704
  };
609
705
  }
610
706
 
@@ -631,6 +727,11 @@ function displaySearchResults(results) {
631
727
  if (results.guidelinesFolder) {
632
728
  sources.push('local guidelines');
633
729
  }
730
+ // Check if built-in guidelines were searched (present in matches)
731
+ const hasBuiltIn = results.files.some(f => f.repo === '[built-in]');
732
+ if (hasBuiltIn) {
733
+ sources.push('built-in guidelines');
734
+ }
634
735
  console.log(` 📁 Sources searched: ${sources.join(', ') || 'none'}`);
635
736
 
636
737
  if (results.repositories && results.repositories.length > 0) {
@@ -653,8 +754,9 @@ function displaySearchResults(results) {
653
754
 
654
755
  console.log(` ✅ Found in ${results.files.length} file(s):`);
655
756
  for (const [repo, files] of Object.entries(filesByRepo)) {
656
- const icon = repo === 'guidelines' ? '📋' : '📦';
657
- console.log(`\n ${icon} ${repo}/`);
757
+ const icon = repo === 'guidelines' ? '📋' : repo === '[built-in]' ? '📦' : '📦';
758
+ const label = repo === '[built-in]' ? 'built-in guidelines' : repo;
759
+ console.log(`\n ${icon} ${label}/`);
658
760
  files.forEach(file => {
659
761
  console.log(` • ${file}`);
660
762
  });
@@ -705,12 +807,13 @@ function displayTopic(result) {
705
807
  return;
706
808
  }
707
809
 
708
- console.log(`\n 📖 ${result.file}`);
810
+ const sourceLabel = result.source === 'built-in guidelines' ? ' [built-in]' : result.source === 'guidelines' ? ' [local]' : '';
811
+ console.log(`\n 📖 ${result.file}${sourceLabel}`);
709
812
  console.log(' ' + '─'.repeat(60));
710
813
  console.log('');
711
814
 
712
- // Display first 100 lines
713
- const lines = result.content.split('\n').slice(0, 100);
815
+ // Display up to 300 lines (covers full guideline files without truncation)
816
+ const lines = result.content.split('\n').slice(0, 300);
714
817
  lines.forEach(line => {
715
818
  const trimmed = line.slice(0, 100);
716
819
  console.log(` ${trimmed}`);
@@ -736,15 +839,40 @@ function displayTopics(result) {
736
839
  }
737
840
 
738
841
  console.log(`\n 📚 Available ABAP Reference Topics`);
739
- console.log(` 📁 Reference folder: ${result.referenceFolder}`);
842
+ if (result.referenceFolder) {
843
+ console.log(` 📁 Reference folder: ${result.referenceFolder}`);
844
+ }
740
845
  console.log('');
741
- console.log(' Topic File');
742
- console.log(' ' + '─'.repeat(60));
743
846
 
744
- result.topics.forEach(({ topic, file }) => {
745
- const paddedTopic = topic.padEnd(20);
746
- console.log(` ${paddedTopic} ${file}`);
747
- });
847
+ if (result.topics.length > 0) {
848
+ console.log(' SAP Cheat Sheets');
849
+ console.log(' Topic File');
850
+ console.log(' ' + '─'.repeat(60));
851
+ result.topics.forEach(({ topic, file }) => {
852
+ console.log(` ${topic.padEnd(20)} ${file}`);
853
+ });
854
+ console.log('');
855
+ }
856
+
857
+ if (result.localGuidelineTopics && result.localGuidelineTopics.length > 0) {
858
+ console.log(' Local Guidelines (guidelines/)');
859
+ console.log(' Topic File');
860
+ console.log(' ' + '─'.repeat(60));
861
+ result.localGuidelineTopics.forEach(({ topic, file }) => {
862
+ console.log(` ${topic.padEnd(20)} ${file}`);
863
+ });
864
+ console.log('');
865
+ }
866
+
867
+ if (result.guidelineTopics && result.guidelineTopics.length > 0) {
868
+ console.log(' Bundled Guidelines (use: abapgit-agent ref --topic <topic>)');
869
+ console.log(' Topic File');
870
+ console.log(' ' + '─'.repeat(60));
871
+ result.guidelineTopics.forEach(({ topic, file }) => {
872
+ console.log(` ${topic.padEnd(20)} ${file}`);
873
+ });
874
+ console.log('');
875
+ }
748
876
  }
749
877
 
750
878
  /**
@@ -933,39 +1061,46 @@ function initGuidelines() {
933
1061
  }
934
1062
 
935
1063
  /**
936
- * Get all guideline files from the project
937
- * @returns {Promise<Array<{name: string, path: string, content: string}>>}
1064
+ * Get all guideline files from a specific path
1065
+ * @param {string} guidelinesPath - Path to guidelines folder
1066
+ * @param {string} [label] - Optional label suffix for relativePath (default: 'guidelines')
1067
+ * @returns {Promise<Array<{name: string, path: string, content: string, relativePath: string}>>}
938
1068
  */
939
- async function getGuidelineFiles() {
940
- const guidelinesFolder = detectGuidelinesFolder();
941
- if (!guidelinesFolder) {
942
- return [];
943
- }
944
-
1069
+ async function getGuidelineFilesFromPath(guidelinesPath, label) {
1070
+ const folderLabel = label || 'guidelines';
945
1071
  const files = [];
946
-
947
1072
  try {
948
- const entries = await readdir(guidelinesFolder);
949
-
1073
+ const entries = await readdir(guidelinesPath);
950
1074
  for (const entry of entries) {
951
1075
  if (entry.endsWith('.md')) {
952
- const fullPath = path.join(guidelinesFolder, entry);
1076
+ const fullPath = path.join(guidelinesPath, entry);
953
1077
  const content = await readFile(fullPath, 'utf8');
954
1078
  files.push({
955
1079
  name: entry,
956
1080
  path: fullPath,
957
1081
  content,
958
- relativePath: path.join('guidelines', entry)
1082
+ relativePath: path.join(folderLabel, entry)
959
1083
  });
960
1084
  }
961
1085
  }
962
1086
  } catch (error) {
963
1087
  // Return empty array on error
964
1088
  }
965
-
966
1089
  return files.sort((a, b) => a.name.localeCompare(b.name));
967
1090
  }
968
1091
 
1092
+ /**
1093
+ * Get all guideline files from the project
1094
+ * @returns {Promise<Array<{name: string, path: string, content: string}>>}
1095
+ */
1096
+ async function getGuidelineFiles() {
1097
+ const guidelinesFolder = detectGuidelinesFolder();
1098
+ if (!guidelinesFolder) {
1099
+ return [];
1100
+ }
1101
+ return getGuidelineFilesFromPath(guidelinesFolder);
1102
+ }
1103
+
969
1104
  /**
970
1105
  * Export guidelines to reference folder
971
1106
  * Copies guideline files to the reference folder for searching
@@ -1134,6 +1269,7 @@ module.exports = {
1134
1269
  ensureReferenceFolder,
1135
1270
  detectGuidelinesFolder,
1136
1271
  getBuiltInGuidelinesPath,
1272
+ getGuidelineFilesFromPath,
1137
1273
  initGuidelines,
1138
1274
  getReferenceRepositories,
1139
1275
  getGuidelineFiles,