abapgit-agent 1.5.0 → 1.6.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.
Files changed (117) hide show
  1. package/README.md +1 -0
  2. package/abap/guidelines/00_index.md +35 -0
  3. package/abap/guidelines/01_sql.md +72 -0
  4. package/abap/guidelines/02_exceptions.md +108 -0
  5. package/abap/guidelines/03_testing.md +252 -0
  6. package/abap/guidelines/04_cds.md +120 -0
  7. package/abap/guidelines/05_classes.md +50 -0
  8. package/abap/guidelines/06_objects.md +103 -0
  9. package/abap/guidelines/07_json.md +22 -0
  10. package/abap/guidelines/08_abapgit.md +193 -0
  11. package/bin/abapgit-agent +467 -30
  12. package/bin/abgagt +24 -0
  13. package/package.json +8 -2
  14. package/src/abap-client.js +65 -2
  15. package/src/agent.js +57 -3
  16. package/src/ref-search.js +989 -0
  17. package/.abapGitAgent.example +0 -11
  18. package/.github/workflows/release.yml +0 -60
  19. package/API.md +0 -710
  20. package/CLAUDE.md +0 -1058
  21. package/CLAUDE_MEM.md +0 -88
  22. package/ERROR_HANDLING.md +0 -30
  23. package/INSTALL.md +0 -155
  24. package/RELEASE_NOTES.md +0 -143
  25. package/abap/CLAUDE.md +0 -1010
  26. package/abap/copilot-instructions.md +0 -79
  27. package/abap/package.devc.xml +0 -10
  28. package/abap/zcl_abgagt_agent.clas.abap +0 -420
  29. package/abap/zcl_abgagt_agent.clas.xml +0 -15
  30. package/abap/zcl_abgagt_cmd_factory.clas.abap +0 -48
  31. package/abap/zcl_abgagt_cmd_factory.clas.xml +0 -15
  32. package/abap/zcl_abgagt_command_create.clas.abap +0 -95
  33. package/abap/zcl_abgagt_command_create.clas.xml +0 -15
  34. package/abap/zcl_abgagt_command_import.clas.abap +0 -138
  35. package/abap/zcl_abgagt_command_import.clas.xml +0 -15
  36. package/abap/zcl_abgagt_command_inspect.clas.abap +0 -456
  37. package/abap/zcl_abgagt_command_inspect.clas.testclasses.abap +0 -121
  38. package/abap/zcl_abgagt_command_inspect.clas.xml +0 -16
  39. package/abap/zcl_abgagt_command_preview.clas.abap +0 -386
  40. package/abap/zcl_abgagt_command_preview.clas.xml +0 -15
  41. package/abap/zcl_abgagt_command_pull.clas.abap +0 -80
  42. package/abap/zcl_abgagt_command_pull.clas.testclasses.abap +0 -87
  43. package/abap/zcl_abgagt_command_pull.clas.xml +0 -16
  44. package/abap/zcl_abgagt_command_tree.clas.abap +0 -237
  45. package/abap/zcl_abgagt_command_tree.clas.xml +0 -15
  46. package/abap/zcl_abgagt_command_unit.clas.abap +0 -297
  47. package/abap/zcl_abgagt_command_unit.clas.xml +0 -15
  48. package/abap/zcl_abgagt_command_view.clas.abap +0 -240
  49. package/abap/zcl_abgagt_command_view.clas.xml +0 -15
  50. package/abap/zcl_abgagt_resource_create.clas.abap +0 -71
  51. package/abap/zcl_abgagt_resource_create.clas.xml +0 -15
  52. package/abap/zcl_abgagt_resource_health.clas.abap +0 -25
  53. package/abap/zcl_abgagt_resource_health.clas.xml +0 -15
  54. package/abap/zcl_abgagt_resource_import.clas.abap +0 -66
  55. package/abap/zcl_abgagt_resource_import.clas.xml +0 -15
  56. package/abap/zcl_abgagt_resource_inspect.clas.abap +0 -63
  57. package/abap/zcl_abgagt_resource_inspect.clas.xml +0 -15
  58. package/abap/zcl_abgagt_resource_preview.clas.abap +0 -67
  59. package/abap/zcl_abgagt_resource_preview.clas.xml +0 -15
  60. package/abap/zcl_abgagt_resource_pull.clas.abap +0 -71
  61. package/abap/zcl_abgagt_resource_pull.clas.xml +0 -15
  62. package/abap/zcl_abgagt_resource_tree.clas.abap +0 -70
  63. package/abap/zcl_abgagt_resource_tree.clas.xml +0 -15
  64. package/abap/zcl_abgagt_resource_unit.clas.abap +0 -64
  65. package/abap/zcl_abgagt_resource_unit.clas.xml +0 -15
  66. package/abap/zcl_abgagt_resource_view.clas.abap +0 -68
  67. package/abap/zcl_abgagt_resource_view.clas.xml +0 -15
  68. package/abap/zcl_abgagt_rest_handler.clas.abap +0 -32
  69. package/abap/zcl_abgagt_rest_handler.clas.xml +0 -15
  70. package/abap/zcl_abgagt_util.clas.abap +0 -93
  71. package/abap/zcl_abgagt_util.clas.testclasses.abap +0 -84
  72. package/abap/zcl_abgagt_util.clas.xml +0 -16
  73. package/abap/zcl_abgagt_viewer_clas.clas.abap +0 -58
  74. package/abap/zcl_abgagt_viewer_clas.clas.xml +0 -15
  75. package/abap/zcl_abgagt_viewer_ddls.clas.abap +0 -83
  76. package/abap/zcl_abgagt_viewer_ddls.clas.xml +0 -15
  77. package/abap/zcl_abgagt_viewer_dtel.clas.abap +0 -98
  78. package/abap/zcl_abgagt_viewer_dtel.clas.xml +0 -15
  79. package/abap/zcl_abgagt_viewer_factory.clas.abap +0 -41
  80. package/abap/zcl_abgagt_viewer_factory.clas.xml +0 -15
  81. package/abap/zcl_abgagt_viewer_intf.clas.abap +0 -58
  82. package/abap/zcl_abgagt_viewer_intf.clas.xml +0 -15
  83. package/abap/zcl_abgagt_viewer_stru.clas.abap +0 -59
  84. package/abap/zcl_abgagt_viewer_stru.clas.xml +0 -15
  85. package/abap/zcl_abgagt_viewer_tabl.clas.abap +0 -59
  86. package/abap/zcl_abgagt_viewer_tabl.clas.xml +0 -15
  87. package/abap/zcl_abgagt_viewer_ttyp.clas.abap +0 -93
  88. package/abap/zcl_abgagt_viewer_ttyp.clas.xml +0 -15
  89. package/abap/zif_abgagt_agent.intf.abap +0 -53
  90. package/abap/zif_abgagt_agent.intf.xml +0 -15
  91. package/abap/zif_abgagt_cmd_factory.intf.abap +0 -7
  92. package/abap/zif_abgagt_cmd_factory.intf.xml +0 -15
  93. package/abap/zif_abgagt_command.intf.abap +0 -26
  94. package/abap/zif_abgagt_command.intf.xml +0 -15
  95. package/abap/zif_abgagt_util.intf.abap +0 -28
  96. package/abap/zif_abgagt_util.intf.xml +0 -15
  97. package/abap/zif_abgagt_viewer.intf.abap +0 -12
  98. package/abap/zif_abgagt_viewer.intf.xml +0 -15
  99. package/docs/commands.md +0 -142
  100. package/docs/create-command.md +0 -129
  101. package/docs/health-command.md +0 -89
  102. package/docs/import-command.md +0 -195
  103. package/docs/init-command.md +0 -189
  104. package/docs/inspect-command.md +0 -169
  105. package/docs/list-command.md +0 -289
  106. package/docs/preview-command.md +0 -528
  107. package/docs/pull-command.md +0 -202
  108. package/docs/status-command.md +0 -68
  109. package/docs/tree-command.md +0 -303
  110. package/docs/unit-command.md +0 -167
  111. package/docs/view-command.md +0 -501
  112. package/img/claude.png +0 -0
  113. package/scripts/claude-integration.js +0 -351
  114. package/scripts/release.js +0 -298
  115. package/scripts/release.sh +0 -60
  116. package/scripts/test-integration.js +0 -139
  117. package/scripts/unrelease.js +0 -277
