abapgit-agent 1.5.0 → 1.6.1

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 (119) hide show
  1. package/README.md +1 -0
  2. package/abap/guidelines/00_index.md +36 -0
  3. package/abap/guidelines/01_sql.md +88 -0
  4. package/abap/guidelines/02_exceptions.md +176 -0
  5. package/abap/guidelines/03_testing.md +269 -0
  6. package/abap/guidelines/04_cds.md +136 -0
  7. package/abap/guidelines/05_classes.md +58 -0
  8. package/abap/guidelines/06_objects.md +110 -0
  9. package/abap/guidelines/07_json.md +24 -0
  10. package/abap/guidelines/08_abapgit.md +222 -0
  11. package/abap/guidelines/09_unit_testable_code.md +568 -0
  12. package/bin/abapgit-agent +513 -38
  13. package/bin/abgagt +24 -0
  14. package/package.json +8 -2
  15. package/src/abap-client.js +65 -2
  16. package/src/agent.js +57 -3
  17. package/src/config.js +1 -1
  18. package/src/ref-search.js +1037 -0
  19. package/.abapGitAgent.example +0 -11
  20. package/.github/workflows/release.yml +0 -60
  21. package/API.md +0 -710
  22. package/CLAUDE.md +0 -1058
  23. package/CLAUDE_MEM.md +0 -88
  24. package/ERROR_HANDLING.md +0 -30
  25. package/INSTALL.md +0 -155
  26. package/RELEASE_NOTES.md +0 -143
  27. package/abap/CLAUDE.md +0 -1010
  28. package/abap/copilot-instructions.md +0 -79
  29. package/abap/package.devc.xml +0 -10
  30. package/abap/zcl_abgagt_agent.clas.abap +0 -420
  31. package/abap/zcl_abgagt_agent.clas.xml +0 -15
  32. package/abap/zcl_abgagt_cmd_factory.clas.abap +0 -48
  33. package/abap/zcl_abgagt_cmd_factory.clas.xml +0 -15
  34. package/abap/zcl_abgagt_command_create.clas.abap +0 -95
  35. package/abap/zcl_abgagt_command_create.clas.xml +0 -15
  36. package/abap/zcl_abgagt_command_import.clas.abap +0 -138
  37. package/abap/zcl_abgagt_command_import.clas.xml +0 -15
  38. package/abap/zcl_abgagt_command_inspect.clas.abap +0 -456
  39. package/abap/zcl_abgagt_command_inspect.clas.testclasses.abap +0 -121
  40. package/abap/zcl_abgagt_command_inspect.clas.xml +0 -16
  41. package/abap/zcl_abgagt_command_preview.clas.abap +0 -386
  42. package/abap/zcl_abgagt_command_preview.clas.xml +0 -15
  43. package/abap/zcl_abgagt_command_pull.clas.abap +0 -80
  44. package/abap/zcl_abgagt_command_pull.clas.testclasses.abap +0 -87
  45. package/abap/zcl_abgagt_command_pull.clas.xml +0 -16
  46. package/abap/zcl_abgagt_command_tree.clas.abap +0 -237
  47. package/abap/zcl_abgagt_command_tree.clas.xml +0 -15
  48. package/abap/zcl_abgagt_command_unit.clas.abap +0 -297
  49. package/abap/zcl_abgagt_command_unit.clas.xml +0 -15
  50. package/abap/zcl_abgagt_command_view.clas.abap +0 -240
  51. package/abap/zcl_abgagt_command_view.clas.xml +0 -15
  52. package/abap/zcl_abgagt_resource_create.clas.abap +0 -71
  53. package/abap/zcl_abgagt_resource_create.clas.xml +0 -15
  54. package/abap/zcl_abgagt_resource_health.clas.abap +0 -25
  55. package/abap/zcl_abgagt_resource_health.clas.xml +0 -15
  56. package/abap/zcl_abgagt_resource_import.clas.abap +0 -66
  57. package/abap/zcl_abgagt_resource_import.clas.xml +0 -15
  58. package/abap/zcl_abgagt_resource_inspect.clas.abap +0 -63
  59. package/abap/zcl_abgagt_resource_inspect.clas.xml +0 -15
  60. package/abap/zcl_abgagt_resource_preview.clas.abap +0 -67
  61. package/abap/zcl_abgagt_resource_preview.clas.xml +0 -15
  62. package/abap/zcl_abgagt_resource_pull.clas.abap +0 -71
  63. package/abap/zcl_abgagt_resource_pull.clas.xml +0 -15
  64. package/abap/zcl_abgagt_resource_tree.clas.abap +0 -70
  65. package/abap/zcl_abgagt_resource_tree.clas.xml +0 -15
  66. package/abap/zcl_abgagt_resource_unit.clas.abap +0 -64
  67. package/abap/zcl_abgagt_resource_unit.clas.xml +0 -15
  68. package/abap/zcl_abgagt_resource_view.clas.abap +0 -68
  69. package/abap/zcl_abgagt_resource_view.clas.xml +0 -15
  70. package/abap/zcl_abgagt_rest_handler.clas.abap +0 -32
  71. package/abap/zcl_abgagt_rest_handler.clas.xml +0 -15
  72. package/abap/zcl_abgagt_util.clas.abap +0 -93
  73. package/abap/zcl_abgagt_util.clas.testclasses.abap +0 -84
  74. package/abap/zcl_abgagt_util.clas.xml +0 -16
  75. package/abap/zcl_abgagt_viewer_clas.clas.abap +0 -58
  76. package/abap/zcl_abgagt_viewer_clas.clas.xml +0 -15
  77. package/abap/zcl_abgagt_viewer_ddls.clas.abap +0 -83
  78. package/abap/zcl_abgagt_viewer_ddls.clas.xml +0 -15
  79. package/abap/zcl_abgagt_viewer_dtel.clas.abap +0 -98
  80. package/abap/zcl_abgagt_viewer_dtel.clas.xml +0 -15
  81. package/abap/zcl_abgagt_viewer_factory.clas.abap +0 -41
  82. package/abap/zcl_abgagt_viewer_factory.clas.xml +0 -15
  83. package/abap/zcl_abgagt_viewer_intf.clas.abap +0 -58
  84. package/abap/zcl_abgagt_viewer_intf.clas.xml +0 -15
  85. package/abap/zcl_abgagt_viewer_stru.clas.abap +0 -59
  86. package/abap/zcl_abgagt_viewer_stru.clas.xml +0 -15
  87. package/abap/zcl_abgagt_viewer_tabl.clas.abap +0 -59
  88. package/abap/zcl_abgagt_viewer_tabl.clas.xml +0 -15
  89. package/abap/zcl_abgagt_viewer_ttyp.clas.abap +0 -93
  90. package/abap/zcl_abgagt_viewer_ttyp.clas.xml +0 -15
  91. package/abap/zif_abgagt_agent.intf.abap +0 -53
  92. package/abap/zif_abgagt_agent.intf.xml +0 -15
  93. package/abap/zif_abgagt_cmd_factory.intf.abap +0 -7
  94. package/abap/zif_abgagt_cmd_factory.intf.xml +0 -15
  95. package/abap/zif_abgagt_command.intf.abap +0 -26
  96. package/abap/zif_abgagt_command.intf.xml +0 -15
  97. package/abap/zif_abgagt_util.intf.abap +0 -28
  98. package/abap/zif_abgagt_util.intf.xml +0 -15
  99. package/abap/zif_abgagt_viewer.intf.abap +0 -12
  100. package/abap/zif_abgagt_viewer.intf.xml +0 -15
  101. package/docs/commands.md +0 -142
  102. package/docs/create-command.md +0 -129
  103. package/docs/health-command.md +0 -89
  104. package/docs/import-command.md +0 -195
  105. package/docs/init-command.md +0 -189
  106. package/docs/inspect-command.md +0 -169
  107. package/docs/list-command.md +0 -289
  108. package/docs/preview-command.md +0 -528
  109. package/docs/pull-command.md +0 -202
  110. package/docs/status-command.md +0 -68
  111. package/docs/tree-command.md +0 -303
  112. package/docs/unit-command.md +0 -167
  113. package/docs/view-command.md +0 -501
  114. package/img/claude.png +0 -0
  115. package/scripts/claude-integration.js +0 -351
  116. package/scripts/release.js +0 -298
  117. package/scripts/release.sh +0 -60
  118. package/scripts/test-integration.js +0 -139
  119. package/scripts/unrelease.js +0 -277
