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.
- package/README.md +1 -0
- package/abap/guidelines/00_index.md +36 -0
- package/abap/guidelines/01_sql.md +88 -0
- package/abap/guidelines/02_exceptions.md +176 -0
- package/abap/guidelines/03_testing.md +269 -0
- package/abap/guidelines/04_cds.md +136 -0
- package/abap/guidelines/05_classes.md +58 -0
- package/abap/guidelines/06_objects.md +110 -0
- package/abap/guidelines/07_json.md +24 -0
- package/abap/guidelines/08_abapgit.md +222 -0
- package/abap/guidelines/09_unit_testable_code.md +568 -0
- package/bin/abapgit-agent +513 -38
- package/bin/abgagt +24 -0
- package/package.json +8 -2
- package/src/abap-client.js +65 -2
- package/src/agent.js +57 -3
- package/src/config.js +1 -1
- package/src/ref-search.js +1037 -0
- package/.abapGitAgent.example +0 -11
- package/.github/workflows/release.yml +0 -60
- package/API.md +0 -710
- package/CLAUDE.md +0 -1058
- package/CLAUDE_MEM.md +0 -88
- package/ERROR_HANDLING.md +0 -30
- package/INSTALL.md +0 -155
- package/RELEASE_NOTES.md +0 -143
- package/abap/CLAUDE.md +0 -1010
- package/abap/copilot-instructions.md +0 -79
- package/abap/package.devc.xml +0 -10
- package/abap/zcl_abgagt_agent.clas.abap +0 -420
- package/abap/zcl_abgagt_agent.clas.xml +0 -15
- package/abap/zcl_abgagt_cmd_factory.clas.abap +0 -48
- package/abap/zcl_abgagt_cmd_factory.clas.xml +0 -15
- package/abap/zcl_abgagt_command_create.clas.abap +0 -95
- package/abap/zcl_abgagt_command_create.clas.xml +0 -15
- package/abap/zcl_abgagt_command_import.clas.abap +0 -138
- package/abap/zcl_abgagt_command_import.clas.xml +0 -15
- package/abap/zcl_abgagt_command_inspect.clas.abap +0 -456
- package/abap/zcl_abgagt_command_inspect.clas.testclasses.abap +0 -121
- package/abap/zcl_abgagt_command_inspect.clas.xml +0 -16
- package/abap/zcl_abgagt_command_preview.clas.abap +0 -386
- package/abap/zcl_abgagt_command_preview.clas.xml +0 -15
- package/abap/zcl_abgagt_command_pull.clas.abap +0 -80
- package/abap/zcl_abgagt_command_pull.clas.testclasses.abap +0 -87
- package/abap/zcl_abgagt_command_pull.clas.xml +0 -16
- package/abap/zcl_abgagt_command_tree.clas.abap +0 -237
- package/abap/zcl_abgagt_command_tree.clas.xml +0 -15
- package/abap/zcl_abgagt_command_unit.clas.abap +0 -297
- package/abap/zcl_abgagt_command_unit.clas.xml +0 -15
- package/abap/zcl_abgagt_command_view.clas.abap +0 -240
- package/abap/zcl_abgagt_command_view.clas.xml +0 -15
- package/abap/zcl_abgagt_resource_create.clas.abap +0 -71
- package/abap/zcl_abgagt_resource_create.clas.xml +0 -15
- package/abap/zcl_abgagt_resource_health.clas.abap +0 -25
- package/abap/zcl_abgagt_resource_health.clas.xml +0 -15
- package/abap/zcl_abgagt_resource_import.clas.abap +0 -66
- package/abap/zcl_abgagt_resource_import.clas.xml +0 -15
- package/abap/zcl_abgagt_resource_inspect.clas.abap +0 -63
- package/abap/zcl_abgagt_resource_inspect.clas.xml +0 -15
- package/abap/zcl_abgagt_resource_preview.clas.abap +0 -67
- package/abap/zcl_abgagt_resource_preview.clas.xml +0 -15
- package/abap/zcl_abgagt_resource_pull.clas.abap +0 -71
- package/abap/zcl_abgagt_resource_pull.clas.xml +0 -15
- package/abap/zcl_abgagt_resource_tree.clas.abap +0 -70
- package/abap/zcl_abgagt_resource_tree.clas.xml +0 -15
- package/abap/zcl_abgagt_resource_unit.clas.abap +0 -64
- package/abap/zcl_abgagt_resource_unit.clas.xml +0 -15
- package/abap/zcl_abgagt_resource_view.clas.abap +0 -68
- package/abap/zcl_abgagt_resource_view.clas.xml +0 -15
- package/abap/zcl_abgagt_rest_handler.clas.abap +0 -32
- package/abap/zcl_abgagt_rest_handler.clas.xml +0 -15
- package/abap/zcl_abgagt_util.clas.abap +0 -93
- package/abap/zcl_abgagt_util.clas.testclasses.abap +0 -84
- package/abap/zcl_abgagt_util.clas.xml +0 -16
- package/abap/zcl_abgagt_viewer_clas.clas.abap +0 -58
- package/abap/zcl_abgagt_viewer_clas.clas.xml +0 -15
- package/abap/zcl_abgagt_viewer_ddls.clas.abap +0 -83
- package/abap/zcl_abgagt_viewer_ddls.clas.xml +0 -15
- package/abap/zcl_abgagt_viewer_dtel.clas.abap +0 -98
- package/abap/zcl_abgagt_viewer_dtel.clas.xml +0 -15
- package/abap/zcl_abgagt_viewer_factory.clas.abap +0 -41
- package/abap/zcl_abgagt_viewer_factory.clas.xml +0 -15
- package/abap/zcl_abgagt_viewer_intf.clas.abap +0 -58
- package/abap/zcl_abgagt_viewer_intf.clas.xml +0 -15
- package/abap/zcl_abgagt_viewer_stru.clas.abap +0 -59
- package/abap/zcl_abgagt_viewer_stru.clas.xml +0 -15
- package/abap/zcl_abgagt_viewer_tabl.clas.abap +0 -59
- package/abap/zcl_abgagt_viewer_tabl.clas.xml +0 -15
- package/abap/zcl_abgagt_viewer_ttyp.clas.abap +0 -93
- package/abap/zcl_abgagt_viewer_ttyp.clas.xml +0 -15
- package/abap/zif_abgagt_agent.intf.abap +0 -53
- package/abap/zif_abgagt_agent.intf.xml +0 -15
- package/abap/zif_abgagt_cmd_factory.intf.abap +0 -7
- package/abap/zif_abgagt_cmd_factory.intf.xml +0 -15
- package/abap/zif_abgagt_command.intf.abap +0 -26
- package/abap/zif_abgagt_command.intf.xml +0 -15
- package/abap/zif_abgagt_util.intf.abap +0 -28
- package/abap/zif_abgagt_util.intf.xml +0 -15
- package/abap/zif_abgagt_viewer.intf.abap +0 -12
- package/abap/zif_abgagt_viewer.intf.xml +0 -15
- package/docs/commands.md +0 -142
- package/docs/create-command.md +0 -129
- package/docs/health-command.md +0 -89
- package/docs/import-command.md +0 -195
- package/docs/init-command.md +0 -189
- package/docs/inspect-command.md +0 -169
- package/docs/list-command.md +0 -289
- package/docs/preview-command.md +0 -528
- package/docs/pull-command.md +0 -202
- package/docs/status-command.md +0 -68
- package/docs/tree-command.md +0 -303
- package/docs/unit-command.md +0 -167
- package/docs/view-command.md +0 -501
- package/img/claude.png +0 -0
- package/scripts/claude-integration.js +0 -351
- package/scripts/release.js +0 -298
- package/scripts/release.sh +0 -60
- package/scripts/test-integration.js +0 -139
- 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
|
+
};
|