abapgit-agent 1.7.1 → 1.8.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.
- package/.abapGitAgent.example +11 -0
- package/README.md +7 -7
- package/abap/.github/copilot-instructions.md +254 -0
- package/abap/CLAUDE.md +432 -0
- package/abap/guidelines/00_index.md +8 -0
- package/abap/guidelines/01_sql.md +8 -0
- package/abap/guidelines/02_exceptions.md +8 -0
- package/abap/guidelines/03_testing.md +8 -0
- package/abap/guidelines/04_cds.md +8 -0
- package/abap/guidelines/05_classes.md +8 -0
- package/abap/guidelines/06_objects.md +8 -0
- package/abap/guidelines/07_json.md +8 -0
- package/abap/guidelines/08_abapgit.md +8 -0
- package/abap/guidelines/09_unit_testable_code.md +8 -0
- package/bin/abapgit-agent +61 -2789
- package/package.json +25 -5
- package/src/agent.js +213 -20
- package/src/commands/create.js +102 -0
- package/src/commands/delete.js +72 -0
- package/src/commands/health.js +24 -0
- package/src/commands/help.js +111 -0
- package/src/commands/import.js +99 -0
- package/src/commands/init.js +321 -0
- package/src/commands/inspect.js +184 -0
- package/src/commands/list.js +143 -0
- package/src/commands/preview.js +277 -0
- package/src/commands/pull.js +278 -0
- package/src/commands/ref.js +96 -0
- package/src/commands/status.js +52 -0
- package/src/commands/syntax.js +290 -0
- package/src/commands/tree.js +209 -0
- package/src/commands/unit.js +133 -0
- package/src/commands/view.js +215 -0
- package/src/commands/where.js +138 -0
- package/src/config.js +11 -1
- package/src/utils/abap-http.js +347 -0
- package/src/{ref-search.js → utils/abap-reference.js} +119 -1
- package/src/utils/git-utils.js +58 -0
- package/src/utils/validators.js +72 -0
- package/src/utils/version-check.js +80 -0
- package/src/abap-client.js +0 -523
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ABAP HTTP request wrapper with CSRF token and session management
|
|
3
|
+
*/
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ABAP HTTP client with CSRF token, cookie, and session caching
|
|
13
|
+
*/
|
|
14
|
+
class AbapHttp {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.csrfToken = null;
|
|
18
|
+
this.cookies = null;
|
|
19
|
+
|
|
20
|
+
// Session cache file path
|
|
21
|
+
const configHash = crypto.createHash('md5')
|
|
22
|
+
.update(`${config.host}:${config.user}:${config.client}`)
|
|
23
|
+
.digest('hex')
|
|
24
|
+
.substring(0, 8);
|
|
25
|
+
|
|
26
|
+
this.sessionFile = path.join(os.tmpdir(), `abapgit-session-${configHash}.json`);
|
|
27
|
+
|
|
28
|
+
// Try to load cached session
|
|
29
|
+
this.loadSession();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Load session from cache file if valid
|
|
34
|
+
*/
|
|
35
|
+
loadSession() {
|
|
36
|
+
if (!fs.existsSync(this.sessionFile)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const session = JSON.parse(fs.readFileSync(this.sessionFile, 'utf8'));
|
|
42
|
+
|
|
43
|
+
// Check if expired (with 2-minute safety margin)
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const safetyMargin = 2 * 60 * 1000; // 2 minutes
|
|
46
|
+
|
|
47
|
+
if (session.expiresAt > now + safetyMargin) {
|
|
48
|
+
this.csrfToken = session.csrfToken;
|
|
49
|
+
this.cookies = session.cookies;
|
|
50
|
+
// Silent - no console output for cached session
|
|
51
|
+
} else {
|
|
52
|
+
// Session expired
|
|
53
|
+
this.clearSession();
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// Corrupted cache file - clear it
|
|
57
|
+
this.clearSession();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Save session to cache file
|
|
63
|
+
*/
|
|
64
|
+
saveSession() {
|
|
65
|
+
// Conservative expiration: 15 minutes
|
|
66
|
+
// (ABAP default session timeout is typically 20 minutes)
|
|
67
|
+
const expiresAt = Date.now() + (15 * 60 * 1000);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
fs.writeFileSync(this.sessionFile, JSON.stringify({
|
|
71
|
+
csrfToken: this.csrfToken,
|
|
72
|
+
cookies: this.cookies,
|
|
73
|
+
expiresAt,
|
|
74
|
+
savedAt: Date.now()
|
|
75
|
+
}));
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// Ignore write errors - session caching is optional
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clear cached session
|
|
83
|
+
*/
|
|
84
|
+
clearSession() {
|
|
85
|
+
this.csrfToken = null;
|
|
86
|
+
this.cookies = null;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
if (fs.existsSync(this.sessionFile)) {
|
|
90
|
+
fs.unlinkSync(this.sessionFile);
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Ignore file deletion errors
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Detect if error is due to expired/invalid session
|
|
99
|
+
* @param {Error|object} error - Error object or response
|
|
100
|
+
* @returns {boolean} True if auth error
|
|
101
|
+
*/
|
|
102
|
+
isAuthError(error) {
|
|
103
|
+
// HTTP status codes
|
|
104
|
+
if (error.statusCode === 401) return true; // Unauthorized
|
|
105
|
+
if (error.statusCode === 403) return true; // Forbidden
|
|
106
|
+
|
|
107
|
+
// Error message patterns
|
|
108
|
+
const message = (error.message || error.error || '').toLowerCase();
|
|
109
|
+
if (message.includes('csrf')) return true;
|
|
110
|
+
if (message.includes('token')) return true;
|
|
111
|
+
if (message.includes('session')) return true;
|
|
112
|
+
if (message.includes('expired')) return true;
|
|
113
|
+
if (message.includes('unauthorized')) return true;
|
|
114
|
+
if (message.includes('forbidden')) return true;
|
|
115
|
+
|
|
116
|
+
// Response body patterns
|
|
117
|
+
if (error.body) {
|
|
118
|
+
const bodyStr = JSON.stringify(error.body).toLowerCase();
|
|
119
|
+
if (bodyStr.includes('csrf')) return true;
|
|
120
|
+
if (bodyStr.includes('session')) return true;
|
|
121
|
+
if (bodyStr.includes('expired')) return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fetch CSRF token using GET /health with X-CSRF-Token: fetch
|
|
129
|
+
* @returns {Promise<string>} CSRF token
|
|
130
|
+
*/
|
|
131
|
+
async fetchCsrfToken() {
|
|
132
|
+
const url = new URL(`/sap/bc/z_abapgit_agent/health`, `https://${this.config.host}:${this.config.sapport}`);
|
|
133
|
+
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const options = {
|
|
136
|
+
hostname: url.hostname,
|
|
137
|
+
port: url.port,
|
|
138
|
+
path: url.pathname,
|
|
139
|
+
method: 'GET',
|
|
140
|
+
headers: {
|
|
141
|
+
'Authorization': `Basic ${Buffer.from(`${this.config.user}:${this.config.password}`).toString('base64')}`,
|
|
142
|
+
'sap-client': this.config.client,
|
|
143
|
+
'sap-language': this.config.language || 'EN',
|
|
144
|
+
'X-CSRF-Token': 'fetch',
|
|
145
|
+
'Content-Type': 'application/json'
|
|
146
|
+
},
|
|
147
|
+
agent: new https.Agent({ rejectUnauthorized: false })
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const req = https.request(options, (res) => {
|
|
151
|
+
const csrfToken = res.headers['x-csrf-token'];
|
|
152
|
+
|
|
153
|
+
// Save cookies from response
|
|
154
|
+
const setCookie = res.headers['set-cookie'];
|
|
155
|
+
if (setCookie) {
|
|
156
|
+
this.cookies = Array.isArray(setCookie)
|
|
157
|
+
? setCookie.map(c => c.split(';')[0]).join('; ')
|
|
158
|
+
: setCookie.split(';')[0];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let body = '';
|
|
162
|
+
res.on('data', chunk => body += chunk);
|
|
163
|
+
res.on('end', () => {
|
|
164
|
+
this.csrfToken = csrfToken;
|
|
165
|
+
|
|
166
|
+
// Save session to cache
|
|
167
|
+
this.saveSession();
|
|
168
|
+
|
|
169
|
+
resolve(csrfToken);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
req.on('error', reject);
|
|
174
|
+
req.end();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Make HTTP request to ABAP REST endpoint with automatic retry on auth failure
|
|
180
|
+
* @param {string} method - HTTP method (GET, POST, DELETE)
|
|
181
|
+
* @param {string} urlPath - URL path
|
|
182
|
+
* @param {object} data - Request body (for POST)
|
|
183
|
+
* @param {object} options - Additional options (headers, csrfToken, isRetry)
|
|
184
|
+
* @returns {Promise<object>} Response JSON
|
|
185
|
+
*/
|
|
186
|
+
async request(method, urlPath, data = null, options = {}) {
|
|
187
|
+
try {
|
|
188
|
+
// Try with current session (cached or fresh)
|
|
189
|
+
return await this._makeRequest(method, urlPath, data, options);
|
|
190
|
+
|
|
191
|
+
} catch (error) {
|
|
192
|
+
// Check if it's an authentication/authorization failure
|
|
193
|
+
if (this.isAuthError(error) && !options.isRetry) {
|
|
194
|
+
// Session expired - refresh and retry once
|
|
195
|
+
console.error('⚠️ Session expired, refreshing...');
|
|
196
|
+
|
|
197
|
+
// Clear stale session
|
|
198
|
+
this.clearSession();
|
|
199
|
+
|
|
200
|
+
// Fetch fresh token/cookies
|
|
201
|
+
await this.fetchCsrfToken();
|
|
202
|
+
|
|
203
|
+
// Retry ONCE with fresh session
|
|
204
|
+
return await this._makeRequest(method, urlPath, data, {
|
|
205
|
+
...options,
|
|
206
|
+
isRetry: true // Prevent infinite loop
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Not an auth error or already retried - propagate
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Internal request implementation (no retry logic)
|
|
217
|
+
* @param {string} method - HTTP method
|
|
218
|
+
* @param {string} urlPath - URL path
|
|
219
|
+
* @param {object} data - Request body
|
|
220
|
+
* @param {object} options - Additional options
|
|
221
|
+
* @returns {Promise<object>} Response JSON
|
|
222
|
+
*/
|
|
223
|
+
async _makeRequest(method, urlPath, data = null, options = {}) {
|
|
224
|
+
return new Promise((resolve, reject) => {
|
|
225
|
+
const url = new URL(urlPath, `https://${this.config.host}:${this.config.sapport}`);
|
|
226
|
+
|
|
227
|
+
const headers = {
|
|
228
|
+
'Content-Type': 'application/json',
|
|
229
|
+
'sap-client': this.config.client,
|
|
230
|
+
'sap-language': this.config.language || 'EN',
|
|
231
|
+
...options.headers
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Add authorization
|
|
235
|
+
headers['Authorization'] = `Basic ${Buffer.from(`${this.config.user}:${this.config.password}`).toString('base64')}`;
|
|
236
|
+
|
|
237
|
+
// Add CSRF token for POST
|
|
238
|
+
if (method === 'POST' && options.csrfToken) {
|
|
239
|
+
headers['X-CSRF-Token'] = options.csrfToken;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Add cookies if available
|
|
243
|
+
if (this.cookies) {
|
|
244
|
+
headers['Cookie'] = this.cookies;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const reqOptions = {
|
|
248
|
+
hostname: url.hostname,
|
|
249
|
+
port: url.port,
|
|
250
|
+
path: url.pathname + url.search,
|
|
251
|
+
method,
|
|
252
|
+
headers,
|
|
253
|
+
agent: new https.Agent({ rejectUnauthorized: false })
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (res) => {
|
|
257
|
+
// Check for auth errors
|
|
258
|
+
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
259
|
+
reject({
|
|
260
|
+
statusCode: res.statusCode,
|
|
261
|
+
message: `Authentication failed: ${res.statusCode}`,
|
|
262
|
+
isAuthError: true
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let body = '';
|
|
268
|
+
res.on('data', chunk => body += chunk);
|
|
269
|
+
res.on('end', () => {
|
|
270
|
+
try {
|
|
271
|
+
// Handle unescaped newlines from ABAP - replace actual newlines with \n
|
|
272
|
+
const cleanedBody = body.replace(/\n/g, '\\n');
|
|
273
|
+
const parsed = JSON.parse(cleanedBody);
|
|
274
|
+
|
|
275
|
+
// Check for CSRF/session errors in response body
|
|
276
|
+
if (this.isAuthError(parsed)) {
|
|
277
|
+
reject({
|
|
278
|
+
statusCode: res.statusCode,
|
|
279
|
+
message: 'CSRF token or session error',
|
|
280
|
+
body: parsed,
|
|
281
|
+
isAuthError: true
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
resolve(parsed);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
// Fallback: try to extract JSON from response
|
|
289
|
+
const jsonMatch = body.match(/\{[\s\S]*\}/);
|
|
290
|
+
if (jsonMatch) {
|
|
291
|
+
try {
|
|
292
|
+
const parsed = JSON.parse(jsonMatch[0].replace(/\n/g, '\\n'));
|
|
293
|
+
resolve(parsed);
|
|
294
|
+
} catch (e2) {
|
|
295
|
+
resolve({ raw: body, error: e2.message });
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
resolve({ raw: body, error: e.message });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
req.on('error', reject);
|
|
305
|
+
|
|
306
|
+
if (data) {
|
|
307
|
+
req.write(JSON.stringify(data));
|
|
308
|
+
}
|
|
309
|
+
req.end();
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Convenience method for GET requests
|
|
315
|
+
* @param {string} urlPath - URL path
|
|
316
|
+
* @param {object} options - Additional options
|
|
317
|
+
* @returns {Promise<object>} Response JSON
|
|
318
|
+
*/
|
|
319
|
+
async get(urlPath, options = {}) {
|
|
320
|
+
return this.request('GET', urlPath, null, options);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Convenience method for POST requests
|
|
325
|
+
* @param {string} urlPath - URL path
|
|
326
|
+
* @param {object} data - Request body
|
|
327
|
+
* @param {object} options - Additional options (must include csrfToken)
|
|
328
|
+
* @returns {Promise<object>} Response JSON
|
|
329
|
+
*/
|
|
330
|
+
async post(urlPath, data, options = {}) {
|
|
331
|
+
return this.request('POST', urlPath, data, options);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Convenience method for DELETE requests
|
|
336
|
+
* @param {string} urlPath - URL path
|
|
337
|
+
* @param {object} options - Additional options
|
|
338
|
+
* @returns {Promise<object>} Response JSON
|
|
339
|
+
*/
|
|
340
|
+
async delete(urlPath, options = {}) {
|
|
341
|
+
return this.request('DELETE', urlPath, null, options);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
AbapHttp
|
|
347
|
+
};
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { promisify } = require('util');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
11
12
|
const readdir = promisify(fs.readdir);
|
|
12
13
|
const readFile = promisify(fs.readFile);
|
|
13
14
|
const stat = promisify(fs.stat);
|
|
@@ -63,6 +64,120 @@ const TOPIC_MAP = {
|
|
|
63
64
|
'tables': '01_Internal_Tables.md'
|
|
64
65
|
};
|
|
65
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Ensure reference folder exists, create if necessary
|
|
69
|
+
* @returns {string|null} Path to reference folder or null if cannot create
|
|
70
|
+
*/
|
|
71
|
+
function ensureReferenceFolder() {
|
|
72
|
+
let refFolder = detectReferenceFolder();
|
|
73
|
+
|
|
74
|
+
if (refFolder && fs.existsSync(refFolder)) {
|
|
75
|
+
return refFolder;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Try to create the reference folder at default location
|
|
79
|
+
const homeDir = require('os').homedir();
|
|
80
|
+
const defaultPath = path.join(homeDir, 'abap-reference');
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
if (!fs.existsSync(defaultPath)) {
|
|
84
|
+
fs.mkdirSync(defaultPath, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
return defaultPath;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Clone a repository to the reference folder
|
|
94
|
+
* @param {string} repoUrl - Git repository URL or short name (e.g., "SAP-samples/abap-cheat-sheets")
|
|
95
|
+
* @param {string|null} name - Optional folder name for the cloned repo
|
|
96
|
+
* @returns {Object} Clone result
|
|
97
|
+
*/
|
|
98
|
+
function cloneRepository(repoUrl, name = null) {
|
|
99
|
+
const refFolder = ensureReferenceFolder();
|
|
100
|
+
|
|
101
|
+
if (!refFolder) {
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
error: 'Could not create reference folder',
|
|
105
|
+
hint: 'Set referenceFolder in .abapGitAgent or ensure ~/abap-reference is writable'
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Parse the repo URL
|
|
110
|
+
let targetName = name;
|
|
111
|
+
let cloneUrl = repoUrl;
|
|
112
|
+
|
|
113
|
+
// Handle short names (e.g., "SAP-samples/abap-cheat-sheets")
|
|
114
|
+
if (!repoUrl.startsWith('http://') && !repoUrl.startsWith('https://') && !repoUrl.startsWith('git@')) {
|
|
115
|
+
cloneUrl = `https://github.com/${repoUrl}.git`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If no name provided, extract from URL
|
|
119
|
+
if (!targetName) {
|
|
120
|
+
// Extract repo name from URL
|
|
121
|
+
const urlParts = cloneUrl.split('/');
|
|
122
|
+
const repoWithGit = urlParts[urlParts.length - 1];
|
|
123
|
+
targetName = repoWithGit.replace(/\.git$/, '');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const targetPath = path.join(refFolder, targetName);
|
|
127
|
+
|
|
128
|
+
// Check if already exists
|
|
129
|
+
if (fs.existsSync(targetPath)) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
error: `Repository already exists: ${targetName}`,
|
|
133
|
+
hint: `Delete '${targetPath}' to re-clone, or use --name to specify a different folder name`,
|
|
134
|
+
existingPath: targetPath
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
// Run git clone
|
|
140
|
+
execSync(`git clone "${cloneUrl}" "${targetPath}"`, {
|
|
141
|
+
stdio: 'pipe',
|
|
142
|
+
encoding: 'utf8'
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
message: `Successfully cloned ${repoUrl}`,
|
|
148
|
+
repository: targetName,
|
|
149
|
+
folder: targetPath,
|
|
150
|
+
referenceFolder: refFolder
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: `Failed to clone: ${error.message}`,
|
|
156
|
+
hint: 'Check the repository URL and your network connection'
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Display clone result in console format
|
|
163
|
+
* @param {Object} result - Clone result
|
|
164
|
+
*/
|
|
165
|
+
function displayCloneResult(result) {
|
|
166
|
+
if (result.success) {
|
|
167
|
+
console.log(`\n ✅ ${result.message}`);
|
|
168
|
+
console.log(`\n 📁 Repository: ${result.repository}`);
|
|
169
|
+
console.log(` 📁 Location: ${result.folder}`);
|
|
170
|
+
console.log(` 📁 Reference folder: ${result.referenceFolder}`);
|
|
171
|
+
console.log(`\n 💡 You can now search this repository with:`);
|
|
172
|
+
console.log(` abapgit-agent ref --list-repos`);
|
|
173
|
+
} else {
|
|
174
|
+
console.error(`\n ❌ ${result.error}`);
|
|
175
|
+
if (result.hint) {
|
|
176
|
+
console.error(`\n 💡 ${result.hint}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
66
181
|
/**
|
|
67
182
|
* Detect reference folder from config or common locations
|
|
68
183
|
* @returns {string|null} Path to reference folder or null if not found
|
|
@@ -1005,7 +1120,7 @@ function displayInitResult(result) {
|
|
|
1005
1120
|
console.log(`\n 💡 Next steps:`);
|
|
1006
1121
|
console.log(` 1. Review the guidelines in abap/guidelines/`);
|
|
1007
1122
|
console.log(` 2. Customize as needed for your project`);
|
|
1008
|
-
console.log(` 3.
|
|
1123
|
+
console.log(` 3. Guidelines are automatically searchable with 'ref' command`);
|
|
1009
1124
|
} else {
|
|
1010
1125
|
console.error(`\n ❌ ${result.error}`);
|
|
1011
1126
|
if (result.hint) {
|
|
@@ -1016,6 +1131,7 @@ function displayInitResult(result) {
|
|
|
1016
1131
|
|
|
1017
1132
|
module.exports = {
|
|
1018
1133
|
detectReferenceFolder,
|
|
1134
|
+
ensureReferenceFolder,
|
|
1019
1135
|
detectGuidelinesFolder,
|
|
1020
1136
|
getBuiltInGuidelinesPath,
|
|
1021
1137
|
initGuidelines,
|
|
@@ -1027,11 +1143,13 @@ module.exports = {
|
|
|
1027
1143
|
listTopics,
|
|
1028
1144
|
listRepositories,
|
|
1029
1145
|
exportGuidelines,
|
|
1146
|
+
cloneRepository,
|
|
1030
1147
|
displaySearchResults,
|
|
1031
1148
|
displayTopic,
|
|
1032
1149
|
displayTopics,
|
|
1033
1150
|
displayRepositories,
|
|
1034
1151
|
displayExportResult,
|
|
1035
1152
|
displayInitResult,
|
|
1153
|
+
displayCloneResult,
|
|
1036
1154
|
TOPIC_MAP
|
|
1037
1155
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-related utilities
|
|
3
|
+
*/
|
|
4
|
+
const pathModule = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get git remote URL from .git/config
|
|
9
|
+
* @returns {string|null} Git remote URL or null if not found
|
|
10
|
+
*/
|
|
11
|
+
function getRemoteUrl() {
|
|
12
|
+
const gitConfigPath = pathModule.join(process.cwd(), '.git', 'config');
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(gitConfigPath)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const content = fs.readFileSync(gitConfigPath, 'utf8');
|
|
19
|
+
const remoteMatch = content.match(/\[remote "origin"\]/);
|
|
20
|
+
|
|
21
|
+
if (!remoteMatch) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const urlMatch = content.match(/ url = (.+)/);
|
|
26
|
+
return urlMatch ? urlMatch[1].trim() : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get current git branch from .git/HEAD
|
|
31
|
+
* @returns {string} Current branch name or 'main' as default
|
|
32
|
+
*/
|
|
33
|
+
function getBranch() {
|
|
34
|
+
const headPath = pathModule.join(process.cwd(), '.git', 'HEAD');
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(headPath)) {
|
|
37
|
+
return 'main';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const content = fs.readFileSync(headPath, 'utf8').trim();
|
|
41
|
+
const match = content.match(/ref: refs\/heads\/(.+)/);
|
|
42
|
+
return match ? match[1] : 'main';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if current directory is a git repository
|
|
47
|
+
* @returns {boolean} True if .git directory exists
|
|
48
|
+
*/
|
|
49
|
+
function isGitRepo() {
|
|
50
|
+
const gitPath = pathModule.join(process.cwd(), '.git');
|
|
51
|
+
return fs.existsSync(gitPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
getRemoteUrl,
|
|
56
|
+
getBranch,
|
|
57
|
+
isGitRepo
|
|
58
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validation utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Convert ISO date formats (YYYY-MM-DD) to ABAP DATS format (YYYYMMDD) in WHERE clause
|
|
7
|
+
* This allows users to use familiar ISO date formats while ensuring compatibility with ABAP SQL
|
|
8
|
+
* @param {string} whereClause - SQL WHERE clause
|
|
9
|
+
* @returns {string} - WHERE clause with dates converted to YYYYMMDD format
|
|
10
|
+
*/
|
|
11
|
+
function convertDatesInWhereClause(whereClause) {
|
|
12
|
+
if (!whereClause) return whereClause;
|
|
13
|
+
|
|
14
|
+
// Pattern to match ISO date format: 'YYYY-MM-DD'
|
|
15
|
+
const isoDatePattern = /'\d{4}-\d{2}-\d{2}'/g;
|
|
16
|
+
|
|
17
|
+
return whereClause.replace(isoDatePattern, (match) => {
|
|
18
|
+
// Extract YYYY, MM, DD from 'YYYY-MM-DD'
|
|
19
|
+
const dateContent = match.slice(1, -1); // Remove quotes: YYYY-MM-DD
|
|
20
|
+
const [year, month, day] = dateContent.split('-');
|
|
21
|
+
// Return in ABAP format: 'YYYYMMDD'
|
|
22
|
+
return `'${year}${month}${day}'`;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate package name format
|
|
28
|
+
* @param {string} packageName - ABAP package name
|
|
29
|
+
* @returns {boolean} True if valid
|
|
30
|
+
*/
|
|
31
|
+
function isValidPackageName(packageName) {
|
|
32
|
+
if (!packageName) return false;
|
|
33
|
+
// Package names: $PACKAGE or ZPACKAGE (can contain _ and alphanumeric)
|
|
34
|
+
return /^(\$|Z|Y)[A-Z0-9_]{0,29}$/i.test(packageName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate object name format
|
|
39
|
+
* @param {string} objectName - ABAP object name
|
|
40
|
+
* @returns {boolean} True if valid
|
|
41
|
+
*/
|
|
42
|
+
function isValidObjectName(objectName) {
|
|
43
|
+
if (!objectName) return false;
|
|
44
|
+
// Object names: up to 30 characters, alphanumeric + underscore
|
|
45
|
+
return /^[A-Z0-9_/]{1,30}$/i.test(objectName);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse file path to extract object type and name
|
|
50
|
+
* @param {string} filePath - File path (e.g., "src/zcl_my_class.clas.abap")
|
|
51
|
+
* @returns {object|null} {type, name} or null if invalid
|
|
52
|
+
*/
|
|
53
|
+
function parseObjectFromFile(filePath) {
|
|
54
|
+
const fileName = filePath.split('/').pop();
|
|
55
|
+
|
|
56
|
+
// Match patterns: zcl_class.clas.abap, zif_intf.intf.abap, etc.
|
|
57
|
+
const match = fileName.match(/^([a-z0-9_]+)\.(clas|intf|prog|fugr|ddls|tabl|dtel|ttyp|stru)\..*$/i);
|
|
58
|
+
|
|
59
|
+
if (!match) return null;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
name: match[1].toUpperCase(),
|
|
63
|
+
type: match[2].toUpperCase()
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
convertDatesInWhereClause,
|
|
69
|
+
isValidPackageName,
|
|
70
|
+
isValidObjectName,
|
|
71
|
+
parseObjectFromFile
|
|
72
|
+
};
|