@@ -0,0 +1,1037 @@
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 and local guidelines
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
+ const guidelinesFolder = detectGuidelinesFolder();
268
+
269
+ // If neither reference folder nor guidelines exist, return error
270
+ if (!refFolder && !guidelinesFolder) {
271
+ return {
272
+ error: 'Reference folder not found',
273
+ hint: 'Configure referenceFolder in .abapGitAgent, clone to ~/abap-reference, or create abap/guidelines/ folder'
274
+ };
275
+ }
276
+
277
+ const results = {
278
+ pattern,
279
+ referenceFolder: refFolder,
280
+ guidelinesFolder: guidelinesFolder,
281
+ repositories: repos.map(r => r.name),
282
+ files: [],
283
+ matches: []
284
+ };
285
+
286
+ try {
287
+ // Search reference repositories if available
288
+ if (repos.length > 0) {
289
+ for (const repo of repos) {
290
+ const searchableFiles = await getSearchableFiles(repo.path, repo.name);
291
+
292
+ for (const fileInfo of searchableFiles) {
293
+ try {
294
+ const content = await readFile(fileInfo.path, 'utf8');
295
+
296
+ if (content.toLowerCase().includes(pattern.toLowerCase())) {
297
+ results.files.push({
298
+ repo: repo.name,
299
+ file: fileInfo.relativePath
300
+ });
301
+
302
+ // Find matching lines with context
303
+ const lines = content.split('\n');
304
+ let matchCount = 0;
305
+
306
+ for (let i = 0; i < lines.length; i++) {
307
+ if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
308
+ const start = Math.max(0, i - 1);
309
+ const end = Math.min(lines.length, i + 2);
310
+ const context = lines.slice(start, end).join('\n');
311
+
312
+ results.matches.push({
313
+ repo: repo.name,
314
+ file: fileInfo.relativePath,
315
+ line: i + 1,
316
+ context
317
+ });
318
+
319
+ matchCount++;
320
+
321
+ // Limit matches per file to avoid overwhelming output
322
+ if (matchCount >= 3) {
323
+ break;
324
+ }
325
+ }
326
+ }
327
+ }
328
+ } catch (error) {
329
+ // Skip files we can't read
330
+ }
331
+ }
332
+ }
333
+ }
334
+
335
+ // Search local guidelines folder if available
336
+ if (guidelinesFolder) {
337
+ const guidelineFiles = await getGuidelineFiles();
338
+
339
+ for (const file of guidelineFiles) {
340
+ if (file.content.toLowerCase().includes(pattern.toLowerCase())) {
341
+ results.files.push({
342
+ repo: 'guidelines',
343
+ file: file.relativePath
344
+ });
345
+
346
+ // Find matching lines with context
347
+ const lines = file.content.split('\n');
348
+ let matchCount = 0;
349
+
350
+ for (let i = 0; i < lines.length; i++) {
351
+ if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
352
+ const start = Math.max(0, i - 1);
353
+ const end = Math.min(lines.length, i + 2);
354
+ const context = lines.slice(start, end).join('\n');
355
+
356
+ results.matches.push({
357
+ repo: 'guidelines',
358
+ file: file.relativePath,
359
+ line: i + 1,
360
+ context
361
+ });
362
+
363
+ matchCount++;
364
+
365
+ // Limit matches per file to avoid overwhelming output
366
+ if (matchCount >= 3) {
367
+ break;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ return results;
376
+ } catch (error) {
377
+ return {
378
+ error: `Search failed: ${error.message}`
379
+ };
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Get content for a specific topic
385
+ * @param {string} topic - Topic name
386
+ * @returns {Promise<Object>} Topic content
387
+ */
388
+ async function getTopic(topic) {
389
+ const topicLower = topic.toLowerCase();
390
+
391
+ // First, check local guidelines folder
392
+ const guidelinesDir = detectGuidelinesFolder();
393
+ if (guidelinesDir) {
394
+ // Map topic to local guideline file
395
+ const guidelineMap = {
396
+ 'abapgit': '08_abapgit.md',
397
+ 'xml': '06_objects.md',
398
+ 'objects': '06_objects.md',
399
+ 'naming': '06_objects.md'
400
+ };
401
+
402
+ const guidelineFile = guidelineMap[topicLower];
403
+ if (guidelineFile) {
404
+ const guidelinePath = path.join(guidelinesDir, guidelineFile);
405
+ if (fs.existsSync(guidelinePath)) {
406
+ try {
407
+ const content = await readFile(guidelinePath, 'utf8');
408
+ return {
409
+ topic,
410
+ file: guidelineFile,
411
+ content: content.slice(0, 5000),
412
+ truncated: content.length > 5000,
413
+ totalLength: content.length,
414
+ source: 'guidelines'
415
+ };
416
+ } catch (error) {
417
+ // Continue to check external reference
418
+ }
419
+ }
420
+ }
421
+ }
422
+
423
+ // Fall back to external reference folder
424
+ const cheatSheetsDir = getCheatSheetsDir();
425
+
426
+ if (!cheatSheetsDir) {
427
+ return {
428
+ error: 'Reference folder not found',
429
+ hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
430
+ };
431
+ }
432
+
433
+ const fileName = TOPIC_MAP[topicLower];
434
+ if (!fileName) {
435
+ return {
436
+ error: `Unknown topic: ${topic}`,
437
+ availableTopics: Object.keys(TOPIC_MAP).filter((v, i, a) => a.indexOf(v) === i).slice(0, 20)
438
+ };
439
+ }
440
+
441
+ const filePath = path.join(cheatSheetsDir, fileName);
442
+
443
+ if (!fs.existsSync(filePath)) {
444
+ return {
445
+ error: `File not found: ${fileName}`
446
+ };
447
+ }
448
+
449
+ try {
450
+ const content = await readFile(filePath, 'utf8');
451
+ return {
452
+ topic,
453
+ file: fileName,
454
+ content: content.slice(0, 5000), // First 5000 chars
455
+ truncated: content.length > 5000,
456
+ totalLength: content.length
457
+ };
458
+ } catch (error) {
459
+ return {
460
+ error: `Failed to read topic: ${error.message}`
461
+ };
462
+ }
463
+ }
464
+
465
+ /**
466
+ * List available topics
467
+ * @returns {Promise<Object>} List of topics
468
+ */
469
+ async function listTopics() {
470
+ const cheatSheetsDir = getCheatSheetsDir();
471
+
472
+ if (!cheatSheetsDir) {
473
+ return {
474
+ error: 'Reference folder not found',
475
+ hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
476
+ };
477
+ }
478
+
479
+ // Build topic list from files that exist
480
+ const topics = [];
481
+ const seenFiles = new Set();
482
+
483
+ for (const [topic, file] of Object.entries(TOPIC_MAP)) {
484
+ if (!seenFiles.has(file) && fs.existsSync(path.join(cheatSheetsDir, file))) {
485
+ topics.push({ topic, file });
486
+ seenFiles.add(file);
487
+ }
488
+ }
489
+
490
+ return {
491
+ referenceFolder: path.dirname(cheatSheetsDir),
492
+ topics: topics.sort((a, b) => a.file.localeCompare(b.file))
493
+ };
494
+ }
495
+
496
+ /**
497
+ * Display search results in console format
498
+ * @param {Object} results - Search results
499
+ */
500
+ function displaySearchResults(results) {
501
+ if (results.error) {
502
+ console.error(`\n ❌ ${results.error}`);
503
+ if (results.hint) {
504
+ console.error(`\n 💡 ${results.hint}`);
505
+ }
506
+ return;
507
+ }
508
+
509
+ console.log(`\n 🔍 Searching for: '${results.pattern}'`);
510
+
511
+ // Show which sources were searched
512
+ const sources = [];
513
+ if (results.referenceFolder) {
514
+ sources.push('reference repositories');
515
+ }
516
+ if (results.guidelinesFolder) {
517
+ sources.push('local guidelines');
518
+ }
519
+ console.log(` 📁 Sources searched: ${sources.join(', ') || 'none'}`);
520
+
521
+ if (results.repositories && results.repositories.length > 0) {
522
+ console.log(` 📚 Repositories (${results.repositories.length}): ${results.repositories.join(', ')}`);
523
+ }
524
+ console.log('');
525
+
526
+ if (results.files.length === 0) {
527
+ console.log(' ⚠️ No matches found.');
528
+ return;
529
+ }
530
+
531
+ // Group files by repository
532
+ const filesByRepo = {};
533
+ results.files.forEach(fileInfo => {
534
+ const repo = fileInfo.repo || 'unknown';
535
+ if (!filesByRepo[repo]) filesByRepo[repo] = [];
536
+ filesByRepo[repo].push(fileInfo.file);
537
+ });
538
+
539
+ console.log(` ✅ Found in ${results.files.length} file(s):`);
540
+ for (const [repo, files] of Object.entries(filesByRepo)) {
541
+ const icon = repo === 'guidelines' ? '📋' : '📦';
542
+ console.log(`\n ${icon} ${repo}/`);
543
+ files.forEach(file => {
544
+ console.log(` • ${file}`);
545
+ });
546
+ }
547
+ console.log('');
548
+
549
+ // Show first 5 matches
550
+ console.log(' 📄 Preview (first 5 matches):');
551
+ console.log(' ' + '─'.repeat(60));
552
+
553
+ const uniqueMatches = [];
554
+ const seenContexts = new Set();
555
+
556
+ for (const match of results.matches) {
557
+ const key = `${match.repo}:${match.file}:${match.context}`;
558
+ if (!seenContexts.has(key)) {
559
+ uniqueMatches.push(match);
560
+ seenContexts.add(key);
561
+ }
562
+ if (uniqueMatches.length >= 5) break;
563
+ }
564
+
565
+ for (const match of uniqueMatches) {
566
+ console.log(` 📄 ${match.repo}/${match.file} (line ${match.line}):`);
567
+ const lines = match.context.split('\n');
568
+ lines.forEach((line, idx) => {
569
+ const prefix = idx === 1 ? ' → ' : ' ';
570
+ const trimmed = line.slice(0, 80);
571
+ console.log(`${prefix}${trimmed}`);
572
+ });
573
+ console.log('');
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Display topic content in console format
579
+ * @param {Object} result - Topic result
580
+ */
581
+ function displayTopic(result) {
582
+ if (result.error) {
583
+ console.error(`\n ❌ ${result.error}`);
584
+ if (result.hint) {
585
+ console.error(`\n 💡 ${result.hint}`);
586
+ }
587
+ if (result.availableTopics) {
588
+ console.error(`\n Available topics: ${result.availableTopics.join(', ')}`);
589
+ }
590
+ return;
591
+ }
592
+
593
+ console.log(`\n 📖 ${result.file}`);
594
+ console.log(' ' + '─'.repeat(60));
595
+ console.log('');
596
+
597
+ // Display first 100 lines
598
+ const lines = result.content.split('\n').slice(0, 100);
599
+ lines.forEach(line => {
600
+ const trimmed = line.slice(0, 100);
601
+ console.log(` ${trimmed}`);
602
+ });
603
+
604
+ if (result.truncated) {
605
+ console.log('');
606
+ console.log(` ... (${result.totalLength - result.content.length} more characters)`);
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Display topic list in console format
612
+ * @param {Object} result - Topics result
613
+ */
614
+ function displayTopics(result) {
615
+ if (result.error) {
616
+ console.error(`\n ❌ ${result.error}`);
617
+ if (result.hint) {
618
+ console.error(`\n 💡 ${result.hint}`);
619
+ }
620
+ return;
621
+ }
622
+
623
+ console.log(`\n 📚 Available ABAP Reference Topics`);
624
+ console.log(` 📁 Reference folder: ${result.referenceFolder}`);
625
+ console.log('');
626
+ console.log(' Topic File');
627
+ console.log(' ' + '─'.repeat(60));
628
+
629
+ result.topics.forEach(({ topic, file }) => {
630
+ const paddedTopic = topic.padEnd(20);
631
+ console.log(` ${paddedTopic} ${file}`);
632
+ });
633
+ }
634
+
635
+ /**
636
+ * List all reference repositories
637
+ * @returns {Promise<Object>} List of repositories
638
+ */
639
+ async function listRepositories() {
640
+ const refFolder = detectReferenceFolder();
641
+ const repos = await getReferenceRepositories();
642
+
643
+ if (!refFolder) {
644
+ return {
645
+ error: 'Reference folder not found',
646
+ hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
647
+ };
648
+ }
649
+
650
+ return {
651
+ referenceFolder: refFolder,
652
+ repositories: repos
653
+ };
654
+ }
655
+
656
+ /**
657
+ * Display repositories in console format
658
+ * @param {Object} result - Repositories result
659
+ */
660
+ function displayRepositories(result) {
661
+ if (result.error) {
662
+ console.error(`\n ❌ ${result.error}`);
663
+ if (result.hint) {
664
+ console.error(`\n 💡 ${result.hint}`);
665
+ }
666
+ return;
667
+ }
668
+
669
+ console.log(`\n 📚 ABAP Reference Repositories`);
670
+ console.log(` 📁 Reference folder: ${result.referenceFolder}`);
671
+ console.log('');
672
+
673
+ if (result.repositories.length === 0) {
674
+ console.log(' ⚠️ No repositories found.');
675
+ console.log('');
676
+ console.log(' Add repositories by cloning them to the reference folder:');
677
+ console.log(' cd ' + result.referenceFolder);
678
+ console.log(' git clone <repo-url>');
679
+ return;
680
+ }
681
+
682
+ console.log(` Found ${result.repositories.length} repository(ies):`);
683
+ console.log('');
684
+ console.log(' Repository Type');
685
+ console.log(' ' + '─'.repeat(50));
686
+
687
+ result.repositories.forEach(repo => {
688
+ let type;
689
+ if (repo.isCustomGuidelines) {
690
+ type = 'Custom Guidelines';
691
+ } else if (repo.isGitRepo) {
692
+ type = 'Git Repo';
693
+ } else {
694
+ type = 'ABAP Folder';
695
+ }
696
+ const paddedName = repo.name.padEnd(30);
697
+ console.log(` ${paddedName} ${type}`);
698
+ });
699
+ }
700
+
701
+ /**
702
+ * Detect guidelines folder in current project
703
+ * Looks for abap/guidelines/ folder
704
+ * @returns {string|null} Path to guidelines folder or null if not found
705
+ */
706
+ function detectGuidelinesFolder() {
707
+ const cwd = process.cwd();
708
+ const possiblePaths = [
709
+ path.join(cwd, 'abap', 'guidelines'),
710
+ path.join(cwd, 'guidelines'),
711
+ path.join(cwd, 'docs', 'guidelines')
712
+ ];
713
+
714
+ for (const guidelinesPath of possiblePaths) {
715
+ if (fs.existsSync(guidelinesPath)) {
716
+ const stats = fs.statSync(guidelinesPath);
717
+ if (stats.isDirectory()) {
718
+ return guidelinesPath;
719
+ }
720
+ }
721
+ }
722
+
723
+ return null;
724
+ }
725
+
726
+ /**
727
+ * Get the path to the built-in guidelines in abapgit-agent
728
+ * @returns {string|null} Path to built-in guidelines or null if not found
729
+ */
730
+ function getBuiltInGuidelinesPath() {
731
+ // Try to find the guidelines relative to this module
732
+ // This allows the CLI to work from any location
733
+ const possiblePaths = [
734
+ // When running from source (bin/abapgit-agent)
735
+ path.join(__dirname, '..', 'abap', 'guidelines'),
736
+ // When running from installed package
737
+ path.join(__dirname, '..', '..', 'abap', 'guidelines')
738
+ ];
739
+
740
+ for (const guidelinesPath of possiblePaths) {
741
+ if (fs.existsSync(guidelinesPath)) {
742
+ const stats = fs.statSync(guidelinesPath);
743
+ if (stats.isDirectory()) {
744
+ return guidelinesPath;
745
+ }
746
+ }
747
+ }
748
+
749
+ return null;
750
+ }
751
+
752
+ /**
753
+ * Initialize guidelines in current project by copying from abapgit-agent
754
+ * @returns {Object} Init result
755
+ */
756
+ function initGuidelines() {
757
+ const builtInPath = getBuiltInGuidelinesPath();
758
+
759
+ if (!builtInPath) {
760
+ return {
761
+ success: false,
762
+ error: 'Built-in guidelines not found',
763
+ hint: 'Make sure abapgit-agent is properly installed'
764
+ };
765
+ }
766
+
767
+ const cwd = process.cwd();
768
+ const targetPath = path.join(cwd, 'abap', 'guidelines');
769
+
770
+ // Check if guidelines already exist
771
+ if (fs.existsSync(targetPath)) {
772
+ return {
773
+ success: false,
774
+ error: 'Guidelines folder already exists',
775
+ hint: `Delete or rename '${targetPath}' first, then run --init again`,
776
+ existingPath: targetPath
777
+ };
778
+ }
779
+
780
+ try {
781
+ // Create target directory
782
+ fs.mkdirSync(targetPath, { recursive: true });
783
+
784
+ // Copy all files from built-in guidelines
785
+ const files = fs.readdirSync(builtInPath);
786
+ let copied = 0;
787
+
788
+ for (const file of files) {
789
+ if (file.endsWith('.md')) {
790
+ const srcPath = path.join(builtInPath, file);
791
+ const destPath = path.join(targetPath, file);
792
+ const content = fs.readFileSync(srcPath, 'utf8');
793
+ fs.writeFileSync(destPath, content);
794
+ copied++;
795
+ }
796
+ }
797
+
798
+ if (copied === 0) {
799
+ return {
800
+ success: false,
801
+ error: 'No guideline files found in built-in guidelines'
802
+ };
803
+ }
804
+
805
+ return {
806
+ success: true,
807
+ message: `Initialized ${copied} guideline(s) in your project`,
808
+ sourceFolder: builtInPath,
809
+ targetFolder: targetPath,
810
+ files: files.filter(f => f.endsWith('.md'))
811
+ };
812
+ } catch (error) {
813
+ return {
814
+ success: false,
815
+ error: `Failed to initialize: ${error.message}`
816
+ };
817
+ }
818
+ }
819
+
820
+ /**
821
+ * Get all guideline files from the project
822
+ * @returns {Promise<Array<{name: string, path: string, content: string}>>}
823
+ */
824
+ async function getGuidelineFiles() {
825
+ const guidelinesFolder = detectGuidelinesFolder();
826
+ if (!guidelinesFolder) {
827
+ return [];
828
+ }
829
+
830
+ const files = [];
831
+
832
+ try {
833
+ const entries = await readdir(guidelinesFolder);
834
+
835
+ for (const entry of entries) {
836
+ if (entry.endsWith('.md')) {
837
+ const fullPath = path.join(guidelinesFolder, entry);
838
+ const content = await readFile(fullPath, 'utf8');
839
+ files.push({
840
+ name: entry,
841
+ path: fullPath,
842
+ content,
843
+ relativePath: path.join('guidelines', entry)
844
+ });
845
+ }
846
+ }
847
+ } catch (error) {
848
+ // Return empty array on error
849
+ }
850
+
851
+ return files.sort((a, b) => a.name.localeCompare(b.name));
852
+ }
853
+
854
+ /**
855
+ * Export guidelines to reference folder
856
+ * Copies guideline files to the reference folder for searching
857
+ * @returns {Promise<Object>} Export result
858
+ */
859
+ async function exportGuidelines() {
860
+ const guidelinesFolder = detectGuidelinesFolder();
861
+ const refFolder = detectReferenceFolder();
862
+
863
+ if (!guidelinesFolder) {
864
+ return {
865
+ success: false,
866
+ error: 'No guidelines folder found',
867
+ hint: 'Create abap/guidelines/ folder in your project'
868
+ };
869
+ }
870
+
871
+ if (!refFolder) {
872
+ return {
873
+ success: false,
874
+ error: 'Reference folder not found',
875
+ hint: 'Configure referenceFolder in .abapGitAgent or clone to ~/abap-reference'
876
+ };
877
+ }
878
+
879
+ const guidelineFiles = await getGuidelineFiles();
880
+
881
+ if (guidelineFiles.length === 0) {
882
+ return {
883
+ success: false,
884
+ error: 'No guideline files found',
885
+ hint: 'Create .md files in abap/guidelines/'
886
+ };
887
+ }
888
+
889
+ // Create a dedicated folder in reference for guidelines
890
+ const exportPath = path.join(refFolder, 'custom-guidelines');
891
+
892
+ try {
893
+ // Create export directory if it doesn't exist
894
+ if (!fs.existsSync(exportPath)) {
895
+ fs.mkdirSync(exportPath, { recursive: true });
896
+ }
897
+
898
+ let exported = 0;
899
+
900
+ for (const file of guidelineFiles) {
901
+ const destPath = path.join(exportPath, file.name);
902
+ fs.writeFileSync(destPath, file.content);
903
+ exported++;
904
+ }
905
+
906
+ return {
907
+ success: true,
908
+ message: `Exported ${exported} guideline(s) to reference folder`,
909
+ sourceFolder: guidelinesFolder,
910
+ exportFolder: exportPath,
911
+ files: guidelineFiles.map(f => f.name)
912
+ };
913
+ } catch (error) {
914
+ return {
915
+ success: false,
916
+ error: `Failed to export: ${error.message}`
917
+ };
918
+ }
919
+ }
920
+
921
+ /**
922
+ * Search guidelines in current project
923
+ * @param {string} pattern - Pattern to search for
924
+ * @returns {Promise<Object>} Search results
925
+ */
926
+ async function searchGuidelines(pattern) {
927
+ const guidelineFiles = await getGuidelineFiles();
928
+
929
+ if (guidelineFiles.length === 0) {
930
+ return {
931
+ pattern,
932
+ guidelinesFound: false,
933
+ message: 'No guideline files found in project'
934
+ };
935
+ }
936
+
937
+ const results = {
938
+ pattern,
939
+ guidelinesFound: true,
940
+ files: [],
941
+ matches: []
942
+ };
943
+
944
+ for (const file of guidelineFiles) {
945
+ if (file.content.toLowerCase().includes(pattern.toLowerCase())) {
946
+ results.files.push(file.relativePath);
947
+
948
+ // Find matching lines with context
949
+ const lines = file.content.split('\n');
950
+
951
+ for (let i = 0; i < lines.length; i++) {
952
+ if (lines[i].toLowerCase().includes(pattern.toLowerCase())) {
953
+ const start = Math.max(0, i - 1);
954
+ const end = Math.min(lines.length, i + 2);
955
+ const context = lines.slice(start, end).join('\n');
956
+
957
+ results.matches.push({
958
+ file: file.relativePath,
959
+ line: i + 1,
960
+ context
961
+ });
962
+ }
963
+ }
964
+ }
965
+ }
966
+
967
+ return results;
968
+ }
969
+
970
+ /**
971
+ * Display export result
972
+ * @param {Object} result - Export result
973
+ */
974
+ function displayExportResult(result) {
975
+ if (result.success) {
976
+ console.log(`\n ✅ ${result.message}`);
977
+ console.log(`\n 📁 Source: ${result.sourceFolder}`);
978
+ console.log(` 📁 Export: ${result.exportFolder}`);
979
+ console.log(`\n Files exported:`);
980
+ for (const file of result.files) {
981
+ console.log(` - ${file}`);
982
+ }
983
+ console.log(`\n 💡 These guidelines will now be searchable via 'ref' command`);
984
+ } else {
985
+ console.error(`\n ❌ ${result.error}`);
986
+ if (result.hint) {
987
+ console.error(`\n 💡 ${result.hint}`);
988
+ }
989
+ }
990
+ }
991
+
992
+ /**
993
+ * Display init result
994
+ * @param {Object} result - Init result
995
+ */
996
+ function displayInitResult(result) {
997
+ if (result.success) {
998
+ console.log(`\n ✅ ${result.message}`);
999
+ console.log(`\n 📁 Source: ${result.sourceFolder}`);
1000
+ console.log(` 📁 Created: ${result.targetFolder}`);
1001
+ console.log(`\n Files copied:`);
1002
+ for (const file of result.files) {
1003
+ console.log(` - ${file}`);
1004
+ }
1005
+ console.log(`\n 💡 Next steps:`);
1006
+ console.log(` 1. Review the guidelines in abap/guidelines/`);
1007
+ console.log(` 2. Customize as needed for your project`);
1008
+ console.log(` 3. Run 'abapgit-agent ref --export' to make them searchable`);
1009
+ } else {
1010
+ console.error(`\n ❌ ${result.error}`);
1011
+ if (result.hint) {
1012
+ console.error(`\n 💡 ${result.hint}`);
1013
+ }
1014
+ }
1015
+ }
1016
+
1017
+ module.exports = {
1018
+ detectReferenceFolder,
1019
+ detectGuidelinesFolder,
1020
+ getBuiltInGuidelinesPath,
1021
+ initGuidelines,
1022
+ getReferenceRepositories,
1023
+ getGuidelineFiles,
1024
+ searchPattern,
1025
+ searchGuidelines,
1026
+ getTopic,
1027
+ listTopics,
1028
+ listRepositories,
1029
+ exportGuidelines,
1030
+ displaySearchResults,
1031
+ displayTopic,
1032
+ displayTopics,
1033
+ displayRepositories,
1034
+ displayExportResult,
1035
+ displayInitResult,
1036
+ TOPIC_MAP
1037
+ };