@@ -0,0 +1,989 @@
1
+ /**
2
+ * ABAP Reference Search - Search ABAP reference repositories for patterns
3
+ *
4
+ * This module provides portable reference lookup across multiple ABAP repositories
5
+ * including cheat sheets and any other ABAP code repositories in the reference folder.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { promisify } = require('util');
11
+ const readdir = promisify(fs.readdir);
12
+ const readFile = promisify(fs.readFile);
13
+ const stat = promisify(fs.stat);
14
+
15
+ // Topic to file mapping
16
+ const TOPIC_MAP = {
17
+ 'internal-tables': '01_Internal_Tables.md',
18
+ 'structures': '02_Structures.md',
19
+ 'sql': '03_ABAP_SQL.md',
20
+ 'oop': '04_ABAP_Object_Orientation.md',
21
+ 'objects': '04_ABAP_Object_Orientation.md',
22
+ 'constructors': '05_Constructor_Expressions.md',
23
+ 'constructor': '05_Constructor_Expressions.md',
24
+ 'dynamic': '06_Dynamic_Programming.md',
25
+ 'rtti': '06_Dynamic_Programming.md',
26
+ 'strings': '07_String_Processing.md',
27
+ 'string': '07_String_Processing.md',
28
+ 'eml': '08_EML_ABAP_for_RAP.md',
29
+ 'hierarchies': '10_ABAP_SQL_Hierarchies.md',
30
+ 'grouping': '11_Internal_Tables_Grouping.md',
31
+ 'amdp': '12_AMDP.md',
32
+ 'flow': '13_Program_Flow_Logic.md',
33
+ 'unit-tests': '14_ABAP_Unit_Tests.md',
34
+ 'unit': '14_ABAP_Unit_Tests.md',
35
+ 'testing': '14_ABAP_Unit_Tests.md',
36
+ 'cds': '15_CDS_View_Entities.md',
37
+ 'datatypes': '16_Data_Types_and_Objects.md',
38
+ 'luw': '17_SAP_LUW.md',
39
+ 'dynpro': '18_Dynpro.md',
40
+ 'cloud': '19_ABAP_for_Cloud_Development.md',
41
+ 'selection-screens': '20_Selection_Screens_Lists.md',
42
+ 'json-xml': '21_XML_JSON.md',
43
+ 'json': '21_XML_JSON.md',
44
+ 'xml': '21_XML_JSON.md',
45
+ 'released-classes': '22_Released_ABAP_Classes.md',
46
+ 'datetime': '23_Date_and_Time.md',
47
+ 'functions': '24_Builtin_Functions.md',
48
+ 'auth': '25_Authorization_Checks.md',
49
+ 'authorization': '25_Authorization_Checks.md',
50
+ 'dictionary': '26_ABAP_Dictionary.md',
51
+ 'exceptions': '27_Exceptions.md',
52
+ 'exception': '27_Exceptions.md',
53
+ 'regex': '28_Regular_Expressions.md',
54
+ 'numeric': '29_Numeric_Operations.md',
55
+ 'ai': '30_Generative_AI.md',
56
+ 'where': '31_WHERE_Conditions.md',
57
+ 'performance': '32_Performance_Notes.md',
58
+ 'news': '33_ABAP_Release_News.md',
59
+ 'patterns': '34_OO_Design_Patterns.md',
60
+ 'design-patterns': '34_OO_Design_Patterns.md',
61
+ 'badis': '35_BAdIs.md',
62
+ 'rap': '36_RAP_Behavior_Definition_Language.md',
63
+ 'tables': '01_Internal_Tables.md'
64
+ };
65
+
66
+ /**
67
+ * Detect reference folder from config or common locations
68
+ * @returns {string|null} Path to reference folder or null if not found
69
+ */
70
+ function detectReferenceFolder() {
71
+ // Try config file in current working directory
72
+ const configPath = path.join(process.cwd(), '.abapGitAgent');
73
+ if (fs.existsSync(configPath)) {
74
+ try {
75
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
76
+ if (config.referenceFolder && fs.existsSync(config.referenceFolder)) {
77
+ const cheatSheetsPath = path.join(config.referenceFolder, 'abap-cheat-sheets');
78
+ if (fs.existsSync(cheatSheetsPath)) {
79
+ return config.referenceFolder;
80
+ }
81
+ }
82
+ } catch (e) {
83
+ // Config exists but couldn't parse, continue to fallback
84
+ }
85
+ }
86
+
87
+ // Fallback to common locations
88
+ const homeDir = require('os').homedir();
89
+ const commonPaths = [
90
+ path.join(homeDir, 'abap-reference'),
91
+ path.join(homeDir, 'Documents', 'abap-reference'),
92
+ path.join(homeDir, 'Documents', 'code', 'abap-reference')
93
+ ];
94
+
95
+ for (const basePath of commonPaths) {
96
+ const cheatSheetsPath = path.join(basePath, 'abap-cheat-sheets');
97
+ if (fs.existsSync(cheatSheetsPath)) {
98
+ return basePath;
99
+ }
100
+ }
101
+
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Get cheat sheets directory path
107
+ * @returns {string|null}
108
+ */
109
+ function getCheatSheetsDir() {
110
+ const refFolder = detectReferenceFolder();
111
+ if (!refFolder) return null;
112
+ return path.join(refFolder, 'abap-cheat-sheets');
113
+ }
114
+
115
+ /**
116
+ * Get all ABAP repositories in the reference folder
117
+ * @returns {Promise<Array<{name: string, path: string}>>}
118
+ */
119
+ async function getReferenceRepositories() {
120
+ const refFolder = detectReferenceFolder();
121
+ if (!refFolder) return [];
122
+
123
+ const repos = [];
124
+
125
+ try {
126
+ const entries = await readdir(refFolder);
127
+
128
+ for (const entry of entries) {
129
+ const fullPath = path.join(refFolder, entry);
130
+ const stats = await stat(fullPath);
131
+
132
+ // Check if it's a directory
133
+ if (stats.isDirectory()) {
134
+ // Check for custom-guidelines folder
135
+ if (entry === 'custom-guidelines') {
136
+ repos.push({
137
+ name: entry,
138
+ path: fullPath,
139
+ isGitRepo: false,
140
+ isCustomGuidelines: true
141
+ });
142
+ continue;
143
+ }
144
+
145
+ const gitDir = path.join(fullPath, '.git');
146
+ const hasGit = fs.existsSync(gitDir);
147
+
148
+ // Check for ABAP files or common ABAP project files
149
+ const hasAbapFiles = await hasAbapContent(fullPath);
150
+
151
+ if (hasGit || hasAbapFiles) {
152
+ repos.push({
153
+ name: entry,
154
+ path: fullPath,
155
+ isGitRepo: hasGit
156
+ });
157
+ }
158
+ }
159
+ }
160
+ } catch (error) {
161
+ // Return empty array on error
162
+ }
163
+
164
+ return repos.sort((a, b) => a.name.localeCompare(b.name));
165
+ }
166
+
167
+ /**
168
+ * Check if a directory contains ABAP-related content
169
+ * @param {string} dirPath
170
+ * @returns {Promise<boolean>}
171
+ */
172
+ async function hasAbapContent(dirPath) {
173
+ const abapIndicators = [
174
+ 'abap/', 'src/', 'zcl_', 'zif_',
175
+ '.abapgit.xml', 'package.devc.xml',
176
+ '.clas.abap', '.intf.abap', '.tabl.xml'
177
+ ];
178
+
179
+ try {
180
+ const entries = await readdir(dirPath);
181
+
182
+ for (const entry of entries) {
183
+ const lowerEntry = entry.toLowerCase();
184
+
185
+ // Check for ABAP file extensions
186
+ if (lowerEntry.endsWith('.abap') ||
187
+ lowerEntry.endsWith('.clas.xml') ||
188
+ lowerEntry.endsWith('.intf.xml') ||
189
+ lowerEntry.endsWith('.tabl.xml') ||
190
+ lowerEntry.endsWith('.ddls.asddls') ||
191
+ lowerEntry.includes('zcl_') ||
192
+ lowerEntry.includes('zif_')) {
193
+ return true;
194
+ }
195
+
196
+ // Check for abap folder
197
+ if (lowerEntry === 'abap' || lowerEntry === 'src') {
198
+ return true;
199
+ }
200
+ }
201
+ } catch (error) {
202
+ return false;
203
+ }
204
+
205
+ return false;
206
+ }
207
+
208
+ /**
209
+ * Recursively get all searchable files from a repository
210
+ * @param {string} repoPath
211
+ * @param {string} repoName
212
+ * @param {Array<string>} extensions
213
+ * @returns {Promise<Array<{repo: string, path: string, relativePath: string}>>}
214
+ */
215
+ async function getSearchableFiles(repoPath, repoName, extensions = ['.md', '.abap', '.txt', '.asddls']) {
216
+ const files = [];
217
+
218
+ async function walkDir(currentPath, relativePath = '') {
219
+ try {
220
+ const entries = await readdir(currentPath);
221
+
222
+ for (const entry of entries) {
223
+ const fullPath = path.join(currentPath, entry);
224
+ const relPath = path.join(relativePath, entry);
225
+ const stats = await stat(fullPath);
226
+
227
+ if (stats.isDirectory()) {
228
+ // Skip common non-source directories
229
+ const skipDirs = ['.git', 'node_modules', 'bin', 'tests', 'test'];
230
+ if (!skipDirs.includes(entry.toLowerCase())) {
231
+ await walkDir(fullPath, relPath);
232
+ }
233
+ } else if (stats.isFile()) {
234
+ const ext = path.extname(entry).toLowerCase();
235
+ const lowerEntry = entry.toLowerCase();
236
+
237
+ // Check if file matches searchable extensions
238
+ const isSearchable = extensions.includes(ext) ||
239
+ extensions.some(e => lowerEntry.endsWith(e));
240
+
241
+ if (isSearchable) {
242
+ files.push({
243
+ repo: repoName,
244
+ path: fullPath,
245
+ relativePath: relPath
246
+ });
247
+ }
248
+ }
249
+ }
250
+ } catch (error) {
251
+ // Skip directories we can't read
252
+ }
253
+ }
254
+
255
+ await walkDir(repoPath);
256
+ return files;
257
+ }
258
+
259
+ /**
260
+ * Search for a pattern across all reference repositories
261
+ * @param {string} pattern - Pattern to search for
262
+ * @returns {Promise<Object>} Search results
263
+ */
264
+ async function searchPattern(pattern) {
265
+ const refFolder = detectReferenceFolder();
266
+ const repos = await getReferenceRepositories();
267
+
268
+ if (!refFolder) {
269
+ return {
270
+ error: 'Reference folder not found',
271
+ hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
272
+ };
273
+ }
274
+
275
+ if (repos.length === 0) {
276
+ return {
277
+ error: 'No ABAP repositories found in reference folder',
278
+ hint: 'Clone ABAP repositories to the reference folder to enable searching'
279
+ };
280
+ }
281
+
282
+ const results = {
283
+ pattern,
284
+ referenceFolder: refFolder,
285
+ repositories: repos.map(r => r.name),
286
+ files: [],
287
+ matches: []
288
+ };
289
+
290
+ try {
291
+ // Search across all repositories
292
+ for (const repo of repos) {
293
+ const searchableFiles = await getSearchableFiles(repo.path, repo.name);
294
+
295
+ for (const fileInfo of searchableFiles) {
296
+ try {
297
+ const content = await readFile(fileInfo.path, 'utf8');
298
+
299
+ if (content.toLowerCase().includes(pattern.toLowerCase())) {
300
+ results.files.push({
301
+ repo: repo.name,
302
+ file: fileInfo.relativePath
303
+ });
304
+
305
+ // Find matching lines with context
306
+ const lines = content.split('\n');
307
+ let matchCount = 0;
308
+
309
+ for (let i = 0; i < lines.length; i++) {
310
+ if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
311
+ const start = Math.max(0, i - 1);
312
+ const end = Math.min(lines.length, i + 2);
313
+ const context = lines.slice(start, end).join('\n');
314
+
315
+ results.matches.push({
316
+ repo: repo.name,
317
+ file: fileInfo.relativePath,
318
+ line: i + 1,
319
+ context
320
+ });
321
+
322
+ matchCount++;
323
+
324
+ // Limit matches per file to avoid overwhelming output
325
+ if (matchCount >= 3) {
326
+ break;
327
+ }
328
+ }
329
+ }
330
+ }
331
+ } catch (error) {
332
+ // Skip files we can't read
333
+ }
334
+ }
335
+ }
336
+
337
+ return results;
338
+ } catch (error) {
339
+ return {
340
+ error: `Search failed: ${error.message}`
341
+ };
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Get content for a specific topic
347
+ * @param {string} topic - Topic name
348
+ * @returns {Promise<Object>} Topic content
349
+ */
350
+ async function getTopic(topic) {
351
+ const topicLower = topic.toLowerCase();
352
+
353
+ // First, check local guidelines folder
354
+ const guidelinesDir = detectGuidelinesFolder();
355
+ if (guidelinesDir) {
356
+ // Map topic to local guideline file
357
+ const guidelineMap = {
358
+ 'abapgit': '08_abapgit.md',
359
+ 'xml': '06_objects.md',
360
+ 'objects': '06_objects.md',
361
+ 'naming': '06_objects.md'
362
+ };
363
+
364
+ const guidelineFile = guidelineMap[topicLower];
365
+ if (guidelineFile) {
366
+ const guidelinePath = path.join(guidelinesDir, guidelineFile);
367
+ if (fs.existsSync(guidelinePath)) {
368
+ try {
369
+ const content = await readFile(guidelinePath, 'utf8');
370
+ return {
371
+ topic,
372
+ file: guidelineFile,
373
+ content: content.slice(0, 5000),
374
+ truncated: content.length > 5000,
375
+ totalLength: content.length,
376
+ source: 'guidelines'
377
+ };
378
+ } catch (error) {
379
+ // Continue to check external reference
380
+ }
381
+ }
382
+ }
383
+ }
384
+
385
+ // Fall back to external reference folder
386
+ const cheatSheetsDir = getCheatSheetsDir();
387
+
388
+ if (!cheatSheetsDir) {
389
+ return {
390
+ error: 'Reference folder not found',
391
+ hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
392
+ };
393
+ }
394
+
395
+ const fileName = TOPIC_MAP[topicLower];
396
+ if (!fileName) {
397
+ return {
398
+ error: `Unknown topic: ${topic}`,
399
+ availableTopics: Object.keys(TOPIC_MAP).filter((v, i, a) => a.indexOf(v) === i).slice(0, 20)
400
+ };
401
+ }
402
+
403
+ const filePath = path.join(cheatSheetsDir, fileName);
404
+
405
+ if (!fs.existsSync(filePath)) {
406
+ return {
407
+ error: `File not found: ${fileName}`
408
+ };
409
+ }
410
+
411
+ try {
412
+ const content = await readFile(filePath, 'utf8');
413
+ return {
414
+ topic,
415
+ file: fileName,
416
+ content: content.slice(0, 5000), // First 5000 chars
417
+ truncated: content.length > 5000,
418
+ totalLength: content.length
419
+ };
420
+ } catch (error) {
421
+ return {
422
+ error: `Failed to read topic: ${error.message}`
423
+ };
424
+ }
425
+ }
426
+
427
+ /**
428
+ * List available topics
429
+ * @returns {Promise<Object>} List of topics
430
+ */
431
+ async function listTopics() {
432
+ const cheatSheetsDir = getCheatSheetsDir();
433
+
434
+ if (!cheatSheetsDir) {
435
+ return {
436
+ error: 'Reference folder not found',
437
+ hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
438
+ };
439
+ }
440
+
441
+ // Build topic list from files that exist
442
+ const topics = [];
443
+ const seenFiles = new Set();
444
+
445
+ for (const [topic, file] of Object.entries(TOPIC_MAP)) {
446
+ if (!seenFiles.has(file) && fs.existsSync(path.join(cheatSheetsDir, file))) {
447
+ topics.push({ topic, file });
448
+ seenFiles.add(file);
449
+ }
450
+ }
451
+
452
+ return {
453
+ referenceFolder: path.dirname(cheatSheetsDir),
454
+ topics: topics.sort((a, b) => a.file.localeCompare(b.file))
455
+ };
456
+ }
457
+
458
+ /**
459
+ * Display search results in console format
460
+ * @param {Object} results - Search results
461
+ */
462
+ function displaySearchResults(results) {
463
+ if (results.error) {
464
+ console.error(`\n ❌ ${results.error}`);
465
+ if (results.hint) {
466
+ console.error(`\n 💡 ${results.hint}`);
467
+ }
468
+ return;
469
+ }
470
+
471
+ console.log(`\n 🔍 Searching for: '${results.pattern}'`);
472
+ console.log(` 📁 Reference folder: ${results.referenceFolder}`);
473
+
474
+ if (results.repositories && results.repositories.length > 0) {
475
+ console.log(` 📚 Repositories (${results.repositories.length}): ${results.repositories.join(', ')}`);
476
+ }
477
+ console.log('');
478
+
479
+ if (results.files.length === 0) {
480
+ console.log(' ⚠️ No matches found.');
481
+ return;
482
+ }
483
+
484
+ // Group files by repository
485
+ const filesByRepo = {};
486
+ results.files.forEach(fileInfo => {
487
+ const repo = fileInfo.repo || 'unknown';
488
+ if (!filesByRepo[repo]) filesByRepo[repo] = [];
489
+ filesByRepo[repo].push(fileInfo.file);
490
+ });
491
+
492
+ console.log(` ✅ Found in ${results.files.length} file(s):`);
493
+ for (const [repo, files] of Object.entries(filesByRepo)) {
494
+ console.log(`\n 📦 ${repo}/`);
495
+ files.forEach(file => {
496
+ console.log(` • ${file}`);
497
+ });
498
+ }
499
+ console.log('');
500
+
501
+ // Show first 5 matches
502
+ console.log(' 📄 Preview (first 5 matches):');
503
+ console.log(' ' + '─'.repeat(60));
504
+
505
+ const uniqueMatches = [];
506
+ const seenContexts = new Set();
507
+
508
+ for (const match of results.matches) {
509
+ const key = `${match.repo}:${match.file}:${match.context}`;
510
+ if (!seenContexts.has(key)) {
511
+ uniqueMatches.push(match);
512
+ seenContexts.add(key);
513
+ }
514
+ if (uniqueMatches.length >= 5) break;
515
+ }
516
+
517
+ for (const match of uniqueMatches) {
518
+ console.log(` 📄 ${match.repo}/${match.file} (line ${match.line}):`);
519
+ const lines = match.context.split('\n');
520
+ lines.forEach((line, idx) => {
521
+ const prefix = idx === 1 ? ' → ' : ' ';
522
+ const trimmed = line.slice(0, 80);
523
+ console.log(`${prefix}${trimmed}`);
524
+ });
525
+ console.log('');
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Display topic content in console format
531
+ * @param {Object} result - Topic result
532
+ */
533
+ function displayTopic(result) {
534
+ if (result.error) {
535
+ console.error(`\n ❌ ${result.error}`);
536
+ if (result.hint) {
537
+ console.error(`\n 💡 ${result.hint}`);
538
+ }
539
+ if (result.availableTopics) {
540
+ console.error(`\n Available topics: ${result.availableTopics.join(', ')}`);
541
+ }
542
+ return;
543
+ }
544
+
545
+ console.log(`\n 📖 ${result.file}`);
546
+ console.log(' ' + '─'.repeat(60));
547
+ console.log('');
548
+
549
+ // Display first 100 lines
550
+ const lines = result.content.split('\n').slice(0, 100);
551
+ lines.forEach(line => {
552
+ const trimmed = line.slice(0, 100);
553
+ console.log(` ${trimmed}`);
554
+ });
555
+
556
+ if (result.truncated) {
557
+ console.log('');
558
+ console.log(` ... (${result.totalLength - result.content.length} more characters)`);
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Display topic list in console format
564
+ * @param {Object} result - Topics result
565
+ */
566
+ function displayTopics(result) {
567
+ if (result.error) {
568
+ console.error(`\n ❌ ${result.error}`);
569
+ if (result.hint) {
570
+ console.error(`\n 💡 ${result.hint}`);
571
+ }
572
+ return;
573
+ }
574
+
575
+ console.log(`\n 📚 Available ABAP Reference Topics`);
576
+ console.log(` 📁 Reference folder: ${result.referenceFolder}`);
577
+ console.log('');
578
+ console.log(' Topic File');
579
+ console.log(' ' + '─'.repeat(60));
580
+
581
+ result.topics.forEach(({ topic, file }) => {
582
+ const paddedTopic = topic.padEnd(20);
583
+ console.log(` ${paddedTopic} ${file}`);
584
+ });
585
+ }
586
+
587
+ /**
588
+ * List all reference repositories
589
+ * @returns {Promise<Object>} List of repositories
590
+ */
591
+ async function listRepositories() {
592
+ const refFolder = detectReferenceFolder();
593
+ const repos = await getReferenceRepositories();
594
+
595
+ if (!refFolder) {
596
+ return {
597
+ error: 'Reference folder not found',
598
+ hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
599
+ };
600
+ }
601
+
602
+ return {
603
+ referenceFolder: refFolder,
604
+ repositories: repos
605
+ };
606
+ }
607
+
608
+ /**
609
+ * Display repositories in console format
610
+ * @param {Object} result - Repositories result
611
+ */
612
+ function displayRepositories(result) {
613
+ if (result.error) {
614
+ console.error(`\n ❌ ${result.error}`);
615
+ if (result.hint) {
616
+ console.error(`\n 💡 ${result.hint}`);
617
+ }
618
+ return;
619
+ }
620
+
621
+ console.log(`\n 📚 ABAP Reference Repositories`);
622
+ console.log(` 📁 Reference folder: ${result.referenceFolder}`);
623
+ console.log('');
624
+
625
+ if (result.repositories.length === 0) {
626
+ console.log(' ⚠️ No repositories found.');
627
+ console.log('');
628
+ console.log(' Add repositories by cloning them to the reference folder:');
629
+ console.log(' cd ' + result.referenceFolder);
630
+ console.log(' git clone <repo-url>');
631
+ return;
632
+ }
633
+
634
+ console.log(` Found ${result.repositories.length} repository(ies):`);
635
+ console.log('');
636
+ console.log(' Repository Type');
637
+ console.log(' ' + '─'.repeat(50));
638
+
639
+ result.repositories.forEach(repo => {
640
+ let type;
641
+ if (repo.isCustomGuidelines) {
642
+ type = 'Custom Guidelines';
643
+ } else if (repo.isGitRepo) {
644
+ type = 'Git Repo';
645
+ } else {
646
+ type = 'ABAP Folder';
647
+ }
648
+ const paddedName = repo.name.padEnd(30);
649
+ console.log(` ${paddedName} ${type}`);
650
+ });
651
+ }
652
+
653
+ /**
654
+ * Detect guidelines folder in current project
655
+ * Looks for abap/guidelines/ folder
656
+ * @returns {string|null} Path to guidelines folder or null if not found
657
+ */
658
+ function detectGuidelinesFolder() {
659
+ const cwd = process.cwd();
660
+ const possiblePaths = [
661
+ path.join(cwd, 'abap', 'guidelines'),
662
+ path.join(cwd, 'guidelines'),
663
+ path.join(cwd, 'docs', 'guidelines')
664
+ ];
665
+
666
+ for (const guidelinesPath of possiblePaths) {
667
+ if (fs.existsSync(guidelinesPath)) {
668
+ const stats = fs.statSync(guidelinesPath);
669
+ if (stats.isDirectory()) {
670
+ return guidelinesPath;
671
+ }
672
+ }
673
+ }
674
+
675
+ return null;
676
+ }
677
+
678
+ /**
679
+ * Get the path to the built-in guidelines in abapgit-agent
680
+ * @returns {string|null} Path to built-in guidelines or null if not found
681
+ */
682
+ function getBuiltInGuidelinesPath() {
683
+ // Try to find the guidelines relative to this module
684
+ // This allows the CLI to work from any location
685
+ const possiblePaths = [
686
+ // When running from source (bin/abapgit-agent)
687
+ path.join(__dirname, '..', 'abap', 'guidelines'),
688
+ // When running from installed package
689
+ path.join(__dirname, '..', '..', 'abap', 'guidelines')
690
+ ];
691
+
692
+ for (const guidelinesPath of possiblePaths) {
693
+ if (fs.existsSync(guidelinesPath)) {
694
+ const stats = fs.statSync(guidelinesPath);
695
+ if (stats.isDirectory()) {
696
+ return guidelinesPath;
697
+ }
698
+ }
699
+ }
700
+
701
+ return null;
702
+ }
703
+
704
+ /**
705
+ * Initialize guidelines in current project by copying from abapgit-agent
706
+ * @returns {Object} Init result
707
+ */
708
+ function initGuidelines() {
709
+ const builtInPath = getBuiltInGuidelinesPath();
710
+
711
+ if (!builtInPath) {
712
+ return {
713
+ success: false,
714
+ error: 'Built-in guidelines not found',
715
+ hint: 'Make sure abapgit-agent is properly installed'
716
+ };
717
+ }
718
+
719
+ const cwd = process.cwd();
720
+ const targetPath = path.join(cwd, 'abap', 'guidelines');
721
+
722
+ // Check if guidelines already exist
723
+ if (fs.existsSync(targetPath)) {
724
+ return {
725
+ success: false,
726
+ error: 'Guidelines folder already exists',
727
+ hint: `Delete or rename '${targetPath}' first, then run --init again`,
728
+ existingPath: targetPath
729
+ };
730
+ }
731
+
732
+ try {
733
+ // Create target directory
734
+ fs.mkdirSync(targetPath, { recursive: true });
735
+
736
+ // Copy all files from built-in guidelines
737
+ const files = fs.readdirSync(builtInPath);
738
+ let copied = 0;
739
+
740
+ for (const file of files) {
741
+ if (file.endsWith('.md')) {
742
+ const srcPath = path.join(builtInPath, file);
743
+ const destPath = path.join(targetPath, file);
744
+ const content = fs.readFileSync(srcPath, 'utf8');
745
+ fs.writeFileSync(destPath, content);
746
+ copied++;
747
+ }
748
+ }
749
+
750
+ if (copied === 0) {
751
+ return {
752
+ success: false,
753
+ error: 'No guideline files found in built-in guidelines'
754
+ };
755
+ }
756
+
757
+ return {
758
+ success: true,
759
+ message: `Initialized ${copied} guideline(s) in your project`,
760
+ sourceFolder: builtInPath,
761
+ targetFolder: targetPath,
762
+ files: files.filter(f => f.endsWith('.md'))
763
+ };
764
+ } catch (error) {
765
+ return {
766
+ success: false,
767
+ error: `Failed to initialize: ${error.message}`
768
+ };
769
+ }
770
+ }
771
+
772
+ /**
773
+ * Get all guideline files from the project
774
+ * @returns {Promise<Array<{name: string, path: string, content: string}>>}
775
+ */
776
+ async function getGuidelineFiles() {
777
+ const guidelinesFolder = detectGuidelinesFolder();
778
+ if (!guidelinesFolder) {
779
+ return [];
780
+ }
781
+
782
+ const files = [];
783
+
784
+ try {
785
+ const entries = await readdir(guidelinesFolder);
786
+
787
+ for (const entry of entries) {
788
+ if (entry.endsWith('.md')) {
789
+ const fullPath = path.join(guidelinesFolder, entry);
790
+ const content = await readFile(fullPath, 'utf8');
791
+ files.push({
792
+ name: entry,
793
+ path: fullPath,
794
+ content,
795
+ relativePath: path.join('guidelines', entry)
796
+ });
797
+ }
798
+ }
799
+ } catch (error) {
800
+ // Return empty array on error
801
+ }
802
+
803
+ return files.sort((a, b) => a.name.localeCompare(b.name));
804
+ }
805
+
806
+ /**
807
+ * Export guidelines to reference folder
808
+ * Copies guideline files to the reference folder for searching
809
+ * @returns {Promise<Object>} Export result
810
+ */
811
+ async function exportGuidelines() {
812
+ const guidelinesFolder = detectGuidelinesFolder();
813
+ const refFolder = detectReferenceFolder();
814
+
815
+ if (!guidelinesFolder) {
816
+ return {
817
+ success: false,
818
+ error: 'No guidelines folder found',
819
+ hint: 'Create abap/guidelines/ folder in your project'
820
+ };
821
+ }
822
+
823
+ if (!refFolder) {
824
+ return {
825
+ success: false,
826
+ error: 'Reference folder not found',
827
+ hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
828
+ };
829
+ }
830
+
831
+ const guidelineFiles = await getGuidelineFiles();
832
+
833
+ if (guidelineFiles.length === 0) {
834
+ return {
835
+ success: false,
836
+ error: 'No guideline files found',
837
+ hint: 'Create .md files in abap/guidelines/'
838
+ };
839
+ }
840
+
841
+ // Create a dedicated folder in reference for guidelines
842
+ const exportPath = path.join(refFolder, 'custom-guidelines');
843
+
844
+ try {
845
+ // Create export directory if it doesn't exist
846
+ if (!fs.existsSync(exportPath)) {
847
+ fs.mkdirSync(exportPath, { recursive: true });
848
+ }
849
+
850
+ let exported = 0;
851
+
852
+ for (const file of guidelineFiles) {
853
+ const destPath = path.join(exportPath, file.name);
854
+ fs.writeFileSync(destPath, file.content);
855
+ exported++;
856
+ }
857
+
858
+ return {
859
+ success: true,
860
+ message: `Exported ${exported} guideline(s) to reference folder`,
861
+ sourceFolder: guidelinesFolder,
862
+ exportFolder: exportPath,
863
+ files: guidelineFiles.map(f => f.name)
864
+ };
865
+ } catch (error) {
866
+ return {
867
+ success: false,
868
+ error: `Failed to export: ${error.message}`
869
+ };
870
+ }
871
+ }
872
+
873
+ /**
874
+ * Search guidelines in current project
875
+ * @param {string} pattern - Pattern to search for
876
+ * @returns {Promise<Object>} Search results
877
+ */
878
+ async function searchGuidelines(pattern) {
879
+ const guidelineFiles = await getGuidelineFiles();
880
+
881
+ if (guidelineFiles.length === 0) {
882
+ return {
883
+ pattern,
884
+ guidelinesFound: false,
885
+ message: 'No guideline files found in project'
886
+ };
887
+ }
888
+
889
+ const results = {
890
+ pattern,
891
+ guidelinesFound: true,
892
+ files: [],
893
+ matches: []
894
+ };
895
+
896
+ for (const file of guidelineFiles) {
897
+ if (file.content.toLowerCase().includes(pattern.toLowerCase())) {
898
+ results.files.push(file.relativePath);
899
+
900
+ // Find matching lines with context
901
+ const lines = file.content.split('\n');
902
+
903
+ for (let i = 0; i < lines.length; i++) {
904
+ if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
905
+ const start = Math.max(0, i - 1);
906
+ const end = Math.min(lines.length, i + 2);
907
+ const context = lines.slice(start, end).join('\n');
908
+
909
+ results.matches.push({
910
+ file: file.relativePath,
911
+ line: i + 1,
912
+ context
913
+ });
914
+ }
915
+ }
916
+ }
917
+ }
918
+
919
+ return results;
920
+ }
921
+
922
+ /**
923
+ * Display export result
924
+ * @param {Object} result - Export result
925
+ */
926
+ function displayExportResult(result) {
927
+ if (result.success) {
928
+ console.log(`\n ✅ ${result.message}`);
929
+ console.log(`\n 📁 Source: ${result.sourceFolder}`);
930
+ console.log(` 📁 Export: ${result.exportFolder}`);
931
+ console.log(`\n Files exported:`);
932
+ for (const file of result.files) {
933
+ console.log(` - ${file}`);
934
+ }
935
+ console.log(`\n 💡 These guidelines will now be searchable via 'ref' command`);
936
+ } else {
937
+ console.error(`\n ❌ ${result.error}`);
938
+ if (result.hint) {
939
+ console.error(`\n 💡 ${result.hint}`);
940
+ }
941
+ }
942
+ }
943
+
944
+ /**
945
+ * Display init result
946
+ * @param {Object} result - Init result
947
+ */
948
+ function displayInitResult(result) {
949
+ if (result.success) {
950
+ console.log(`\n ✅ ${result.message}`);
951
+ console.log(`\n 📁 Source: ${result.sourceFolder}`);
952
+ console.log(` 📁 Created: ${result.targetFolder}`);
953
+ console.log(`\n Files copied:`);
954
+ for (const file of result.files) {
955
+ console.log(` - ${file}`);
956
+ }
957
+ console.log(`\n 💡 Next steps:`);
958
+ console.log(` 1. Review the guidelines in abap/guidelines/`);
959
+ console.log(` 2. Customize as needed for your project`);
960
+ console.log(` 3. Run 'abapgit-agent ref --export' to make them searchable`);
961
+ } else {
962
+ console.error(`\n ❌ ${result.error}`);
963
+ if (result.hint) {
964
+ console.error(`\n 💡 ${result.hint}`);
965
+ }
966
+ }
967
+ }
968
+
969
+ module.exports = {
970
+ detectReferenceFolder,
971
+ detectGuidelinesFolder,
972
+ getBuiltInGuidelinesPath,
973
+ initGuidelines,
974
+ getReferenceRepositories,
975
+ getGuidelineFiles,
976
+ searchPattern,
977
+ searchGuidelines,
978
+ getTopic,
979
+ listTopics,
980
+ listRepositories,
981
+ exportGuidelines,
982
+ displaySearchResults,
983
+ displayTopic,
984
+ displayTopics,
985
+ displayRepositories,
986
+ displayExportResult,
987
+ displayInitResult,
988
+ TOPIC_MAP
989
+ };