abapgit-agent 1.7.2 → 1.8.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 +26 -8
- package/abap/CLAUDE.md +146 -26
- package/abap/guidelines/00_index.md +8 -0
- package/abap/guidelines/01_sql.md +28 -0
- package/abap/guidelines/02_exceptions.md +8 -0
- package/abap/guidelines/03_testing.md +8 -0
- package/abap/guidelines/04_cds.md +151 -36
- 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 +52 -3
- package/abap/guidelines/09_unit_testable_code.md +8 -0
- package/abap/guidelines/10_common_errors.md +95 -0
- package/bin/abapgit-agent +61 -2852
- package/package.json +21 -5
- package/src/agent.js +205 -16
- 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 +340 -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/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 -526
- /package/src/{ref-search.js → utils/abap-reference.js} +0 -0
package/bin/abapgit-agent
CHANGED
|
@@ -19,1478 +19,48 @@
|
|
|
19
19
|
const pathModule = require('path');
|
|
20
20
|
const fs = require('fs');
|
|
21
21
|
|
|
22
|
-
//
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Convert ISO date formats (YYYY-MM-DD) to ABAP DATS format (YYYYMMDD) in WHERE clause
|
|
30
|
-
* This allows users to use familiar ISO date formats while ensuring compatibility with ABAP SQL
|
|
31
|
-
* @param {string} whereClause - SQL WHERE clause
|
|
32
|
-
* @returns {string} - WHERE clause with dates converted to YYYYMMDD format
|
|
33
|
-
*/
|
|
34
|
-
function convertDatesInWhereClause(whereClause) {
|
|
35
|
-
if (!whereClause) return whereClause;
|
|
36
|
-
|
|
37
|
-
// Pattern to match ISO date format: 'YYYY-MM-DD'
|
|
38
|
-
const isoDatePattern = /'\d{4}-\d{2}-\d{2}'/g;
|
|
39
|
-
|
|
40
|
-
return whereClause.replace(isoDatePattern, (match) => {
|
|
41
|
-
// Extract YYYY, MM, DD from 'YYYY-MM-DD'
|
|
42
|
-
const dateContent = match.slice(1, -1); // Remove quotes: YYYY-MM-DD
|
|
43
|
-
const [year, month, day] = dateContent.split('-');
|
|
44
|
-
// Return in ABAP format: 'YYYYMMDD'
|
|
45
|
-
return `'${year}${month}${day}'`;
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get CLI version from package.json
|
|
51
|
-
*/
|
|
52
|
-
function getCliVersion() {
|
|
53
|
-
const packageJsonPath = pathModule.join(__dirname, '..', 'package.json');
|
|
54
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
55
|
-
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
56
|
-
return pkg.version || '1.0.0';
|
|
57
|
-
}
|
|
58
|
-
return '1.0.0';
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check version compatibility between CLI and ABAP API
|
|
63
|
-
*/
|
|
64
|
-
async function checkVersionCompatibility() {
|
|
65
|
-
const cliVersion = getCliVersion();
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const config = loadConfig();
|
|
69
|
-
const https = require('https');
|
|
70
|
-
const url = new URL(`/sap/bc/z_abapgit_agent/health`, `https://${config.host}:${config.sapport}`);
|
|
71
|
-
|
|
72
|
-
return new Promise((resolve) => {
|
|
73
|
-
const options = {
|
|
74
|
-
hostname: url.hostname,
|
|
75
|
-
port: url.port,
|
|
76
|
-
path: url.pathname,
|
|
77
|
-
method: 'GET',
|
|
78
|
-
headers: {
|
|
79
|
-
'Authorization': `Basic ${Buffer.from(`${config.user}:${config.password}`).toString('base64')}`,
|
|
80
|
-
'sap-client': config.client,
|
|
81
|
-
'sap-language': config.language || 'EN',
|
|
82
|
-
'Content-Type': 'application/json'
|
|
83
|
-
},
|
|
84
|
-
agent: new https.Agent({ rejectUnauthorized: false })
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const req = https.request(options, (res) => {
|
|
88
|
-
let body = '';
|
|
89
|
-
res.on('data', chunk => body += chunk);
|
|
90
|
-
res.on('end', () => {
|
|
91
|
-
try {
|
|
92
|
-
const result = JSON.parse(body);
|
|
93
|
-
const apiVersion = result.version || '1.0.0';
|
|
94
|
-
|
|
95
|
-
if (cliVersion !== apiVersion) {
|
|
96
|
-
console.log(`\n⚠️ Version mismatch: CLI ${cliVersion}, ABAP API ${apiVersion}`);
|
|
97
|
-
console.log(' Some commands may not work correctly.');
|
|
98
|
-
console.log(' Update ABAP code: abapgit-agent pull\n');
|
|
99
|
-
}
|
|
100
|
-
resolve({ cliVersion, apiVersion, compatible: cliVersion === apiVersion });
|
|
101
|
-
} catch (e) {
|
|
102
|
-
resolve({ cliVersion, apiVersion: null, compatible: false, error: e.message });
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
req.on('error', (e) => {
|
|
108
|
-
resolve({ cliVersion, apiVersion: null, compatible: false, error: e.message });
|
|
109
|
-
});
|
|
110
|
-
req.end();
|
|
111
|
-
});
|
|
112
|
-
} catch (error) {
|
|
113
|
-
return { cliVersion, apiVersion: null, compatible: false, error: error.message };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Load configuration from .abapGitAgent in current working directory
|
|
119
|
-
*/
|
|
120
|
-
function loadConfig() {
|
|
121
|
-
const repoConfigPath = pathModule.join(process.cwd(), '.abapGitAgent');
|
|
122
|
-
|
|
123
|
-
if (fs.existsSync(repoConfigPath)) {
|
|
124
|
-
return JSON.parse(fs.readFileSync(repoConfigPath, 'utf8'));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Fallback to environment variables
|
|
128
|
-
return {
|
|
129
|
-
host: process.env.ABAP_HOST,
|
|
130
|
-
sapport: parseInt(process.env.ABAP_PORT, 10) || 443,
|
|
131
|
-
client: process.env.ABAP_CLIENT || '100',
|
|
132
|
-
user: process.env.ABAP_USER,
|
|
133
|
-
password: process.env.ABAP_PASSWORD,
|
|
134
|
-
language: process.env.ABAP_LANGUAGE || 'EN',
|
|
135
|
-
gitUsername: process.env.GIT_USERNAME,
|
|
136
|
-
gitPassword: process.env.GIT_PASSWORD,
|
|
137
|
-
transport: process.env.ABAP_TRANSPORT
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Get transport request from config or environment
|
|
143
|
-
*/
|
|
144
|
-
function getTransport() {
|
|
145
|
-
const config = loadConfig();
|
|
146
|
-
return config.transport;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Check if ABAP integration is configured for this repo
|
|
151
|
-
*/
|
|
152
|
-
function isAbapIntegrationEnabled() {
|
|
153
|
-
const repoConfigPath = pathModule.join(process.cwd(), '.abapGitAgent');
|
|
154
|
-
return fs.existsSync(repoConfigPath);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Get git remote URL from .git/config
|
|
159
|
-
*/
|
|
160
|
-
function getGitRemoteUrl() {
|
|
161
|
-
const gitConfigPath = pathModule.join(process.cwd(), '.git', 'config');
|
|
162
|
-
|
|
163
|
-
if (!fs.existsSync(gitConfigPath)) {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const content = fs.readFileSync(gitConfigPath, 'utf8');
|
|
168
|
-
const remoteMatch = content.match(/\[remote "origin"\]/);
|
|
169
|
-
|
|
170
|
-
if (!remoteMatch) {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const urlMatch = content.match(/ url = (.+)/);
|
|
175
|
-
return urlMatch ? urlMatch[1].trim() : null;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Get current git branch
|
|
180
|
-
*/
|
|
181
|
-
function getGitBranch() {
|
|
182
|
-
const headPath = pathModule.join(process.cwd(), '.git', 'HEAD');
|
|
183
|
-
|
|
184
|
-
if (!fs.existsSync(headPath)) {
|
|
185
|
-
return 'main';
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const content = fs.readFileSync(headPath, 'utf8').trim();
|
|
189
|
-
const match = content.match(/ref: refs\/heads\/(.+)/);
|
|
190
|
-
return match ? match[1] : 'main';
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Read cookies from file
|
|
195
|
-
* Supports both Netscape format and simple cookie string format
|
|
196
|
-
*/
|
|
197
|
-
function readNetscapeCookies() {
|
|
198
|
-
const cookiePath = pathModule.join(process.cwd(), COOKIE_FILE);
|
|
199
|
-
if (!fs.existsSync(cookiePath)) return '';
|
|
200
|
-
|
|
201
|
-
const content = fs.readFileSync(cookiePath, 'utf8').trim();
|
|
202
|
-
if (!content) return '';
|
|
203
|
-
|
|
204
|
-
// Check if it's Netscape format (has tabs)
|
|
205
|
-
if (content.includes('\t')) {
|
|
206
|
-
const lines = content.split('\n');
|
|
207
|
-
const cookies = [];
|
|
208
|
-
|
|
209
|
-
for (const line of lines) {
|
|
210
|
-
const trimmed = line.trim();
|
|
211
|
-
if (!trimmed || (trimmed.startsWith('#') && !trimmed.startsWith('#HttpOnly'))) continue;
|
|
212
|
-
|
|
213
|
-
const parts = trimmed.split('\t');
|
|
214
|
-
if (parts.length >= 7) {
|
|
215
|
-
cookies.push(`${parts[5]}=${parts[6]}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return cookies.join('; ');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Simple format - just return as-is
|
|
223
|
-
return content;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Save cookies to Netscape format cookie file
|
|
228
|
-
*/
|
|
229
|
-
function saveCookies(cookies, host) {
|
|
230
|
-
const cookiePath = pathModule.join(process.cwd(), COOKIE_FILE);
|
|
231
|
-
|
|
232
|
-
// Parse cookies and convert to Netscape format
|
|
233
|
-
const cookieList = cookies.split(';').map(c => c.trim()).filter(Boolean);
|
|
234
|
-
const lines = ['# Netscape HTTP Cookie File', '# https://curl.se/docs/http-cookies.html', '# This file was generated by libcurl! Edit at your own risk.'];
|
|
235
|
-
|
|
236
|
-
for (const cookie of cookieList) {
|
|
237
|
-
const [name, ...valueParts] = cookie.split('=');
|
|
238
|
-
const value = valueParts.join('=');
|
|
239
|
-
|
|
240
|
-
// HttpOnly cookies get #HttpOnly_ prefix
|
|
241
|
-
const domain = name.includes('SAP') || name.includes('MYSAP') || name.includes('SAPLOGON')
|
|
242
|
-
? `#HttpOnly_${host}`
|
|
243
|
-
: host;
|
|
244
|
-
|
|
245
|
-
// Format: domain, flag, path, secure, expiration, name, value
|
|
246
|
-
lines.push(`${domain}\tFALSE\t/\tFALSE\t0\t${name.trim()}\t${value}`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
fs.writeFileSync(cookiePath, lines.join('\n'));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Fetch CSRF token using GET /health with X-CSRF-Token: fetch
|
|
254
|
-
*/
|
|
255
|
-
async function fetchCsrfToken(config) {
|
|
256
|
-
const https = require('https');
|
|
257
|
-
const url = new URL(`/sap/bc/z_abapgit_agent/health`, `https://${config.host}:${config.sapport}`);
|
|
258
|
-
|
|
259
|
-
return new Promise((resolve, reject) => {
|
|
260
|
-
// Clear stale cookies before fetching new token
|
|
261
|
-
const cookiePath = pathModule.join(process.cwd(), COOKIE_FILE);
|
|
262
|
-
if (fs.existsSync(cookiePath)) {
|
|
263
|
-
fs.unlinkSync(cookiePath);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const cookieHeader = readNetscapeCookies();
|
|
267
|
-
|
|
268
|
-
const options = {
|
|
269
|
-
hostname: url.hostname,
|
|
270
|
-
port: url.port,
|
|
271
|
-
path: url.pathname,
|
|
272
|
-
method: 'GET',
|
|
273
|
-
headers: {
|
|
274
|
-
'Authorization': `Basic ${Buffer.from(`${config.user}:${config.password}`).toString('base64')}`,
|
|
275
|
-
'sap-client': config.client,
|
|
276
|
-
'sap-language': config.language || 'EN',
|
|
277
|
-
'X-CSRF-Token': 'fetch',
|
|
278
|
-
'Content-Type': 'application/json',
|
|
279
|
-
...(cookieHeader && { 'Cookie': cookieHeader })
|
|
280
|
-
},
|
|
281
|
-
agent: new https.Agent({ rejectUnauthorized: false })
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const req = https.request(options, (res) => {
|
|
285
|
-
const csrfToken = res.headers['x-csrf-token'];
|
|
286
|
-
|
|
287
|
-
// Save new cookies from response
|
|
288
|
-
const setCookie = res.headers['set-cookie'];
|
|
289
|
-
if (setCookie) {
|
|
290
|
-
const cookies = Array.isArray(setCookie)
|
|
291
|
-
? setCookie.map(c => c.split(';')[0]).join('; ')
|
|
292
|
-
: setCookie.split(';')[0];
|
|
293
|
-
saveCookies(cookies, config.host);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
let body = '';
|
|
297
|
-
res.on('data', chunk => body += chunk);
|
|
298
|
-
res.on('end', () => {
|
|
299
|
-
resolve(csrfToken);
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
req.on('error', reject);
|
|
304
|
-
req.end();
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Make HTTP request to ABAP REST endpoint
|
|
310
|
-
*/
|
|
311
|
-
function request(method, urlPath, data = null, options = {}) {
|
|
312
|
-
return new Promise((resolve, reject) => {
|
|
313
|
-
const https = require('https');
|
|
314
|
-
const http = require('http');
|
|
315
|
-
const config = loadConfig();
|
|
316
|
-
const url = new URL(urlPath, `https://${config.host}:${config.sapport}`);
|
|
317
|
-
|
|
318
|
-
const headers = {
|
|
319
|
-
'Content-Type': 'application/json',
|
|
320
|
-
'sap-client': config.client,
|
|
321
|
-
'sap-language': config.language || 'EN',
|
|
322
|
-
...options.headers
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
// Add authorization
|
|
326
|
-
headers['Authorization'] = `Basic ${Buffer.from(`${config.user}:${config.password}`).toString('base64')}`;
|
|
327
|
-
|
|
328
|
-
// Add CSRF token for POST
|
|
329
|
-
if (method === 'POST' && options.csrfToken) {
|
|
330
|
-
headers['X-CSRF-Token'] = options.csrfToken;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Add cookies if available
|
|
334
|
-
const cookieHeader = readNetscapeCookies();
|
|
335
|
-
if (cookieHeader) {
|
|
336
|
-
headers['Cookie'] = cookieHeader;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const reqOptions = {
|
|
340
|
-
hostname: url.hostname,
|
|
341
|
-
port: url.port,
|
|
342
|
-
path: url.pathname,
|
|
343
|
-
method,
|
|
344
|
-
headers,
|
|
345
|
-
agent: new https.Agent({ rejectUnauthorized: false })
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
const req = (url.protocol === 'https:' ? https : http).request(reqOptions, (res) => {
|
|
349
|
-
let body = '';
|
|
350
|
-
res.on('data', chunk => body += chunk);
|
|
351
|
-
res.on('end', () => {
|
|
352
|
-
try {
|
|
353
|
-
// Handle unescaped newlines from ABAP - replace actual newlines with \n
|
|
354
|
-
const cleanedBody = body.replace(/\n/g, '\\n');
|
|
355
|
-
resolve(JSON.parse(cleanedBody));
|
|
356
|
-
} catch (e) {
|
|
357
|
-
// Fallback: try to extract JSON from response
|
|
358
|
-
const jsonMatch = body.match(/\{[\s\S]*\}/);
|
|
359
|
-
if (jsonMatch) {
|
|
360
|
-
try {
|
|
361
|
-
resolve(JSON.parse(jsonMatch[0].replace(/\n/g, '\\n')));
|
|
362
|
-
} catch (e2) {
|
|
363
|
-
resolve({ raw: body, error: e2.message });
|
|
364
|
-
}
|
|
365
|
-
} else {
|
|
366
|
-
resolve({ raw: body, error: e.message });
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
req.on('error', reject);
|
|
373
|
-
|
|
374
|
-
if (data) {
|
|
375
|
-
req.write(JSON.stringify(data));
|
|
376
|
-
}
|
|
377
|
-
req.end();
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Inspect ABAP source file
|
|
383
|
-
* Reads file content and sends to /inspect
|
|
384
|
-
*/
|
|
385
|
-
async function syntaxCheckSource(sourceFile, csrfToken, config) {
|
|
386
|
-
console.log(` Syntax check for file: ${sourceFile}`);
|
|
387
|
-
|
|
388
|
-
try {
|
|
389
|
-
// Read file content
|
|
390
|
-
const absolutePath = pathModule.isAbsolute(sourceFile)
|
|
391
|
-
? sourceFile
|
|
392
|
-
: pathModule.join(process.cwd(), sourceFile);
|
|
393
|
-
|
|
394
|
-
if (!fs.existsSync(absolutePath)) {
|
|
395
|
-
console.error(` ❌ File not found: ${absolutePath}`);
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const fileContent = fs.readFileSync(absolutePath, 'utf8');
|
|
400
|
-
|
|
401
|
-
// Extract source name from file path (basename with extension)
|
|
402
|
-
// e.g., "zcl_my_class.clas.abap" -> "ZCL_MY_CLASS.CLASS.ABAP"
|
|
403
|
-
const sourceName = pathModule.basename(sourceFile).toUpperCase();
|
|
404
|
-
|
|
405
|
-
// Send files array to syntax-check endpoint
|
|
406
|
-
const data = {
|
|
407
|
-
files: [sourceName]
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/inspect', data, { csrfToken: csrfToken });
|
|
411
|
-
|
|
412
|
-
// Handle new table-based result format (array) or old single result
|
|
413
|
-
let results = [];
|
|
414
|
-
if (Array.isArray(result)) {
|
|
415
|
-
results = result;
|
|
416
|
-
} else {
|
|
417
|
-
// Convert old single result to array format for compatibility
|
|
418
|
-
results = [{
|
|
419
|
-
OBJECT_TYPE: 'UNKNOWN',
|
|
420
|
-
OBJECT_NAME: pathModule.basename(sourceFile),
|
|
421
|
-
SUCCESS: result.SUCCESS !== undefined ? result.SUCCESS === 'X' || result.SUCCESS === true : result.success,
|
|
422
|
-
ERROR_COUNT: result.ERROR_COUNT || result.error_count || 0,
|
|
423
|
-
ERRORS: result.ERRORS || result.errors || [],
|
|
424
|
-
WARNINGS: result.warnings || []
|
|
425
|
-
}];
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Process each result
|
|
429
|
-
let hasErrors = false;
|
|
430
|
-
for (const res of results) {
|
|
431
|
-
// Handle both uppercase and lowercase keys
|
|
432
|
-
const success = res.SUCCESS !== undefined ? res.SUCCESS : res.success;
|
|
433
|
-
const objectType = res.OBJECT_TYPE !== undefined ? res.OBJECT_TYPE : res.object_type;
|
|
434
|
-
const objectName = res.OBJECT_NAME !== undefined ? res.OBJECT_NAME : res.object_name;
|
|
435
|
-
const errorCount = res.ERROR_COUNT !== undefined ? res.ERROR_COUNT : (res.error_count || 0);
|
|
436
|
-
const errors = res.ERRORS !== undefined ? res.ERRORS : (res.errors || []);
|
|
437
|
-
const warnings = res.WARNINGS !== undefined ? res.WARNINGS : (res.warnings || []);
|
|
438
|
-
|
|
439
|
-
console.log('\n');
|
|
440
|
-
|
|
441
|
-
if (errorCount > 0 || warnings.length > 0) {
|
|
442
|
-
if (errorCount > 0) {
|
|
443
|
-
console.log(`❌ ${objectType} ${objectName} - Syntax check failed (${errorCount} error(s)):`);
|
|
444
|
-
} else {
|
|
445
|
-
console.log(`⚠️ ${objectType} ${objectName} - Syntax check passed with warnings (${warnings.length}):`);
|
|
446
|
-
}
|
|
447
|
-
console.log('\nErrors:');
|
|
448
|
-
console.log('─'.repeat(60));
|
|
449
|
-
|
|
450
|
-
for (const err of errors) {
|
|
451
|
-
const line = err.LINE || err.line || '?';
|
|
452
|
-
const column = err.COLUMN || err.column || '?';
|
|
453
|
-
const text = err.TEXT || err.text || 'Unknown error';
|
|
454
|
-
|
|
455
|
-
console.log(` Line ${line}, Column ${column}:`);
|
|
456
|
-
console.log(` ${text}`);
|
|
457
|
-
console.log('');
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Show warnings if any
|
|
461
|
-
if (warnings.length > 0) {
|
|
462
|
-
console.log('Warnings:');
|
|
463
|
-
console.log('─'.repeat(60));
|
|
464
|
-
for (const warn of warnings) {
|
|
465
|
-
const line = warn.LINE || warn.line || '?';
|
|
466
|
-
const text = warn.MESSAGE || warn.message || 'Unknown warning';
|
|
467
|
-
console.log(` Line ${line}: ${text}`);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
hasErrors = true;
|
|
471
|
-
} else if (success === true || success === 'X') {
|
|
472
|
-
console.log(`✅ ${objectType} ${objectName} - Syntax check passed`);
|
|
473
|
-
} else {
|
|
474
|
-
console.log(`⚠️ ${objectType} ${objectName} - Syntax check returned unexpected status`);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return result;
|
|
479
|
-
} catch (error) {
|
|
480
|
-
console.error(`\n Error: ${error.message}`);
|
|
481
|
-
process.exit(1);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Inspect all files in one request
|
|
487
|
-
*/
|
|
488
|
-
async function inspectAllFiles(files, csrfToken, config, variant = null) {
|
|
489
|
-
// Convert files to uppercase names (same as syntaxCheckSource does)
|
|
490
|
-
const fileNames = files.map(f => {
|
|
491
|
-
const baseName = pathModule.basename(f).toUpperCase();
|
|
492
|
-
return baseName;
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
try {
|
|
496
|
-
// Send all files in one request
|
|
497
|
-
const data = {
|
|
498
|
-
files: fileNames
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
// Add variant if specified
|
|
502
|
-
if (variant) {
|
|
503
|
-
data.variant = variant;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/inspect', data, { csrfToken: csrfToken });
|
|
507
|
-
|
|
508
|
-
// Handle both table result and old single result
|
|
509
|
-
let results = [];
|
|
510
|
-
if (Array.isArray(result)) {
|
|
511
|
-
results = result;
|
|
512
|
-
} else {
|
|
513
|
-
// Convert single result to array format
|
|
514
|
-
results = [{
|
|
515
|
-
OBJECT_TYPE: 'UNKNOWN',
|
|
516
|
-
OBJECT_NAME: files.join(', '),
|
|
517
|
-
SUCCESS: result.SUCCESS !== undefined ? result.SUCCESS === 'X' || result.SUCCESS === true : result.success,
|
|
518
|
-
ERROR_COUNT: result.ERROR_COUNT || result.error_count || 0,
|
|
519
|
-
ERRORS: result.ERRORS || result.errors || [],
|
|
520
|
-
WARNINGS: result.warnings || []
|
|
521
|
-
}];
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
return results;
|
|
525
|
-
} catch (error) {
|
|
526
|
-
console.error(`\n Error: ${error.message}`);
|
|
527
|
-
process.exit(1);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Process a single inspect result
|
|
533
|
-
*/
|
|
534
|
-
async function processInspectResult(res) {
|
|
535
|
-
// Handle both uppercase and lowercase keys
|
|
536
|
-
const success = res.SUCCESS !== undefined ? res.SUCCESS : res.success;
|
|
537
|
-
const objectType = res.OBJECT_TYPE !== undefined ? res.OBJECT_TYPE : res.object_type;
|
|
538
|
-
const objectName = res.OBJECT_NAME !== undefined ? res.OBJECT_NAME : res.object_name;
|
|
539
|
-
const errorCount = res.ERROR_COUNT !== undefined ? res.ERROR_COUNT : (res.error_count || 0);
|
|
540
|
-
const errors = res.ERRORS !== undefined ? res.ERRORS : (res.errors || []);
|
|
541
|
-
const warnings = res.WARNINGS !== undefined ? res.WARNINGS : (res.warnings || []);
|
|
542
|
-
const infos = res.INFOS !== undefined ? res.INFOS : (res.infos || []);
|
|
543
|
-
|
|
544
|
-
if (errorCount > 0 || warnings.length > 0 || infos.length > 0) {
|
|
545
|
-
if (errorCount > 0) {
|
|
546
|
-
console.log(`❌ ${objectType} ${objectName} - Syntax check failed (${errorCount} error(s)):`);
|
|
547
|
-
} else {
|
|
548
|
-
const total = warnings.length + infos.length;
|
|
549
|
-
console.log(`⚠️ ${objectType} ${objectName} - Syntax check passed with warnings (${total}):`);
|
|
550
|
-
}
|
|
551
|
-
console.log('\nErrors:');
|
|
552
|
-
console.log('─'.repeat(60));
|
|
553
|
-
|
|
554
|
-
for (const err of errors) {
|
|
555
|
-
const line = err.LINE || err.line || '?';
|
|
556
|
-
const column = err.COLUMN || err.column || '?';
|
|
557
|
-
const text = err.TEXT || err.text || 'Unknown error';
|
|
558
|
-
const methodName = err.METHOD_NAME || err.method_name;
|
|
559
|
-
const sobjname = err.SOBJNAME || err.sobjname;
|
|
560
|
-
|
|
561
|
-
if (methodName) {
|
|
562
|
-
console.log(` Method: ${methodName}`);
|
|
563
|
-
}
|
|
564
|
-
console.log(` Line ${line}, Column ${column}:`);
|
|
565
|
-
if (sobjname && sobjname.includes('====')) {
|
|
566
|
-
console.log(` Include: ${sobjname}`);
|
|
567
|
-
}
|
|
568
|
-
console.log(` ${text}`);
|
|
569
|
-
console.log('');
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Show warnings if any
|
|
573
|
-
if (warnings.length > 0) {
|
|
574
|
-
console.log('Warnings:');
|
|
575
|
-
console.log('─'.repeat(60));
|
|
576
|
-
for (const warn of warnings) {
|
|
577
|
-
const line = warn.LINE || warn.line || '?';
|
|
578
|
-
const text = warn.MESSAGE || warn.message || 'Unknown warning';
|
|
579
|
-
const methodName = warn.METHOD_NAME || warn.method_name;
|
|
580
|
-
const sobjname = warn.SOBJNAME || warn.sobjname;
|
|
581
|
-
|
|
582
|
-
if (methodName) {
|
|
583
|
-
console.log(` Method: ${methodName}`);
|
|
584
|
-
}
|
|
585
|
-
console.log(` Line ${line}:`);
|
|
586
|
-
if (sobjname && sobjname.includes('====')) {
|
|
587
|
-
console.log(` Include: ${sobjname}`);
|
|
588
|
-
}
|
|
589
|
-
console.log(` ${text}`);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Show infos if any
|
|
594
|
-
if (infos.length > 0) {
|
|
595
|
-
console.log('Info:');
|
|
596
|
-
console.log('─'.repeat(60));
|
|
597
|
-
for (const info of infos) {
|
|
598
|
-
const line = info.LINE || info.line || '?';
|
|
599
|
-
const text = info.MESSAGE || info.message || 'Unknown info';
|
|
600
|
-
const methodName = info.METHOD_NAME || info.method_name;
|
|
601
|
-
const sobjname = info.SOBJNAME || info.sobjname;
|
|
602
|
-
|
|
603
|
-
if (methodName) {
|
|
604
|
-
console.log(` Method: ${methodName}`);
|
|
605
|
-
}
|
|
606
|
-
console.log(` Line ${line}:`);
|
|
607
|
-
if (sobjname && sobjname.includes('====')) {
|
|
608
|
-
console.log(` Include: ${sobjname}`);
|
|
609
|
-
}
|
|
610
|
-
console.log(` ${text}`);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
} else if (success === true || success === 'X') {
|
|
614
|
-
console.log(`✅ ${objectType} ${objectName} - Syntax check passed`);
|
|
615
|
-
} else {
|
|
616
|
-
console.log(`⚠️ ${objectType} ${objectName} - Syntax check returned unexpected status`);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Run unit tests for package or objects
|
|
622
|
-
*/
|
|
623
|
-
async function runUnitTests(options) {
|
|
624
|
-
console.log('\n Running unit tests');
|
|
625
|
-
|
|
626
|
-
try {
|
|
627
|
-
const config = loadConfig();
|
|
628
|
-
|
|
629
|
-
// Fetch CSRF token first
|
|
630
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
631
|
-
|
|
632
|
-
const data = {};
|
|
633
|
-
|
|
634
|
-
if (options.package) {
|
|
635
|
-
data.package = options.package;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
if (options.objects && options.objects.length > 0) {
|
|
639
|
-
data.objects = options.objects;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/unit', data, { csrfToken });
|
|
643
|
-
|
|
644
|
-
// Display raw result for debugging
|
|
645
|
-
if (process.env.DEBUG) {
|
|
646
|
-
console.log('Raw result:', JSON.stringify(result, null, 2));
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Handle uppercase keys from ABAP
|
|
650
|
-
const success = result.SUCCESS || result.success;
|
|
651
|
-
const testCount = result.TEST_COUNT || result.test_count || 0;
|
|
652
|
-
const passedCount = result.PASSED_COUNT || result.passed_count || 0;
|
|
653
|
-
const failedCount = result.FAILED_COUNT || result.failed_count || 0;
|
|
654
|
-
const message = result.MESSAGE || result.message || '';
|
|
655
|
-
const errors = result.ERRORS || result.errors || [];
|
|
656
|
-
|
|
657
|
-
console.log('\n');
|
|
658
|
-
|
|
659
|
-
if (success === 'X' || success === true) {
|
|
660
|
-
console.log(`✅ All tests passed!`);
|
|
661
|
-
} else {
|
|
662
|
-
console.log(`❌ Some tests failed`);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
console.log(` ${message}`);
|
|
666
|
-
console.log(` Tests: ${testCount} | Passed: ${passedCount} | Failed: ${failedCount}`);
|
|
667
|
-
|
|
668
|
-
if (failedCount > 0 && errors.length > 0) {
|
|
669
|
-
console.log('\nFailed Tests:');
|
|
670
|
-
console.log('─'.repeat(60));
|
|
671
|
-
|
|
672
|
-
for (const err of errors) {
|
|
673
|
-
const className = err.CLASS_NAME || err.class_name || '?';
|
|
674
|
-
const methodName = err.METHOD_NAME || err.method_name || '?';
|
|
675
|
-
const errorKind = err.ERROR_KIND || err.error_kind || '';
|
|
676
|
-
const errorText = err.ERROR_TEXT || err.error_text || 'Unknown error';
|
|
677
|
-
|
|
678
|
-
console.log(` ✗ ${className}=>${methodName}`);
|
|
679
|
-
if (errorKind) {
|
|
680
|
-
console.log(` Kind: ${errorKind}`);
|
|
681
|
-
}
|
|
682
|
-
console.log(` Error: ${errorText}`);
|
|
683
|
-
console.log('');
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
return result;
|
|
688
|
-
} catch (error) {
|
|
689
|
-
console.error(`\n Error: ${error.message}`);
|
|
690
|
-
process.exit(1);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
* Run unit test for a single file
|
|
696
|
-
*/
|
|
697
|
-
async function runUnitTestForFile(sourceFile, csrfToken, config, coverage = false) {
|
|
698
|
-
console.log(` Running unit test for: ${sourceFile}`);
|
|
699
|
-
|
|
700
|
-
try {
|
|
701
|
-
// Read file content
|
|
702
|
-
const absolutePath = pathModule.isAbsolute(sourceFile)
|
|
703
|
-
? sourceFile
|
|
704
|
-
: pathModule.join(process.cwd(), sourceFile);
|
|
705
|
-
|
|
706
|
-
if (!fs.existsSync(absolutePath)) {
|
|
707
|
-
console.error(` ❌ File not found: ${absolutePath}`);
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Extract object type and name from file path
|
|
712
|
-
// e.g., "zcl_my_test.clas.abap" -> CLAS, ZCL_MY_TEST
|
|
713
|
-
const fileName = pathModule.basename(sourceFile).toUpperCase();
|
|
714
|
-
const parts = fileName.split('.');
|
|
715
|
-
if (parts.length < 3) {
|
|
716
|
-
console.error(` ❌ Invalid file format: ${sourceFile}`);
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// obj_name is first part (may contain path), obj_type is second part
|
|
721
|
-
const objType = parts[1] === 'CLASS' ? 'CLAS' : parts[1];
|
|
722
|
-
let objName = parts[0];
|
|
723
|
-
|
|
724
|
-
// Handle subdirectory paths
|
|
725
|
-
const lastSlash = objName.lastIndexOf('/');
|
|
726
|
-
if (lastSlash >= 0) {
|
|
727
|
-
objName = objName.substring(lastSlash + 1);
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Send files array to unit endpoint (ABAP expects string_table of file names)
|
|
731
|
-
const data = {
|
|
732
|
-
files: [sourceFile],
|
|
733
|
-
coverage: coverage
|
|
734
|
-
};
|
|
735
|
-
|
|
736
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/unit', data, { csrfToken });
|
|
737
|
-
|
|
738
|
-
// Handle uppercase keys from ABAP
|
|
739
|
-
const success = result.SUCCESS || result.success;
|
|
740
|
-
const testCount = result.TEST_COUNT || result.test_count || 0;
|
|
741
|
-
const passedCount = result.PASSED_COUNT || result.passed_count || 0;
|
|
742
|
-
const failedCount = result.FAILED_COUNT || result.failed_count || 0;
|
|
743
|
-
const message = result.MESSAGE || result.message || '';
|
|
744
|
-
const errors = result.ERRORS || result.errors || [];
|
|
745
|
-
|
|
746
|
-
// Handle coverage data
|
|
747
|
-
const coverageStats = result.COVERAGE_STATS || result.coverage_stats;
|
|
748
|
-
|
|
749
|
-
if (testCount === 0) {
|
|
750
|
-
console.log(` ➖ ${objName} - No unit tests`);
|
|
751
|
-
} else if (success === 'X' || success === true) {
|
|
752
|
-
console.log(` ✅ ${objName} - All tests passed`);
|
|
753
|
-
} else {
|
|
754
|
-
console.log(` ❌ ${objName} - Tests failed`);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
console.log(` Tests: ${testCount} | Passed: ${passedCount} | Failed: ${failedCount}`);
|
|
758
|
-
|
|
759
|
-
// Display coverage if available
|
|
760
|
-
if (coverage && coverageStats) {
|
|
761
|
-
const totalLines = coverageStats.TOTAL_LINES || coverageStats.total_lines || 0;
|
|
762
|
-
const coveredLines = coverageStats.COVERED_LINES || coverageStats.covered_lines || 0;
|
|
763
|
-
const coverageRate = coverageStats.COVERAGE_RATE || coverageStats.coverage_rate || 0;
|
|
764
|
-
|
|
765
|
-
console.log(` 📊 Coverage: ${coverageRate}%`);
|
|
766
|
-
console.log(` Total Lines: ${totalLines}`);
|
|
767
|
-
console.log(` Covered Lines: ${coveredLines}`);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
if (failedCount > 0 && errors.length > 0) {
|
|
771
|
-
for (const err of errors) {
|
|
772
|
-
const className = err.CLASS_NAME || err.class_name || '?';
|
|
773
|
-
const methodName = err.METHOD_NAME || err.method_name || '?';
|
|
774
|
-
const errorText = err.ERROR_TEXT || err.error_text || 'Unknown error';
|
|
775
|
-
console.log(` ✗ ${className}=>${methodName}: ${errorText}`);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
return result;
|
|
780
|
-
} catch (error) {
|
|
781
|
-
console.error(`\n Error: ${error.message}`);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
/**
|
|
786
|
-
* Run tree command and return raw result
|
|
787
|
-
*/
|
|
788
|
-
async function runTreeCommand(packageName, depth, includeTypes, csrfToken, config) {
|
|
789
|
-
const data = {
|
|
790
|
-
package: packageName,
|
|
791
|
-
depth: depth,
|
|
792
|
-
include_objects: includeTypes
|
|
793
|
-
};
|
|
794
|
-
|
|
795
|
-
return await request('POST', '/sap/bc/z_abapgit_agent/tree', data, { csrfToken });
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
/**
|
|
799
|
-
* Display tree output in human-readable format
|
|
800
|
-
*/
|
|
801
|
-
async function displayTreeOutput(packageName, depth, includeTypes) {
|
|
802
|
-
const config = loadConfig();
|
|
803
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
804
|
-
|
|
805
|
-
console.log(`\n Getting package tree for: ${packageName}`);
|
|
806
|
-
|
|
807
|
-
const result = await runTreeCommand(packageName, depth, includeTypes, csrfToken, config);
|
|
808
|
-
|
|
809
|
-
// Handle uppercase keys from ABAP
|
|
810
|
-
const success = result.SUCCESS || result.success;
|
|
811
|
-
const error = result.ERROR || result.error;
|
|
812
|
-
|
|
813
|
-
if (!success || error) {
|
|
814
|
-
console.error(`\n ❌ Error: ${error || 'Failed to get tree'}`);
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Parse hierarchy structure (ABAP returns flat nodes with parent refs)
|
|
819
|
-
const nodes = result.NODES || result.nodes || [];
|
|
820
|
-
const rootPackage = result.PACKAGE || result.package || packageName;
|
|
821
|
-
const parentPackage = result.PARENT_PACKAGE || result.parent_package;
|
|
822
|
-
const totalPackages = result.TOTAL_PACKAGES || result.total_packages || 0;
|
|
823
|
-
const totalObjects = result.TOTAL_OBJECTS || result.total_objects || 0;
|
|
824
|
-
const objectTypes = result.OBJECTS || result.objects || [];
|
|
825
|
-
|
|
826
|
-
console.log(`\n Package Tree: ${rootPackage}`);
|
|
827
|
-
|
|
828
|
-
// Display parent info if available
|
|
829
|
-
if (parentPackage && parentPackage !== rootPackage) {
|
|
830
|
-
console.log(` ⬆️ Parent: ${parentPackage}`);
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
console.log('');
|
|
834
|
-
|
|
835
|
-
// Build and display tree from flat nodes list
|
|
836
|
-
const lines = buildTreeLinesFromNodes(nodes, '', true);
|
|
837
|
-
for (const line of lines) {
|
|
838
|
-
console.log(` ${line}`);
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
console.log('');
|
|
842
|
-
console.log(' Summary');
|
|
843
|
-
console.log(` PACKAGES: ${totalPackages}`);
|
|
844
|
-
console.log(` OBJECTS: ${totalObjects}`);
|
|
845
|
-
|
|
846
|
-
// Display object types if available
|
|
847
|
-
if (includeTypes && objectTypes.length > 0) {
|
|
848
|
-
const typeStr = objectTypes.map(t => `${t.OBJECT || t.object}=${t.COUNT || t.count}`).join(' ');
|
|
849
|
-
console.log(` TYPES: ${typeStr}`);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
/**
|
|
854
|
-
* Build tree display lines from flat nodes list
|
|
855
|
-
*/
|
|
856
|
-
function buildTreeLinesFromNodes(nodes, prefix, isLast) {
|
|
857
|
-
const lines = [];
|
|
858
|
-
|
|
859
|
-
if (nodes.length === 0) {
|
|
860
|
-
return lines;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
// First node is the root
|
|
864
|
-
const root = nodes[0];
|
|
865
|
-
const icon = '📦';
|
|
866
|
-
lines.push(`${prefix}${isLast ? '└─ ' : '├─ '} ${icon} ${root.PACKAGE || root.package}`);
|
|
867
|
-
|
|
868
|
-
// Get children (nodes with depth > 0, grouped by depth)
|
|
869
|
-
const children = nodes.filter(n => (n.DEPTH || n.depth) > 0);
|
|
870
|
-
|
|
871
|
-
// Group children by depth
|
|
872
|
-
const byDepth = {};
|
|
873
|
-
children.forEach(n => {
|
|
874
|
-
const d = n.DEPTH || n.depth;
|
|
875
|
-
if (!byDepth[d]) byDepth[d] = [];
|
|
876
|
-
byDepth[d].push(n);
|
|
877
|
-
});
|
|
878
|
-
|
|
879
|
-
// Process depth 1 children
|
|
880
|
-
const depth1 = byDepth[1] || [];
|
|
881
|
-
const newPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
882
|
-
|
|
883
|
-
for (let i = 0; i < depth1.length; i++) {
|
|
884
|
-
const child = depth1[i];
|
|
885
|
-
const isSubLast = i === depth1.length - 1;
|
|
886
|
-
const childLines = buildChildLines(child, newPrefix, isSubLast, byDepth);
|
|
887
|
-
lines.push(...childLines);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
return lines;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function buildChildLines(node, prefix, isLast, byDepth) {
|
|
894
|
-
const lines = [];
|
|
895
|
-
const icon = '📦';
|
|
896
|
-
lines.push(`${prefix}${isLast ? '└─ ' : '├─ '} ${icon} ${node.PACKAGE || node.package}`);
|
|
897
|
-
|
|
898
|
-
const newPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
899
|
-
const nodeDepth = node.DEPTH || node.depth;
|
|
900
|
-
const children = byDepth[nodeDepth + 1] || [];
|
|
901
|
-
|
|
902
|
-
// Find children of this node
|
|
903
|
-
const myChildren = children.filter(n => (n.PARENT || n.parent) === (node.PACKAGE || node.package));
|
|
904
|
-
|
|
905
|
-
for (let i = 0; i < myChildren.length; i++) {
|
|
906
|
-
const child = myChildren[i];
|
|
907
|
-
const isSubLast = i === myChildren.length - 1;
|
|
908
|
-
const childLines = buildChildLines(child, newPrefix, isSubLast, byDepth);
|
|
909
|
-
lines.push(...childLines);
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
return lines;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Pull and activate repository
|
|
917
|
-
*/
|
|
918
|
-
async function pull(gitUrl, branch = 'main', files = null, transportRequest = null) {
|
|
919
|
-
console.log(`\n🚀 Starting pull for: ${gitUrl}`);
|
|
920
|
-
console.log(` Branch: ${branch}`);
|
|
921
|
-
if (files && files.length > 0) {
|
|
922
|
-
console.log(` Files: ${files.join(', ')}`);
|
|
923
|
-
}
|
|
924
|
-
if (transportRequest) {
|
|
925
|
-
console.log(` Transport Request: ${transportRequest}`);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
try {
|
|
929
|
-
const config = loadConfig();
|
|
930
|
-
|
|
931
|
-
// Fetch CSRF token first
|
|
932
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
933
|
-
|
|
934
|
-
// Prepare request data with git credentials
|
|
935
|
-
const data = {
|
|
936
|
-
url: gitUrl,
|
|
937
|
-
branch: branch,
|
|
938
|
-
username: config.gitUsername,
|
|
939
|
-
password: config.gitPassword
|
|
940
|
-
};
|
|
941
|
-
|
|
942
|
-
// Add files array if specified
|
|
943
|
-
if (files && files.length > 0) {
|
|
944
|
-
data.files = files;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// Add transport request if specified
|
|
948
|
-
if (transportRequest) {
|
|
949
|
-
data.transport_request = transportRequest;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/pull', data, { csrfToken });
|
|
953
|
-
|
|
954
|
-
console.log('\n');
|
|
955
|
-
|
|
956
|
-
// Display raw result for debugging
|
|
957
|
-
if (process.env.DEBUG) {
|
|
958
|
-
console.log('Raw result:', JSON.stringify(result, null, 2));
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
// Handle uppercase keys from ABAP
|
|
962
|
-
const success = result.SUCCESS || result.success;
|
|
963
|
-
const jobId = result.JOB_ID || result.job_id;
|
|
964
|
-
const message = result.MESSAGE || result.message;
|
|
965
|
-
const errorDetail = result.ERROR_DETAIL || result.error_detail;
|
|
966
|
-
const transportRequestUsed = result.TRANSPORT_REQUEST || result.transport_request;
|
|
967
|
-
const activatedCount = result.ACTIVATED_COUNT || result.activated_count || 0;
|
|
968
|
-
const failedCount = result.FAILED_COUNT || result.failed_count || 0;
|
|
969
|
-
const logMessages = result.LOG_MESSAGES || result.log_messages || [];
|
|
970
|
-
const activatedObjects = result.ACTIVATED_OBJECTS || result.activated_objects || [];
|
|
971
|
-
const failedObjects = result.FAILED_OBJECTS || result.failed_objects || [];
|
|
972
|
-
|
|
973
|
-
// Icon mapping for message types
|
|
974
|
-
const getIcon = (type) => {
|
|
975
|
-
const icons = {
|
|
976
|
-
'S': '✅', // Success
|
|
977
|
-
'E': '❌', // Error
|
|
978
|
-
'W': '⚠️', // Warning
|
|
979
|
-
'A': '🛑' // Abort
|
|
980
|
-
};
|
|
981
|
-
return icons[type] || '';
|
|
982
|
-
};
|
|
983
|
-
|
|
984
|
-
// Calculate display width accounting for emoji (2 cells) vs ASCII (1 cell)
|
|
985
|
-
const calcWidth = (str) => {
|
|
986
|
-
if (!str) return 0;
|
|
987
|
-
let width = 0;
|
|
988
|
-
let i = 0;
|
|
989
|
-
while (i < str.length) {
|
|
990
|
-
const code = str.codePointAt(i);
|
|
991
|
-
if (!code) break;
|
|
992
|
-
// Variation selectors (FE00-FE0F) and ZWJ (200D) take 0 width
|
|
993
|
-
if (code >= 0xFE00 && code <= 0xFE0F) {
|
|
994
|
-
i += 1;
|
|
995
|
-
continue;
|
|
996
|
-
}
|
|
997
|
-
if (code === 0x200D) { // Zero width joiner
|
|
998
|
-
i += 1;
|
|
999
|
-
continue;
|
|
1000
|
-
}
|
|
1001
|
-
// Emoji and wide characters take 2 cells
|
|
1002
|
-
if (code > 0xFFFF) {
|
|
1003
|
-
width += 2;
|
|
1004
|
-
i += 2; // Skip surrogate pair
|
|
1005
|
-
} else if (code > 127) {
|
|
1006
|
-
width += 2;
|
|
1007
|
-
i += 1;
|
|
1008
|
-
} else {
|
|
1009
|
-
width += 1;
|
|
1010
|
-
i += 1;
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
return width;
|
|
1014
|
-
};
|
|
1015
|
-
|
|
1016
|
-
// Pad string to display width
|
|
1017
|
-
const padToWidth = (str, width) => {
|
|
1018
|
-
const s = str || '';
|
|
1019
|
-
const currentWidth = calcWidth(s);
|
|
1020
|
-
return s + ' '.repeat(Math.max(0, width - currentWidth));
|
|
1021
|
-
};
|
|
1022
|
-
|
|
1023
|
-
if (success === 'X' || success === true) {
|
|
1024
|
-
console.log(`✅ Pull completed successfully!`);
|
|
1025
|
-
console.log(` Job ID: ${jobId || 'N/A'}`);
|
|
1026
|
-
console.log(` Message: ${message || 'N/A'}`);
|
|
1027
|
-
} else {
|
|
1028
|
-
console.log(`❌ Pull completed with errors!`);
|
|
1029
|
-
console.log(` Job ID: ${jobId || 'N/A'}`);
|
|
1030
|
-
console.log(` Message: ${message || 'N/A'}`);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
// Display error detail if available
|
|
1034
|
-
if (errorDetail && errorDetail.trim()) {
|
|
1035
|
-
console.log(`\n📋 Error Details:`);
|
|
1036
|
-
console.log('─'.repeat(TERM_WIDTH));
|
|
1037
|
-
// Handle escaped newlines from ABAP JSON
|
|
1038
|
-
const formattedDetail = errorDetail.replace(/\\n/g, '\n');
|
|
1039
|
-
console.log(formattedDetail);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// Display all messages as table (from log_messages)
|
|
1043
|
-
if (logMessages && logMessages.length > 0) {
|
|
1044
|
-
console.log(`\n📋 Pull Log (${logMessages.length} messages):`);
|
|
1045
|
-
|
|
1046
|
-
// Calculate column widths based on terminal width
|
|
1047
|
-
const tableWidth = Math.min(TERM_WIDTH, 120);
|
|
1048
|
-
const iconCol = 4; // Fixed width for icon column
|
|
1049
|
-
const objCol = 28;
|
|
1050
|
-
const msgCol = tableWidth - iconCol - objCol - 6; // Account for vertical lines (3 chars)
|
|
1051
|
-
|
|
1052
|
-
// Pad string to display width using calcWidth for emoji support
|
|
1053
|
-
const padToWidth = (str, width) => {
|
|
1054
|
-
const s = str || '';
|
|
1055
|
-
const currentWidth = calcWidth(s);
|
|
1056
|
-
return s + ' '.repeat(Math.max(0, width - currentWidth));
|
|
1057
|
-
};
|
|
1058
|
-
|
|
1059
|
-
const headerLine = '─'.repeat(iconCol) + '┼' + '─'.repeat(objCol) + '┼' + '─'.repeat(msgCol);
|
|
1060
|
-
const headerText = padToWidth('Icon', iconCol) + '│' + padToWidth('Object', objCol) + '│' + 'Message'.substring(0, msgCol);
|
|
1061
|
-
const borderLine = '─'.repeat(tableWidth);
|
|
1062
|
-
|
|
1063
|
-
console.log(borderLine);
|
|
1064
|
-
console.log(headerText);
|
|
1065
|
-
console.log(headerLine);
|
|
1066
|
-
|
|
1067
|
-
for (const msg of logMessages) {
|
|
1068
|
-
const icon = getIcon(msg.TYPE);
|
|
1069
|
-
const objType = msg.OBJ_TYPE || '';
|
|
1070
|
-
const objName = msg.OBJ_NAME || '';
|
|
1071
|
-
const obj = objType && objName ? `${objType} ${objName}` : '';
|
|
1072
|
-
const text = msg.TEXT || '';
|
|
1073
|
-
|
|
1074
|
-
// Truncate long text
|
|
1075
|
-
const displayText = text.length > msgCol ? text.substring(0, msgCol - 3) + '...' : text;
|
|
1076
|
-
|
|
1077
|
-
console.log(padToWidth(icon, iconCol) + '│' + padToWidth(obj.substring(0, objCol), objCol) + '│' + displayText);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// Extract objects with errors from log messages (type 'E' = Error)
|
|
1082
|
-
const failedObjectsFromLog = [];
|
|
1083
|
-
const objectsWithErrors = new Set();
|
|
1084
|
-
|
|
1085
|
-
for (const msg of logMessages) {
|
|
1086
|
-
if (msg.TYPE === 'E' && msg.OBJ_TYPE && msg.OBJ_NAME) {
|
|
1087
|
-
const objKey = `${msg.OBJ_TYPE} ${msg.OBJ_NAME}`;
|
|
1088
|
-
objectsWithErrors.add(objKey);
|
|
1089
|
-
failedObjectsFromLog.push({
|
|
1090
|
-
OBJ_TYPE: msg.OBJ_TYPE,
|
|
1091
|
-
OBJ_NAME: msg.OBJ_NAME,
|
|
1092
|
-
TEXT: msg.TEXT || 'Activation failed',
|
|
1093
|
-
EXCEPTION: msg.EXCEPTION || ''
|
|
1094
|
-
});
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
// Filter activated objects - only include objects without errors
|
|
1099
|
-
const filteredActivatedObjects = activatedObjects.filter(obj => {
|
|
1100
|
-
const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
|
|
1101
|
-
return objKey && !objectsWithErrors.has(objKey);
|
|
1102
|
-
});
|
|
1103
|
-
|
|
1104
|
-
// Display unique activated objects (excluding objects with errors)
|
|
1105
|
-
if (filteredActivatedObjects && filteredActivatedObjects.length > 0) {
|
|
1106
|
-
console.log(`\n📦 Activated Objects (${filteredActivatedObjects.length}):`);
|
|
1107
|
-
console.log('─'.repeat(TERM_WIDTH));
|
|
1108
|
-
for (const obj of filteredActivatedObjects) {
|
|
1109
|
-
console.log(`✅ ${obj.OBJ_TYPE} ${obj.OBJ_NAME}`);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
// Display failed objects log (all error messages, duplicates allowed)
|
|
1114
|
-
if (failedObjectsFromLog.length > 0) {
|
|
1115
|
-
console.log(`\n❌ Failed Objects Log (${failedObjectsFromLog.length} entries):`);
|
|
1116
|
-
console.log('─'.repeat(TERM_WIDTH));
|
|
1117
|
-
for (const obj of failedObjectsFromLog) {
|
|
1118
|
-
const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
|
|
1119
|
-
let text = obj.TEXT || 'Activation failed';
|
|
1120
|
-
// Include exception text if available
|
|
1121
|
-
if (obj.EXCEPTION && obj.EXCEPTION.trim()) {
|
|
1122
|
-
text = `${text}\nException: ${obj.EXCEPTION}`;
|
|
1123
|
-
}
|
|
1124
|
-
console.log(`❌ ${objKey}: ${text}`);
|
|
1125
|
-
}
|
|
1126
|
-
} else if (failedObjects && failedObjects.length > 0) {
|
|
1127
|
-
// Fallback to API failed_objects if no errors in log
|
|
1128
|
-
console.log(`\n❌ Failed Objects Log (${failedObjects.length}):`);
|
|
1129
|
-
console.log('─'.repeat(TERM_WIDTH));
|
|
1130
|
-
for (const obj of failedObjects) {
|
|
1131
|
-
const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
|
|
1132
|
-
let text = obj.TEXT || 'Activation failed';
|
|
1133
|
-
// Include exception text if available
|
|
1134
|
-
if (obj.EXCEPTION && obj.EXCEPTION.trim()) {
|
|
1135
|
-
text = `${text}\nException: ${obj.EXCEPTION}`;
|
|
1136
|
-
}
|
|
1137
|
-
console.log(`❌ ${objKey}: ${text}`);
|
|
1138
|
-
}
|
|
1139
|
-
} else if (failedCount > 0) {
|
|
1140
|
-
console.log(`\n❌ Failed Objects Log (${failedCount})`);
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
return result;
|
|
1144
|
-
} catch (error) {
|
|
1145
|
-
console.error(`\n❌ Error: ${error.message}`);
|
|
1146
|
-
process.exit(1);
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
/**
|
|
1151
|
-
* Copy a file if source exists (helper for init --update)
|
|
1152
|
-
* @param {string} srcPath - Source file path
|
|
1153
|
-
* @param {string} destPath - Destination file path
|
|
1154
|
-
* @param {string} label - Label for console output
|
|
1155
|
-
* @param {boolean} createParentDir - Whether to create parent directory
|
|
1156
|
-
* @returns {Promise<void>}
|
|
1157
|
-
*/
|
|
1158
|
-
async function copyFileIfExists(srcPath, destPath, label, createParentDir = false) {
|
|
1159
|
-
try {
|
|
1160
|
-
if (fs.existsSync(srcPath)) {
|
|
1161
|
-
if (createParentDir) {
|
|
1162
|
-
const parentDir = pathModule.dirname(destPath);
|
|
1163
|
-
if (!fs.existsSync(parentDir)) {
|
|
1164
|
-
fs.mkdirSync(parentDir, { recursive: true });
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
fs.copyFileSync(srcPath, destPath);
|
|
1168
|
-
console.log(`✅ Updated ${label}`);
|
|
1169
|
-
} else {
|
|
1170
|
-
console.log(`⚠️ ${label} not found in abapgit-agent`);
|
|
1171
|
-
}
|
|
1172
|
-
} catch (error) {
|
|
1173
|
-
console.error(`Error copying ${label}: ${error.message}`);
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
/**
|
|
1178
|
-
* Copy guidelines folder (helper for init --update)
|
|
1179
|
-
* @param {string} srcPath - Source folder path
|
|
1180
|
-
* @param {string} destPath - Destination folder path
|
|
1181
|
-
* @param {boolean} overwrite - Whether to overwrite existing files
|
|
1182
|
-
* @returns {Promise<void>}
|
|
1183
|
-
*/
|
|
1184
|
-
async function copyGuidelinesFolder(srcPath, destPath, overwrite = false) {
|
|
1185
|
-
try {
|
|
1186
|
-
if (fs.existsSync(srcPath)) {
|
|
1187
|
-
// Create destination directory if needed
|
|
1188
|
-
if (!fs.existsSync(destPath)) {
|
|
1189
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
const files = fs.readdirSync(srcPath);
|
|
1193
|
-
let copied = 0;
|
|
1194
|
-
|
|
1195
|
-
for (const file of files) {
|
|
1196
|
-
if (file.endsWith('.md')) {
|
|
1197
|
-
const srcFile = pathModule.join(srcPath, file);
|
|
1198
|
-
const destFile = pathModule.join(destPath, file);
|
|
1199
|
-
|
|
1200
|
-
// Skip if file exists and not overwriting
|
|
1201
|
-
if (fs.existsSync(destFile) && !overwrite) {
|
|
1202
|
-
continue;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
fs.copyFileSync(srcFile, destFile);
|
|
1206
|
-
copied++;
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
if (copied > 0) {
|
|
1211
|
-
console.log(`✅ Updated guidelines/ (${copied} files)`);
|
|
1212
|
-
} else {
|
|
1213
|
-
console.log(`⚠️ No guideline files found`);
|
|
1214
|
-
}
|
|
1215
|
-
} else {
|
|
1216
|
-
console.log(`⚠️ guidelines folder not found in abapgit-agent`);
|
|
1217
|
-
}
|
|
1218
|
-
} catch (error) {
|
|
1219
|
-
console.error(`Error copying guidelines: ${error.message}`);
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
/**
|
|
1224
|
-
* Run init command - Initialize local configuration
|
|
1225
|
-
*/
|
|
1226
|
-
async function runInit(args) {
|
|
1227
|
-
const folderArgIndex = args.indexOf('--folder');
|
|
1228
|
-
const packageArgIndex = args.indexOf('--package');
|
|
1229
|
-
const updateMode = args.includes('--update');
|
|
1230
|
-
|
|
1231
|
-
// Get parameters
|
|
1232
|
-
let folder = folderArgIndex !== -1 && folderArgIndex + 1 < args.length
|
|
1233
|
-
? args[folderArgIndex + 1]
|
|
1234
|
-
: '/src/';
|
|
1235
|
-
|
|
1236
|
-
// Normalize folder path: ensure it starts with / and ends with /
|
|
1237
|
-
folder = folder.trim();
|
|
1238
|
-
if (!folder.startsWith('/')) {
|
|
1239
|
-
folder = '/' + folder;
|
|
1240
|
-
}
|
|
1241
|
-
if (!folder.endsWith('/')) {
|
|
1242
|
-
folder = folder + '/';
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
const packageName = packageArgIndex !== -1 && packageArgIndex + 1 < args.length
|
|
1246
|
-
? args[packageArgIndex + 1]
|
|
1247
|
-
: null;
|
|
1248
|
-
|
|
1249
|
-
// Validate package is required for non-update mode
|
|
1250
|
-
if (!updateMode && !packageName) {
|
|
1251
|
-
console.error('Error: --package is required');
|
|
1252
|
-
console.error('Usage: abapgit-agent init --folder /src --package ZMY_PACKAGE');
|
|
1253
|
-
console.error(' abapgit-agent init --update # Update files to latest version');
|
|
1254
|
-
process.exit(1);
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
// Check if current folder is git repo root
|
|
1258
|
-
const gitDir = pathModule.join(process.cwd(), '.git');
|
|
1259
|
-
if (!fs.existsSync(gitDir)) {
|
|
1260
|
-
console.error('Error: Current folder is not a git repository.');
|
|
1261
|
-
console.error('Run this command from the root folder of your git repository.');
|
|
1262
|
-
process.exit(1);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// In update mode, just copy the files and return
|
|
1266
|
-
if (updateMode) {
|
|
1267
|
-
console.log(`\n🔄 Updating abapGit Agent files`);
|
|
1268
|
-
console.log('');
|
|
1269
|
-
|
|
1270
|
-
// Copy CLAUDE.md
|
|
1271
|
-
await copyFileIfExists(
|
|
1272
|
-
pathModule.join(__dirname, '..', 'abap', 'CLAUDE.md'),
|
|
1273
|
-
pathModule.join(process.cwd(), 'CLAUDE.md'),
|
|
1274
|
-
'CLAUDE.md'
|
|
1275
|
-
);
|
|
1276
|
-
|
|
1277
|
-
// Copy copilot-instructions.md
|
|
1278
|
-
await copyFileIfExists(
|
|
1279
|
-
pathModule.join(__dirname, '..', 'abap', '.github', 'copilot-instructions.md'),
|
|
1280
|
-
pathModule.join(process.cwd(), '.github', 'copilot-instructions.md'),
|
|
1281
|
-
'.github/copilot-instructions.md',
|
|
1282
|
-
true // create parent dir
|
|
1283
|
-
);
|
|
1284
|
-
|
|
1285
|
-
// Copy guidelines folder to project root
|
|
1286
|
-
await copyGuidelinesFolder(
|
|
1287
|
-
pathModule.join(__dirname, '..', 'abap', 'guidelines'),
|
|
1288
|
-
pathModule.join(process.cwd(), 'guidelines'),
|
|
1289
|
-
true // overwrite
|
|
1290
|
-
);
|
|
1291
|
-
|
|
1292
|
-
console.log(`
|
|
1293
|
-
📋 Update complete!
|
|
1294
|
-
Run 'abapgit-agent ref --list-topics' to see available topics.
|
|
1295
|
-
`);
|
|
1296
|
-
return;
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
// Validate package is required
|
|
1300
|
-
if (!packageName) {
|
|
1301
|
-
console.error('Error: --package is required');
|
|
1302
|
-
console.error('Usage: abapgit-agent init --folder /src --package ZMY_PACKAGE');
|
|
1303
|
-
console.error(' abapgit-agent init --update # Update files to latest version');
|
|
1304
|
-
process.exit(1);
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
console.log(`\n🚀 Initializing abapGit Agent for local repository`);
|
|
1308
|
-
console.log(` Folder: ${folder}`);
|
|
1309
|
-
console.log(` Package: ${packageName}`);
|
|
1310
|
-
console.log('');
|
|
1311
|
-
|
|
1312
|
-
// Detect git remote URL
|
|
1313
|
-
const gitUrl = getGitRemoteUrl();
|
|
1314
|
-
if (!gitUrl) {
|
|
1315
|
-
console.error('Error: No git remote configured.');
|
|
1316
|
-
console.error('Configure a remote with: git remote add origin <url>');
|
|
1317
|
-
process.exit(1);
|
|
1318
|
-
}
|
|
1319
|
-
console.log(`📌 Git remote: ${gitUrl}`);
|
|
1320
|
-
|
|
1321
|
-
// Check if .abapGitAgent already exists
|
|
1322
|
-
const configPath = pathModule.join(process.cwd(), '.abapGitAgent');
|
|
1323
|
-
if (fs.existsSync(configPath)) {
|
|
1324
|
-
console.error('Error: .abapGitAgent already exists.');
|
|
1325
|
-
console.error('To reinitialize, delete the existing file first.');
|
|
1326
|
-
process.exit(1);
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
// Copy .abapGitAgent.example to .abapGitAgent
|
|
1330
|
-
const samplePath = pathModule.join(__dirname, '..', '.abapGitAgent.example');
|
|
1331
|
-
if (!fs.existsSync(samplePath)) {
|
|
1332
|
-
console.error('Error: .abapGitAgent.example not found.');
|
|
1333
|
-
process.exit(1);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
try {
|
|
1337
|
-
// Read sample and update with package/folder
|
|
1338
|
-
const sampleContent = fs.readFileSync(samplePath, 'utf8');
|
|
1339
|
-
const config = JSON.parse(sampleContent);
|
|
1340
|
-
config.package = packageName;
|
|
1341
|
-
config.folder = folder;
|
|
1342
|
-
|
|
1343
|
-
// Write updated config
|
|
1344
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
1345
|
-
console.log(`✅ Created .abapGitAgent`);
|
|
1346
|
-
} catch (error) {
|
|
1347
|
-
console.error(`Error creating .abapGitAgent: ${error.message}`);
|
|
1348
|
-
process.exit(1);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// Update .gitignore
|
|
1352
|
-
const gitignorePath = pathModule.join(process.cwd(), '.gitignore');
|
|
1353
|
-
const ignoreEntries = ['.abapGitAgent', '.abapgit_agent_cookies.txt'];
|
|
1354
|
-
let existingIgnore = '';
|
|
1355
|
-
|
|
1356
|
-
if (fs.existsSync(gitignorePath)) {
|
|
1357
|
-
existingIgnore = fs.readFileSync(gitignorePath, 'utf8');
|
|
1358
|
-
}
|
|
22
|
+
// Import utilities
|
|
23
|
+
const gitUtils = require('../src/utils/git-utils');
|
|
24
|
+
const versionCheck = require('../src/utils/version-check');
|
|
25
|
+
const validators = require('../src/utils/validators');
|
|
26
|
+
const { AbapHttp } = require('../src/utils/abap-http');
|
|
27
|
+
const { loadConfig, getTransport, isAbapIntegrationEnabled } = require('../src/config');
|
|
1359
28
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
for (const entry of ignoreEntries) {
|
|
1364
|
-
if (!newIgnoreContent.includes(entry)) {
|
|
1365
|
-
if (newIgnoreContent && !newIgnoreContent.endsWith('\n')) {
|
|
1366
|
-
newIgnoreContent += '\n';
|
|
1367
|
-
}
|
|
1368
|
-
newIgnoreContent += entry + '\n';
|
|
1369
|
-
updated = true;
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
if (updated) {
|
|
1374
|
-
fs.writeFileSync(gitignorePath, newIgnoreContent);
|
|
1375
|
-
console.log(`✅ Updated .gitignore`);
|
|
1376
|
-
} else {
|
|
1377
|
-
fs.writeFileSync(gitignorePath, newIgnoreContent);
|
|
1378
|
-
console.log(`✅ .gitignore already up to date`);
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// Copy CLAUDE.md
|
|
1382
|
-
const claudeMdPath = pathModule.join(__dirname, '..', 'abap', 'CLAUDE.md');
|
|
1383
|
-
const localClaudeMdPath = pathModule.join(process.cwd(), 'CLAUDE.md');
|
|
1384
|
-
try {
|
|
1385
|
-
if (fs.existsSync(claudeMdPath)) {
|
|
1386
|
-
fs.copyFileSync(claudeMdPath, localClaudeMdPath);
|
|
1387
|
-
console.log(`✅ Created CLAUDE.md`);
|
|
1388
|
-
} else {
|
|
1389
|
-
console.log(`⚠️ CLAUDE.md not found in abap/ directory`);
|
|
1390
|
-
}
|
|
1391
|
-
} catch (error) {
|
|
1392
|
-
console.error(`Error copying CLAUDE.md: ${error.message}`);
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// Copy copilot-instructions.md for GitHub Copilot
|
|
1396
|
-
const copilotMdPath = pathModule.join(__dirname, '..', 'abap', '.github', 'copilot-instructions.md');
|
|
1397
|
-
const githubDir = pathModule.join(process.cwd(), '.github');
|
|
1398
|
-
const localCopilotMdPath = pathModule.join(githubDir, 'copilot-instructions.md');
|
|
1399
|
-
try {
|
|
1400
|
-
if (fs.existsSync(copilotMdPath)) {
|
|
1401
|
-
// Ensure .github directory exists
|
|
1402
|
-
if (!fs.existsSync(githubDir)) {
|
|
1403
|
-
fs.mkdirSync(githubDir, { recursive: true });
|
|
1404
|
-
}
|
|
1405
|
-
fs.copyFileSync(copilotMdPath, localCopilotMdPath);
|
|
1406
|
-
console.log(`✅ Created .github/copilot-instructions.md`);
|
|
1407
|
-
} else {
|
|
1408
|
-
console.log(`⚠️ copilot-instructions.md not found in abap/ directory`);
|
|
1409
|
-
}
|
|
1410
|
-
} catch (error) {
|
|
1411
|
-
console.error(`Error copying copilot-instructions.md: ${error.message}`);
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
// Copy guidelines folder to project root
|
|
1415
|
-
const guidelinesSrcPath = pathModule.join(__dirname, '..', 'abap', 'guidelines');
|
|
1416
|
-
const guidelinesDestPath = pathModule.join(process.cwd(), 'guidelines');
|
|
1417
|
-
try {
|
|
1418
|
-
if (fs.existsSync(guidelinesSrcPath)) {
|
|
1419
|
-
if (!fs.existsSync(guidelinesDestPath)) {
|
|
1420
|
-
// Create guidelines directory
|
|
1421
|
-
fs.mkdirSync(guidelinesDestPath, { recursive: true });
|
|
1422
|
-
// Copy all files from guidelines folder
|
|
1423
|
-
const files = fs.readdirSync(guidelinesSrcPath);
|
|
1424
|
-
for (const file of files) {
|
|
1425
|
-
if (file.endsWith('.md')) {
|
|
1426
|
-
fs.copyFileSync(
|
|
1427
|
-
pathModule.join(guidelinesSrcPath, file),
|
|
1428
|
-
pathModule.join(guidelinesDestPath, file)
|
|
1429
|
-
);
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
console.log(`✅ Created guidelines/ (${files.filter(f => f.endsWith('.md')).length} files)`);
|
|
1433
|
-
} else {
|
|
1434
|
-
console.log(`⚠️ guidelines/ already exists, skipped`);
|
|
1435
|
-
}
|
|
1436
|
-
} else {
|
|
1437
|
-
console.log(`⚠️ guidelines folder not found in abap/ directory`);
|
|
1438
|
-
}
|
|
1439
|
-
} catch (error) {
|
|
1440
|
-
console.error(`Error copying guidelines: ${error.message}`);
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
// Create folder
|
|
1444
|
-
const folderPath = pathModule.join(process.cwd(), folder);
|
|
1445
|
-
try {
|
|
1446
|
-
if (!fs.existsSync(folderPath)) {
|
|
1447
|
-
fs.mkdirSync(folderPath, { recursive: true });
|
|
1448
|
-
console.log(`✅ Created folder: ${folder}`);
|
|
1449
|
-
|
|
1450
|
-
// Create .gitkeep
|
|
1451
|
-
const gitkeepPath = pathModule.join(folderPath, '.gitkeep');
|
|
1452
|
-
fs.writeFileSync(gitkeepPath, '');
|
|
1453
|
-
} else {
|
|
1454
|
-
console.log(`✅ Folder already exists: ${folder}`);
|
|
1455
|
-
}
|
|
1456
|
-
} catch (error) {
|
|
1457
|
-
console.error(`Error creating folder: ${error.message}`);
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
console.log(`
|
|
1461
|
-
📋 Next steps:
|
|
1462
|
-
1. Edit .abapGitAgent with your ABAP credentials (host, user, password)
|
|
1463
|
-
2. Run 'abapgit-agent create --import' to create online repository
|
|
1464
|
-
3. Run 'abapgit-agent pull' to activate objects
|
|
1465
|
-
`);
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
/**
|
|
1469
|
-
* Check agent health
|
|
1470
|
-
*/
|
|
1471
|
-
async function healthCheck() {
|
|
1472
|
-
try {
|
|
1473
|
-
const result = await request('GET', '/sap/bc/z_abapgit_agent/health');
|
|
1474
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1475
|
-
return result;
|
|
1476
|
-
} catch (error) {
|
|
1477
|
-
console.error(`Health check failed: ${error.message}`);
|
|
1478
|
-
process.exit(1);
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
29
|
+
// Get terminal width for responsive table
|
|
30
|
+
const getTermWidth = () => process.stdout.columns || 80;
|
|
31
|
+
const TERM_WIDTH = getTermWidth();
|
|
1481
32
|
|
|
1482
|
-
/**
|
|
1483
|
-
* Main CLI
|
|
1484
|
-
*/
|
|
1485
33
|
async function main() {
|
|
1486
34
|
const args = process.argv.slice(2);
|
|
1487
35
|
const command = args[0];
|
|
1488
36
|
|
|
1489
|
-
//
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
37
|
+
// Load command modules (Phase 2 refactoring)
|
|
38
|
+
const commandModules = {
|
|
39
|
+
help: require('../src/commands/help'),
|
|
40
|
+
health: require('../src/commands/health'),
|
|
41
|
+
status: require('../src/commands/status'),
|
|
42
|
+
create: require('../src/commands/create'),
|
|
43
|
+
delete: require('../src/commands/delete'),
|
|
44
|
+
import: require('../src/commands/import'),
|
|
45
|
+
inspect: require('../src/commands/inspect'),
|
|
46
|
+
unit: require('../src/commands/unit'),
|
|
47
|
+
syntax: require('../src/commands/syntax'),
|
|
48
|
+
tree: require('../src/commands/tree'),
|
|
49
|
+
list: require('../src/commands/list'),
|
|
50
|
+
view: require('../src/commands/view'),
|
|
51
|
+
preview: require('../src/commands/preview'),
|
|
52
|
+
where: require('../src/commands/where'),
|
|
53
|
+
ref: require('../src/commands/ref'),
|
|
54
|
+
init: require('../src/commands/init'),
|
|
55
|
+
pull: require('../src/commands/pull')
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Check if this is a modular command
|
|
59
|
+
if (commandModules[command] || command === '--help' || command === '-h') {
|
|
60
|
+
const cmd = command === '--help' || command === '-h' ? commandModules.help : commandModules[command];
|
|
61
|
+
|
|
62
|
+
// Check if command requires ABAP configuration
|
|
63
|
+
if (cmd.requiresAbapConfig && !isAbapIntegrationEnabled()) {
|
|
1494
64
|
console.log(`
|
|
1495
65
|
⚠️ ABAP Git Agent not configured for this repository.
|
|
1496
66
|
|
|
@@ -1510,1397 +80,36 @@ To enable integration:
|
|
|
1510
80
|
`);
|
|
1511
81
|
process.exit(1);
|
|
1512
82
|
}
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
// Version compatibility check for commands that interact with ABAP
|
|
1516
|
-
const abapCommands = ['create', 'delete', 'import', 'pull', 'inspect', 'unit', 'tree', 'view', 'preview', 'list'];
|
|
1517
|
-
if (command && abapCommands.includes(command)) {
|
|
1518
|
-
await checkVersionCompatibility();
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
try {
|
|
1522
|
-
switch (command) {
|
|
1523
|
-
case 'init':
|
|
1524
|
-
await runInit(args);
|
|
1525
|
-
return; // Don't check ABAP integration for init
|
|
1526
|
-
|
|
1527
|
-
case 'create': {
|
|
1528
|
-
const helpIndex = args.findIndex(a => a === '--help' || a === '-h');
|
|
1529
|
-
|
|
1530
|
-
// Show help if requested
|
|
1531
|
-
if (helpIndex !== -1) {
|
|
1532
|
-
console.log(`
|
|
1533
|
-
Usage:
|
|
1534
|
-
abapgit-agent create
|
|
1535
|
-
|
|
1536
|
-
Description:
|
|
1537
|
-
Create abapGit online repository in ABAP system.
|
|
1538
|
-
Auto-detects URL from git remote and package from .abapGitAgent.
|
|
1539
|
-
|
|
1540
|
-
Prerequisites:
|
|
1541
|
-
- Run "abapgit-agent init" first
|
|
1542
|
-
- Edit .abapGitAgent with credentials (host, user, password)
|
|
1543
|
-
|
|
1544
|
-
Examples:
|
|
1545
|
-
abapgit-agent create # Create repo in ABAP
|
|
1546
|
-
`);
|
|
1547
|
-
return;
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
// Get parameters from config
|
|
1551
|
-
const config = loadConfig();
|
|
1552
|
-
const repoUrl = getGitRemoteUrl();
|
|
1553
|
-
|
|
1554
|
-
if (!repoUrl) {
|
|
1555
|
-
console.error('Error: No git remote configured. Please configure a remote origin.');
|
|
1556
|
-
process.exit(1);
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
if (!config.package) {
|
|
1560
|
-
console.error('Error: Package not configured. Run "abapgit-agent init" first or set package in .abapGitAgent.');
|
|
1561
|
-
process.exit(1);
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
const branch = getGitBranch();
|
|
1565
|
-
|
|
1566
|
-
// Extract repo name from git URL
|
|
1567
|
-
const repoName = repoUrl.split('/').pop().replace('.git', '');
|
|
1568
|
-
|
|
1569
|
-
console.log(`\n🚀 Creating online repository`);
|
|
1570
|
-
console.log(` URL: ${repoUrl}`);
|
|
1571
|
-
console.log(` Package: ${config.package}`);
|
|
1572
|
-
console.log(` Folder: ${config.folder || '/src/'}`);
|
|
1573
|
-
console.log(` Name: ${repoName}`);
|
|
1574
|
-
console.log(` Branch: ${branch}`);
|
|
1575
|
-
|
|
1576
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
1577
|
-
|
|
1578
|
-
const data = {
|
|
1579
|
-
url: repoUrl,
|
|
1580
|
-
package: config.package,
|
|
1581
|
-
name: repoName,
|
|
1582
|
-
branch: branch,
|
|
1583
|
-
folder: config.folder || '/src/'
|
|
1584
|
-
};
|
|
1585
|
-
|
|
1586
|
-
if (config.gitUsername) {
|
|
1587
|
-
data.username = config.gitUsername;
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
if (config.gitPassword) {
|
|
1591
|
-
data.password = config.gitPassword;
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/create', data, { csrfToken });
|
|
1595
|
-
|
|
1596
|
-
console.log('\n');
|
|
1597
|
-
|
|
1598
|
-
// Handle uppercase keys from ABAP
|
|
1599
|
-
const success = result.SUCCESS || result.success;
|
|
1600
|
-
const repoKey = result.REPO_KEY || result.repo_key;
|
|
1601
|
-
const createdRepoName = result.REPO_NAME || result.repo_name;
|
|
1602
|
-
const message = result.MESSAGE || result.message;
|
|
1603
|
-
const error = result.ERROR || result.error;
|
|
1604
|
-
|
|
1605
|
-
if (success === 'X' || success === true) {
|
|
1606
|
-
console.log(`✅ Repository created successfully!`);
|
|
1607
|
-
console.log(` URL: ${repoUrl}`);
|
|
1608
|
-
console.log(` Package: ${config.package}`);
|
|
1609
|
-
console.log(` Name: ${createdRepoName || repoName}`);
|
|
1610
|
-
} else {
|
|
1611
|
-
console.log(`❌ Failed to create repository`);
|
|
1612
|
-
console.log(` Error: ${error || message || 'Unknown error'}`);
|
|
1613
|
-
process.exit(1);
|
|
1614
|
-
}
|
|
1615
|
-
break;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
case 'delete': {
|
|
1619
|
-
const helpIndex = args.findIndex(a => a === '--help' || a === '-h');
|
|
1620
|
-
|
|
1621
|
-
// Show help if requested
|
|
1622
|
-
if (helpIndex !== -1) {
|
|
1623
|
-
console.log(`
|
|
1624
|
-
Usage:
|
|
1625
|
-
abapgit-agent delete
|
|
1626
|
-
|
|
1627
|
-
Description:
|
|
1628
|
-
Delete abapGit online repository from ABAP system.
|
|
1629
|
-
Auto-detects URL from git remote of current directory.
|
|
1630
|
-
|
|
1631
|
-
Prerequisites:
|
|
1632
|
-
- Run "abapgit-agent create" first
|
|
1633
|
-
|
|
1634
|
-
Examples:
|
|
1635
|
-
abapgit-agent delete # Delete repo for current git remote
|
|
1636
|
-
`);
|
|
1637
|
-
return;
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
// Get URL from current git remote
|
|
1641
|
-
const config = loadConfig();
|
|
1642
|
-
const repoUrl = getGitRemoteUrl();
|
|
1643
|
-
|
|
1644
|
-
if (!repoUrl) {
|
|
1645
|
-
console.error('Error: No git remote configured. Please configure a remote origin.');
|
|
1646
|
-
process.exit(1);
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
console.log(`\n🗑️ Deleting online repository`);
|
|
1650
|
-
console.log(` URL: ${repoUrl}`);
|
|
1651
|
-
|
|
1652
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
1653
|
-
|
|
1654
|
-
const data = {
|
|
1655
|
-
url: repoUrl
|
|
1656
|
-
};
|
|
1657
|
-
|
|
1658
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/delete', data, { csrfToken });
|
|
1659
|
-
|
|
1660
|
-
console.log('\n');
|
|
1661
|
-
|
|
1662
|
-
// Handle uppercase keys from ABAP
|
|
1663
|
-
const success = result.SUCCESS || result.success;
|
|
1664
|
-
const repoKey = result.REPO_KEY || result.repo_key;
|
|
1665
|
-
const message = result.MESSAGE || result.message;
|
|
1666
|
-
const error = result.ERROR || result.error;
|
|
1667
|
-
|
|
1668
|
-
if (success === 'X' || success === true) {
|
|
1669
|
-
console.log(`✅ Repository deleted successfully!`);
|
|
1670
|
-
console.log(` Key: ${repoKey}`);
|
|
1671
|
-
} else {
|
|
1672
|
-
console.log(`❌ Failed to delete repository`);
|
|
1673
|
-
console.log(` Error: ${error || message || 'Unknown error'}`);
|
|
1674
|
-
process.exit(1);
|
|
1675
|
-
}
|
|
1676
|
-
break;
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
case 'import': {
|
|
1680
|
-
const helpIndex = args.findIndex(a => a === '--help' || a === '-h');
|
|
1681
|
-
|
|
1682
|
-
// Show help if requested
|
|
1683
|
-
if (helpIndex !== -1) {
|
|
1684
|
-
console.log(`
|
|
1685
|
-
Usage:
|
|
1686
|
-
abapgit-agent import [--message <message>]
|
|
1687
|
-
|
|
1688
|
-
Description:
|
|
1689
|
-
Import existing objects from package to git repository.
|
|
1690
|
-
Uses the git remote URL to find the abapGit online repository.
|
|
1691
|
-
|
|
1692
|
-
Prerequisites:
|
|
1693
|
-
- Run "abapgit-agent create" first or create repository in abapGit UI
|
|
1694
|
-
- Package must have objects to import
|
|
1695
|
-
|
|
1696
|
-
Options:
|
|
1697
|
-
--message Commit message (default: "feat: initial import from ABAP package <package>")
|
|
1698
|
-
|
|
1699
|
-
Examples:
|
|
1700
|
-
abapgit-agent import
|
|
1701
|
-
abapgit-agent import --message "Initial import from SAP"
|
|
1702
|
-
`);
|
|
1703
|
-
return;
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
// Get parameters from config
|
|
1707
|
-
const config = loadConfig();
|
|
1708
|
-
const repoUrl = getGitRemoteUrl();
|
|
1709
|
-
|
|
1710
|
-
if (!repoUrl) {
|
|
1711
|
-
console.error('Error: No git remote configured. Please configure a remote origin.');
|
|
1712
|
-
process.exit(1);
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
const messageArgIndex = args.indexOf('--message');
|
|
1716
|
-
let commitMessage = null;
|
|
1717
|
-
if (messageArgIndex !== -1 && messageArgIndex + 1 < args.length) {
|
|
1718
|
-
commitMessage = args[messageArgIndex + 1];
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
console.log(`\n📦 Importing objects to git repository`);
|
|
1722
|
-
console.log(` URL: ${repoUrl}`);
|
|
1723
|
-
if (commitMessage) {
|
|
1724
|
-
console.log(` Message: ${commitMessage}`);
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
1728
|
-
|
|
1729
|
-
const data = {
|
|
1730
|
-
url: repoUrl
|
|
1731
|
-
};
|
|
1732
|
-
|
|
1733
|
-
if (commitMessage) {
|
|
1734
|
-
data.message = commitMessage;
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
if (config.gitUsername) {
|
|
1738
|
-
data.username = config.gitUsername;
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
if (config.gitPassword) {
|
|
1742
|
-
data.password = config.gitPassword;
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/import', data, { csrfToken });
|
|
1746
|
-
|
|
1747
|
-
console.log('\n');
|
|
1748
|
-
|
|
1749
|
-
// Handle uppercase keys from ABAP
|
|
1750
|
-
const success = result.SUCCESS || result.success;
|
|
1751
|
-
const filesStaged = result.FILES_STAGED || result.files_staged;
|
|
1752
|
-
const abapCommitMessage = result.COMMIT_MESSAGE || result.commit_message;
|
|
1753
|
-
const error = result.ERROR || result.error;
|
|
1754
|
-
|
|
1755
|
-
if (success === 'X' || success === true) {
|
|
1756
|
-
console.log(`✅ Objects imported successfully!`);
|
|
1757
|
-
console.log(` Files staged: ${filesStaged}`);
|
|
1758
|
-
console.log(` Commit: ${commitMessage || abapCommitMessage}`);
|
|
1759
|
-
} else {
|
|
1760
|
-
console.log(`❌ Import failed`);
|
|
1761
|
-
console.log(` Error: ${error || resultMessage || 'Unknown error'}`);
|
|
1762
|
-
process.exit(1);
|
|
1763
|
-
}
|
|
1764
|
-
break;
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
case 'pull':
|
|
1768
|
-
const urlArgIndex = args.indexOf('--url');
|
|
1769
|
-
const branchArgIndex = args.indexOf('--branch');
|
|
1770
|
-
const filesArgIndex = args.indexOf('--files');
|
|
1771
|
-
const transportArgIndex = args.indexOf('--transport');
|
|
1772
|
-
|
|
1773
|
-
// Auto-detect git remote URL if not provided
|
|
1774
|
-
let gitUrl = urlArgIndex !== -1 ? args[urlArgIndex + 1] : null;
|
|
1775
|
-
let branch = branchArgIndex !== -1 ? args[branchArgIndex + 1] : getGitBranch();
|
|
1776
|
-
let files = null;
|
|
1777
|
-
|
|
1778
|
-
// Transport: CLI arg takes priority, then config/environment, then null
|
|
1779
|
-
let transportRequest = null;
|
|
1780
|
-
if (transportArgIndex !== -1 && transportArgIndex + 1 < args.length) {
|
|
1781
|
-
// Explicit --transport argument
|
|
1782
|
-
transportRequest = args[transportArgIndex + 1];
|
|
1783
|
-
} else {
|
|
1784
|
-
// Fall back to config or environment variable
|
|
1785
|
-
transportRequest = getTransport();
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
if (filesArgIndex !== -1 && filesArgIndex + 1 < args.length) {
|
|
1789
|
-
files = args[filesArgIndex + 1].split(',').map(f => f.trim());
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
if (!gitUrl) {
|
|
1793
|
-
gitUrl = getGitRemoteUrl();
|
|
1794
|
-
if (!gitUrl) {
|
|
1795
|
-
console.error('Error: Not in a git repository or no remote configured.');
|
|
1796
|
-
console.error('Either run from a git repo, or specify --url <git-url>');
|
|
1797
|
-
process.exit(1);
|
|
1798
|
-
}
|
|
1799
|
-
console.log(`📌 Auto-detected git remote: ${gitUrl}`);
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
await pull(gitUrl, branch, files, transportRequest);
|
|
1803
|
-
break;
|
|
1804
|
-
|
|
1805
|
-
case 'health':
|
|
1806
|
-
await healthCheck();
|
|
1807
|
-
break;
|
|
1808
|
-
|
|
1809
|
-
case 'status':
|
|
1810
|
-
if (isAbapIntegrationEnabled()) {
|
|
1811
|
-
console.log('✅ ABAP Git Agent is ENABLED');
|
|
1812
|
-
console.log(' Config location:', pathModule.join(process.cwd(), '.abapGitAgent'));
|
|
1813
|
-
|
|
1814
|
-
// Check if repo exists in ABAP
|
|
1815
|
-
const config = loadConfig();
|
|
1816
|
-
const repoUrl = getGitRemoteUrl();
|
|
1817
|
-
|
|
1818
|
-
if (repoUrl) {
|
|
1819
|
-
try {
|
|
1820
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
1821
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/status', { url: repoUrl }, { csrfToken });
|
|
1822
|
-
|
|
1823
|
-
const status = result.status || result.STATUS || result.SUCCESS;
|
|
1824
|
-
if (status === 'Found' || status === 'X' || status === true) {
|
|
1825
|
-
console.log(' Repository: ✅ Created');
|
|
1826
|
-
console.log(` Package: ${result.PACKAGE || result.package}`);
|
|
1827
|
-
console.log(` URL: ${repoUrl}`);
|
|
1828
|
-
console.log(` Key: ${result.REPO_KEY || result.repo_key}`);
|
|
1829
|
-
} else {
|
|
1830
|
-
console.log(' Repository: ❌ Not created in ABAP system');
|
|
1831
|
-
console.log(` URL: ${repoUrl}`);
|
|
1832
|
-
console.log(' Run "abapgit-agent create" to create repository');
|
|
1833
|
-
}
|
|
1834
|
-
} catch (err) {
|
|
1835
|
-
console.log(' Repository: ⚠️ Unable to check status');
|
|
1836
|
-
console.log(' Error:', err.message);
|
|
1837
|
-
}
|
|
1838
|
-
} else {
|
|
1839
|
-
console.log(' No git remote configured');
|
|
1840
|
-
}
|
|
1841
|
-
} else {
|
|
1842
|
-
console.log('❌ ABAP Git Agent is NOT configured');
|
|
1843
|
-
}
|
|
1844
|
-
break;
|
|
1845
|
-
|
|
1846
|
-
case 'inspect': {
|
|
1847
|
-
const filesArgIndex = args.indexOf('--files');
|
|
1848
|
-
if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
|
|
1849
|
-
console.error('Error: --files parameter required');
|
|
1850
|
-
console.error('Usage: abapgit-agent inspect --files <file1>,<file2>,... [--variant <check-variant>]');
|
|
1851
|
-
console.error('Example: abapgit-agent inspect --files zcl_my_class.clas.abap');
|
|
1852
|
-
console.error('Example: abapgit-agent inspect --files zcl_my_class.clas.abap --variant ALL_CHECKS');
|
|
1853
|
-
process.exit(1);
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
const filesSyntaxCheck = args[filesArgIndex + 1].split(',').map(f => f.trim());
|
|
1857
|
-
|
|
1858
|
-
// Parse optional --variant parameter
|
|
1859
|
-
const variantArgIndex = args.indexOf('--variant');
|
|
1860
|
-
const variant = variantArgIndex !== -1 ? args[variantArgIndex + 1] : null;
|
|
1861
|
-
|
|
1862
|
-
console.log(`\n Inspect for ${filesSyntaxCheck.length} file(s)`);
|
|
1863
|
-
if (variant) {
|
|
1864
|
-
console.log(` Using variant: ${variant}`);
|
|
1865
|
-
}
|
|
1866
|
-
console.log('');
|
|
1867
|
-
|
|
1868
|
-
const config = loadConfig();
|
|
1869
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
1870
|
-
|
|
1871
|
-
// Send all files in one request
|
|
1872
|
-
const results = await inspectAllFiles(filesSyntaxCheck, csrfToken, config, variant);
|
|
1873
|
-
|
|
1874
|
-
// Process results
|
|
1875
|
-
for (const result of results) {
|
|
1876
|
-
await processInspectResult(result);
|
|
1877
|
-
}
|
|
1878
|
-
break;
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
case 'unit': {
|
|
1882
|
-
const filesArgIndex = args.indexOf('--files');
|
|
1883
|
-
if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
|
|
1884
|
-
console.error('Error: --files parameter required');
|
|
1885
|
-
console.error('Usage: abapgit-agent unit --files <file1>,<file2>,... [--coverage]');
|
|
1886
|
-
console.error('Example: abapgit-agent unit --files zcl_my_test.clas.abap');
|
|
1887
|
-
console.error('Example: abapgit-agent unit --files zcl_my_test.clas.abap --coverage');
|
|
1888
|
-
process.exit(1);
|
|
1889
|
-
}
|
|
1890
|
-
|
|
1891
|
-
const files = args[filesArgIndex + 1].split(',').map(f => f.trim());
|
|
1892
|
-
|
|
1893
|
-
// Check for coverage option
|
|
1894
|
-
const coverage = args.includes('--coverage');
|
|
1895
|
-
|
|
1896
|
-
console.log(`\n Running unit tests for ${files.length} file(s)${coverage ? ' (with coverage)' : ''}`);
|
|
1897
|
-
console.log('');
|
|
1898
|
-
|
|
1899
|
-
const config = loadConfig();
|
|
1900
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
1901
|
-
|
|
1902
|
-
for (const sourceFile of files) {
|
|
1903
|
-
await runUnitTestForFile(sourceFile, csrfToken, config, coverage);
|
|
1904
|
-
}
|
|
1905
|
-
break;
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
case 'tree': {
|
|
1909
|
-
const packageArgIndex = args.indexOf('--package');
|
|
1910
|
-
if (packageArgIndex === -1) {
|
|
1911
|
-
console.error('Error: --package parameter required');
|
|
1912
|
-
console.error('Usage: abapgit-agent tree --package <package> [--depth <n>] [--include-types] [--json]');
|
|
1913
|
-
console.error('Example: abapgit-agent tree --package ZMY_PACKAGE');
|
|
1914
|
-
process.exit(1);
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
// Check if package value is missing (happens when shell variable expands to empty)
|
|
1918
|
-
if (packageArgIndex + 1 >= args.length) {
|
|
1919
|
-
console.error('Error: --package parameter value is missing');
|
|
1920
|
-
console.error('');
|
|
1921
|
-
console.error('Tip: If you are using a shell variable, make sure to quote it:');
|
|
1922
|
-
console.error(' abapgit-agent tree --package "$ZMY_PACKAGE"');
|
|
1923
|
-
console.error(' or escape the $ character:');
|
|
1924
|
-
console.error(' abapgit-agent tree --package \\$ZMY_PACKAGE');
|
|
1925
|
-
console.error('');
|
|
1926
|
-
console.error('Usage: abapgit-agent tree --package <package> [--depth <n>] [--include-types] [--json]');
|
|
1927
|
-
console.error('Example: abapgit-agent tree --package ZMY_PACKAGE');
|
|
1928
|
-
process.exit(1);
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
const packageName = args[packageArgIndex + 1];
|
|
1932
|
-
|
|
1933
|
-
// Check for empty/whitespace-only package name
|
|
1934
|
-
if (!packageName || packageName.trim() === '') {
|
|
1935
|
-
console.error('Error: --package parameter cannot be empty');
|
|
1936
|
-
console.error('Usage: abapgit-agent tree --package <package> [--depth <n>] [--include-types] [--json]');
|
|
1937
|
-
console.error('Example: abapgit-agent tree --package ZMY_PACKAGE');
|
|
1938
|
-
process.exit(1);
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
// Optional depth parameter
|
|
1942
|
-
const depthArgIndex = args.indexOf('--depth');
|
|
1943
|
-
let depth = 3;
|
|
1944
|
-
if (depthArgIndex !== -1 && depthArgIndex + 1 < args.length) {
|
|
1945
|
-
depth = parseInt(args[depthArgIndex + 1], 10);
|
|
1946
|
-
if (isNaN(depth) || depth < 1) {
|
|
1947
|
-
console.error('Error: --depth must be a positive number');
|
|
1948
|
-
process.exit(1);
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
// Optional include-types parameter (--include-objects is deprecated alias)
|
|
1953
|
-
const includeTypes = args.includes('--include-types') || args.includes('--include-objects');
|
|
1954
|
-
|
|
1955
|
-
// Optional json parameter
|
|
1956
|
-
const jsonOutput = args.includes('--json');
|
|
1957
|
-
|
|
1958
|
-
if (jsonOutput) {
|
|
1959
|
-
const config = loadConfig();
|
|
1960
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
1961
|
-
const result = await runTreeCommand(packageName, depth, includeTypes, csrfToken, config);
|
|
1962
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1963
|
-
} else {
|
|
1964
|
-
await displayTreeOutput(packageName, depth, includeTypes);
|
|
1965
|
-
}
|
|
1966
|
-
break;
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
case 'list': {
|
|
1970
|
-
const packageArgIndex = args.indexOf('--package');
|
|
1971
|
-
if (packageArgIndex === -1) {
|
|
1972
|
-
console.error('Error: --package parameter required');
|
|
1973
|
-
console.error('Usage: abapgit-agent list --package <package> [--type <types>] [--name <pattern>] [--limit <n>] [--offset <n>] [--json]');
|
|
1974
|
-
console.error('Example: abapgit-agent list --package $ZMY_PACKAGE');
|
|
1975
|
-
console.error('Example: abapgit-agent list --package $ZMY_PACKAGE --type CLAS,INTF');
|
|
1976
|
-
process.exit(1);
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
// Check if package value is missing
|
|
1980
|
-
if (packageArgIndex + 1 >= args.length) {
|
|
1981
|
-
console.error('Error: --package parameter value is missing');
|
|
1982
|
-
process.exit(1);
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
const packageName = args[packageArgIndex + 1];
|
|
1986
83
|
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
console.error('Error: --package parameter cannot be empty');
|
|
1990
|
-
process.exit(1);
|
|
1991
|
-
}
|
|
84
|
+
// Load config if needed
|
|
85
|
+
const config = cmd.requiresAbapConfig ? loadConfig() : null;
|
|
1992
86
|
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
// Optional name pattern
|
|
1998
|
-
const nameArgIndex = args.indexOf('--name');
|
|
1999
|
-
const name = nameArgIndex !== -1 && nameArgIndex + 1 < args.length ? args[nameArgIndex + 1] : null;
|
|
2000
|
-
|
|
2001
|
-
// Optional limit
|
|
2002
|
-
const limitArgIndex = args.indexOf('--limit');
|
|
2003
|
-
let limit = 100;
|
|
2004
|
-
if (limitArgIndex !== -1 && limitArgIndex + 1 < args.length) {
|
|
2005
|
-
limit = parseInt(args[limitArgIndex + 1], 10);
|
|
2006
|
-
if (isNaN(limit) || limit < 1) {
|
|
2007
|
-
console.error('Error: --limit must be a positive number');
|
|
2008
|
-
process.exit(1);
|
|
2009
|
-
}
|
|
2010
|
-
if (limit > 1000) {
|
|
2011
|
-
console.error('Error: --limit value too high (max: 1000)');
|
|
2012
|
-
process.exit(1);
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
// Optional offset
|
|
2017
|
-
const offsetArgIndex = args.indexOf('--offset');
|
|
2018
|
-
let offset = 0;
|
|
2019
|
-
if (offsetArgIndex !== -1 && offsetArgIndex + 1 < args.length) {
|
|
2020
|
-
offset = parseInt(args[offsetArgIndex + 1], 10);
|
|
2021
|
-
if (isNaN(offset) || offset < 0) {
|
|
2022
|
-
console.error('Error: --offset must be a non-negative number');
|
|
2023
|
-
process.exit(1);
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
// Optional json parameter
|
|
2028
|
-
const jsonOutput = args.includes('--json');
|
|
2029
|
-
|
|
2030
|
-
const config = loadConfig();
|
|
2031
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
2032
|
-
|
|
2033
|
-
const data = {
|
|
2034
|
-
package: packageName,
|
|
2035
|
-
limit: limit,
|
|
2036
|
-
offset: offset
|
|
2037
|
-
};
|
|
2038
|
-
|
|
2039
|
-
if (type) {
|
|
2040
|
-
data.type = type;
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
if (name) {
|
|
2044
|
-
data.name = name;
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/list', data, { csrfToken });
|
|
2048
|
-
|
|
2049
|
-
// Handle uppercase keys from ABAP
|
|
2050
|
-
const success = result.SUCCESS || result.success;
|
|
2051
|
-
const error = result.ERROR || result.error;
|
|
2052
|
-
const objects = result.OBJECTS || result.objects || [];
|
|
2053
|
-
const byType = result.BY_TYPE || result.by_type || [];
|
|
2054
|
-
const total = result.TOTAL || result.total || 0;
|
|
2055
|
-
|
|
2056
|
-
if (!success || error) {
|
|
2057
|
-
console.error(`\n Error: ${error || 'Failed to list objects'}`);
|
|
2058
|
-
process.exit(1);
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
if (jsonOutput) {
|
|
2062
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2063
|
-
} else {
|
|
2064
|
-
// Display human-readable output
|
|
2065
|
-
let title = `Objects in ${packageName}`;
|
|
2066
|
-
if (type) {
|
|
2067
|
-
title += ` (${type} only`;
|
|
2068
|
-
if (total !== objects.length) {
|
|
2069
|
-
title += `, Total: ${total}`;
|
|
2070
|
-
}
|
|
2071
|
-
title += ')';
|
|
2072
|
-
} else if (total !== objects.length) {
|
|
2073
|
-
title += ` (Total: ${total})`;
|
|
2074
|
-
}
|
|
2075
|
-
console.log(`\n${title}\n`);
|
|
2076
|
-
|
|
2077
|
-
// Group objects by type
|
|
2078
|
-
const objectsByType = {};
|
|
2079
|
-
for (const obj of objects) {
|
|
2080
|
-
const objType = (obj.TYPE || obj.type || '???').toUpperCase();
|
|
2081
|
-
if (!objectsByType[objType]) {
|
|
2082
|
-
objectsByType[objType] = [];
|
|
2083
|
-
}
|
|
2084
|
-
objectsByType[objType].push(obj.NAME || obj.name);
|
|
2085
|
-
}
|
|
2086
|
-
|
|
2087
|
-
// Display grouped objects
|
|
2088
|
-
for (const objType of Object.keys(objectsByType).sort()) {
|
|
2089
|
-
const objNames = objectsByType[objType];
|
|
2090
|
-
console.log(` ${objType} (${objNames.length})`);
|
|
2091
|
-
for (const objName of objNames) {
|
|
2092
|
-
console.log(` ${objName}`);
|
|
2093
|
-
}
|
|
2094
|
-
console.log('');
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
break;
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
case 'view': {
|
|
2101
|
-
const objectsArgIndex = args.indexOf('--objects');
|
|
2102
|
-
if (objectsArgIndex === -1 || objectsArgIndex + 1 >= args.length) {
|
|
2103
|
-
console.error('Error: --objects parameter required');
|
|
2104
|
-
console.error('Usage: abapgit-agent view --objects <obj1>,<obj2>,... [--type <type>] [--json]');
|
|
2105
|
-
console.error('Example: abapgit-agent view --objects ZCL_MY_CLASS');
|
|
2106
|
-
console.error('Example: abapgit-agent view --objects ZCL_CLASS1,ZCL_CLASS2 --type CLAS');
|
|
2107
|
-
process.exit(1);
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
const objects = args[objectsArgIndex + 1].split(',').map(o => o.trim().toUpperCase());
|
|
2111
|
-
const typeArg = args.indexOf('--type');
|
|
2112
|
-
const type = typeArg !== -1 ? args[typeArg + 1].toUpperCase() : null;
|
|
2113
|
-
const jsonOutput = args.includes('--json');
|
|
2114
|
-
|
|
2115
|
-
console.log(`\n Viewing ${objects.length} object(s)`);
|
|
2116
|
-
|
|
2117
|
-
const config = loadConfig();
|
|
2118
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
2119
|
-
|
|
2120
|
-
const data = {
|
|
2121
|
-
objects: objects
|
|
2122
|
-
};
|
|
2123
|
-
|
|
2124
|
-
if (type) {
|
|
2125
|
-
data.type = type;
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/view', data, { csrfToken });
|
|
2129
|
-
|
|
2130
|
-
// Handle uppercase keys from ABAP
|
|
2131
|
-
const success = result.SUCCESS || result.success;
|
|
2132
|
-
const viewObjects = result.OBJECTS || result.objects || [];
|
|
2133
|
-
const message = result.MESSAGE || result.message || '';
|
|
2134
|
-
const error = result.ERROR || result.error;
|
|
2135
|
-
|
|
2136
|
-
if (!success || error) {
|
|
2137
|
-
console.error(`\n Error: ${error || 'Failed to view objects'}`);
|
|
2138
|
-
break;
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
if (jsonOutput) {
|
|
2142
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2143
|
-
} else {
|
|
2144
|
-
console.log(`\n ${message}`);
|
|
2145
|
-
console.log('');
|
|
2146
|
-
|
|
2147
|
-
for (let i = 0; i < viewObjects.length; i++) {
|
|
2148
|
-
const obj = viewObjects[i];
|
|
2149
|
-
const objName = obj.NAME || obj.name || `Object ${i + 1}`;
|
|
2150
|
-
const objType = obj.TYPE || obj.type || '';
|
|
2151
|
-
const objTypeText = obj.TYPE_TEXT || obj.type_text || '';
|
|
2152
|
-
const description = obj.DESCRIPTION || obj.description || '';
|
|
2153
|
-
const methods = obj.METHODS || obj.methods || [];
|
|
2154
|
-
const components = obj.COMPONENTS || obj.components || [];
|
|
2155
|
-
const notFound = obj.NOT_FOUND || obj.not_found || false;
|
|
2156
|
-
|
|
2157
|
-
// Check if object was not found
|
|
2158
|
-
if (notFound) {
|
|
2159
|
-
console.log(` ❌ ${objName} (${objTypeText})`);
|
|
2160
|
-
console.log(` Object not found: ${objName}`);
|
|
2161
|
-
continue;
|
|
2162
|
-
}
|
|
2163
|
-
|
|
2164
|
-
console.log(` 📖 ${objName} (${objTypeText})`);
|
|
2165
|
-
if (description) {
|
|
2166
|
-
console.log(` ${description}`);
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
// Display source code for classes, interfaces, CDS views, and programs/source includes
|
|
2170
|
-
const source = obj.SOURCE || obj.source || '';
|
|
2171
|
-
if (source && (objType === 'INTF' || objType === 'Interface' || objType === 'CLAS' || objType === 'Class' || objType === 'DDLS' || objType === 'CDS View' || objType === 'PROG' || objType === 'Program')) {
|
|
2172
|
-
console.log('');
|
|
2173
|
-
// Replace escaped newlines with actual newlines and display
|
|
2174
|
-
const displaySource = source.replace(/\\n/g, '\n');
|
|
2175
|
-
const lines = displaySource.split('\n');
|
|
2176
|
-
for (const line of lines) {
|
|
2177
|
-
console.log(` ${line}`);
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
if (methods.length > 0) {
|
|
2182
|
-
console.log(` Methods: ${methods.length}`);
|
|
2183
|
-
for (const method of methods.slice(0, 5)) {
|
|
2184
|
-
const name = method.NAME || method.name || '';
|
|
2185
|
-
const visibility = method.VISIBILITY || method.visibility || '';
|
|
2186
|
-
console.log(` - ${visibility} ${name}`);
|
|
2187
|
-
}
|
|
2188
|
-
if (methods.length > 5) {
|
|
2189
|
-
console.log(` ... and ${methods.length - 5} more`);
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
if (components.length > 0) {
|
|
2194
|
-
// Check if this is a data element (DTEL) - show domain info in property format
|
|
2195
|
-
if (objType === 'DTEL' || objType === 'Data Element') {
|
|
2196
|
-
const propWidth = 18;
|
|
2197
|
-
const valueWidth = 40;
|
|
2198
|
-
|
|
2199
|
-
// Build separator with corners
|
|
2200
|
-
const sep = '┌' + '─'.repeat(propWidth + 2) + '┬' + '─'.repeat(valueWidth + 2) + '┐';
|
|
2201
|
-
const mid = '├' + '─'.repeat(propWidth + 2) + '┼' + '─'.repeat(valueWidth + 2) + '┤';
|
|
2202
|
-
const end = '└' + '─'.repeat(propWidth + 2) + '┴' + '─'.repeat(valueWidth + 2) + '┘';
|
|
2203
|
-
|
|
2204
|
-
// Helper to build row
|
|
2205
|
-
const buildPropRow = (property, value) => {
|
|
2206
|
-
return '│ ' + String(property || '').padEnd(propWidth) + ' │ ' +
|
|
2207
|
-
String(value || '').substring(0, valueWidth).padEnd(valueWidth) + ' │';
|
|
2208
|
-
};
|
|
2209
|
-
|
|
2210
|
-
console.log(` DATA ELEMENT ${objName}:`);
|
|
2211
|
-
console.log(sep);
|
|
2212
|
-
console.log(buildPropRow('Property', 'Value'));
|
|
2213
|
-
console.log(mid);
|
|
2214
|
-
|
|
2215
|
-
// Collect properties from top-level fields and components
|
|
2216
|
-
const domain = obj.DOMAIN || obj.domain || '';
|
|
2217
|
-
const domainType = obj.DOMAIN_TYPE || obj.domain_type || '';
|
|
2218
|
-
const domainLength = obj.DOMAIN_LENGTH || obj.domain_length || 0;
|
|
2219
|
-
const domainDecimals = obj.DOMAIN_DECIMALS || obj.domain_decimals || 0;
|
|
2220
|
-
const description = obj.DESCRIPTION || obj.description || '';
|
|
2221
|
-
|
|
2222
|
-
if (domainType) {
|
|
2223
|
-
console.log(buildPropRow('Data Type', domainType));
|
|
2224
|
-
}
|
|
2225
|
-
if (domainLength) {
|
|
2226
|
-
console.log(buildPropRow('Length', String(domainLength)));
|
|
2227
|
-
}
|
|
2228
|
-
if (domainDecimals) {
|
|
2229
|
-
console.log(buildPropRow('Decimals', String(domainDecimals)));
|
|
2230
|
-
}
|
|
2231
|
-
if (description) {
|
|
2232
|
-
console.log(buildPropRow('Description', description));
|
|
2233
|
-
}
|
|
2234
|
-
if (domain) {
|
|
2235
|
-
console.log(buildPropRow('Domain', domain));
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
console.log(end);
|
|
2239
|
-
} else if (objType === 'TTYP' || objType === 'Table Type') {
|
|
2240
|
-
// Show TTYP details as simple text lines
|
|
2241
|
-
console.log('');
|
|
2242
|
-
for (const comp of components) {
|
|
2243
|
-
const desc = comp.DESCRIPTION || comp.description || '';
|
|
2244
|
-
if (desc) {
|
|
2245
|
-
console.log(` ${desc}`);
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
} else {
|
|
2249
|
-
// Build table display for TABL/STRU with Data Element and Description
|
|
2250
|
-
const colWidths = {
|
|
2251
|
-
field: 16, // Max field name length
|
|
2252
|
-
key: 3,
|
|
2253
|
-
type: 8,
|
|
2254
|
-
length: 8,
|
|
2255
|
-
dataelement: 30, // Max data element name length
|
|
2256
|
-
description: 60, // Max field description length
|
|
2257
|
-
};
|
|
2258
|
-
|
|
2259
|
-
// Helper to truncate with ellipsis if needed
|
|
2260
|
-
const truncate = (str, maxLen) => {
|
|
2261
|
-
const s = String(str || '');
|
|
2262
|
-
if (s.length <= maxLen) return s;
|
|
2263
|
-
return s.substring(0, maxLen - 1) + '…';
|
|
2264
|
-
};
|
|
2265
|
-
|
|
2266
|
-
// Helper to build row
|
|
2267
|
-
const buildRow = (field, key, type, length, dataelement, description) => {
|
|
2268
|
-
return ' | ' + truncate(field, colWidths.field).padEnd(colWidths.field) + ' | ' + String(key || '').padEnd(colWidths.key) + ' | ' + truncate(type, colWidths.type).padEnd(colWidths.type) + ' | ' + String(length || '').padStart(colWidths.length) + ' | ' + truncate(dataelement, colWidths.dataelement).padEnd(colWidths.dataelement) + ' | ' + truncate(description, colWidths.description).padEnd(colWidths.description) + ' |';
|
|
2269
|
-
};
|
|
2270
|
-
|
|
2271
|
-
// Build separator line (matches row structure with | at ends and + between columns)
|
|
2272
|
-
const sep = ' |' + '-'.repeat(colWidths.field + 2) + '+' +
|
|
2273
|
-
'-'.repeat(colWidths.key + 2) + '+' +
|
|
2274
|
-
'-'.repeat(colWidths.type + 2) + '+' +
|
|
2275
|
-
'-'.repeat(colWidths.length + 2) + '+' +
|
|
2276
|
-
'-'.repeat(colWidths.dataelement + 2) + '+' +
|
|
2277
|
-
'-'.repeat(colWidths.description + 2) + '|';
|
|
2278
|
-
|
|
2279
|
-
// Header
|
|
2280
|
-
console.log(` TABLE ${objName}:`);
|
|
2281
|
-
console.log(sep);
|
|
2282
|
-
console.log(buildRow('Field', 'Key', 'Type', 'Length', 'Data Elem', 'Description'));
|
|
2283
|
-
console.log(sep);
|
|
2284
|
-
|
|
2285
|
-
// Rows
|
|
2286
|
-
for (const comp of components) {
|
|
2287
|
-
const key = comp.KEY || comp.key || false ? 'X' : '';
|
|
2288
|
-
const dataelement = comp.DATAELEMENT || comp.dataelement || '';
|
|
2289
|
-
const description = comp.DESCRIPTION || comp.description || '';
|
|
2290
|
-
console.log(buildRow(comp.FIELD || comp.field, key, comp.TYPE || comp.type, comp.LENGTH || comp.length, dataelement, description));
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
console.log(sep);
|
|
2294
|
-
}
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
console.log('');
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
}
|
|
2301
|
-
break;
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
case 'preview': {
|
|
2305
|
-
const objectsArgIndex = args.indexOf('--objects');
|
|
2306
|
-
if (objectsArgIndex === -1 || objectsArgIndex + 1 >= args.length) {
|
|
2307
|
-
console.error('Error: --objects parameter required');
|
|
2308
|
-
console.error('Usage: abapgit-agent preview --objects <table1>,<view1>,... [--type <type>] [--limit <n>] [--offset <n>] [--where <condition>] [--columns <cols>] [--vertical] [--compact] [--json]');
|
|
2309
|
-
console.error('Example: abapgit-agent preview --objects SFLIGHT');
|
|
2310
|
-
console.error('Example: abapgit-agent preview --objects ZC_MY_CDS_VIEW --type DDLS');
|
|
2311
|
-
console.error('Example: abapgit-agent preview --objects SFLIGHT --where "CARRID = \'AA\'"');
|
|
2312
|
-
console.error('Example: abapgit-agent preview --objects SFLIGHT --offset 10 --limit 20');
|
|
2313
|
-
process.exit(1);
|
|
2314
|
-
}
|
|
2315
|
-
|
|
2316
|
-
const objects = args[objectsArgIndex + 1].split(',').map(o => o.trim().toUpperCase());
|
|
2317
|
-
const typeArg = args.indexOf('--type');
|
|
2318
|
-
const type = typeArg !== -1 ? args[typeArg + 1].toUpperCase() : null;
|
|
2319
|
-
const limitArg = args.indexOf('--limit');
|
|
2320
|
-
const limitRaw = limitArg !== -1 ? args[limitArg + 1] : null;
|
|
2321
|
-
const limitParsed = parseInt(limitRaw, 10);
|
|
2322
|
-
const limit = limitRaw && !limitRaw.startsWith('--') && !isNaN(limitParsed) ? Math.max(1, limitParsed) : 100;
|
|
2323
|
-
const offsetArg = args.indexOf('--offset');
|
|
2324
|
-
const offsetRaw = offsetArg !== -1 ? args[offsetArg + 1] : null;
|
|
2325
|
-
const offsetParsed = parseInt(offsetRaw, 10);
|
|
2326
|
-
const offset = offsetRaw && !offsetRaw.startsWith('--') && !isNaN(offsetParsed) ? Math.max(0, offsetParsed) : 0;
|
|
2327
|
-
const whereArg = args.indexOf('--where');
|
|
2328
|
-
const where = whereArg !== -1 ? args[whereArg + 1] : null;
|
|
2329
|
-
const columnsArg = args.indexOf('--columns');
|
|
2330
|
-
const columns = columnsArg !== -1 ? args[columnsArg + 1].split(',').map(c => c.trim().toUpperCase()) : null;
|
|
2331
|
-
const verticalOutput = args.includes('--vertical');
|
|
2332
|
-
const compactOutput = args.includes('--compact');
|
|
2333
|
-
const jsonOutput = args.includes('--json');
|
|
2334
|
-
|
|
2335
|
-
console.log(`\n Previewing ${objects.length} object(s)`);
|
|
2336
|
-
|
|
2337
|
-
const config = loadConfig();
|
|
2338
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
2339
|
-
|
|
2340
|
-
const data = {
|
|
2341
|
-
objects: objects,
|
|
2342
|
-
limit: Math.min(Math.max(1, limit), 500),
|
|
2343
|
-
offset: Math.max(0, offset)
|
|
2344
|
-
};
|
|
2345
|
-
|
|
2346
|
-
if (type) {
|
|
2347
|
-
data.type = type;
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
if (where) {
|
|
2351
|
-
data.where = convertDatesInWhereClause(where);
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
if (columns) {
|
|
2355
|
-
data.columns = columns;
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/preview', data, { csrfToken });
|
|
2359
|
-
|
|
2360
|
-
// Handle uppercase keys from ABAP
|
|
2361
|
-
const success = result.SUCCESS || result.success;
|
|
2362
|
-
const previewObjects = result.OBJECTS || result.objects || [];
|
|
2363
|
-
const message = result.MESSAGE || result.message || '';
|
|
2364
|
-
const error = result.ERROR || result.error;
|
|
2365
|
-
|
|
2366
|
-
if (!success || error) {
|
|
2367
|
-
console.error(`\n Error: ${error || 'Failed to preview objects'}`);
|
|
2368
|
-
break;
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
const pagination = result.PAGINATION || result.pagination || null;
|
|
2372
|
-
|
|
2373
|
-
if (jsonOutput) {
|
|
2374
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2375
|
-
} else {
|
|
2376
|
-
// Build pagination message
|
|
2377
|
-
let paginationMsg = '';
|
|
2378
|
-
const paginationTotal = pagination ? (pagination.TOTAL || pagination.total || 0) : 0;
|
|
2379
|
-
const paginationHasMore = pagination ? (pagination.HAS_MORE || pagination.has_more || false) : false;
|
|
2380
|
-
if (pagination && paginationTotal > 0) {
|
|
2381
|
-
// Handle case where offset exceeds total
|
|
2382
|
-
if (offset >= paginationTotal) {
|
|
2383
|
-
paginationMsg = ` (Offset ${offset} exceeds total ${paginationTotal})`;
|
|
2384
|
-
} else {
|
|
2385
|
-
const start = offset + 1;
|
|
2386
|
-
const end = Math.min(offset + limit, paginationTotal);
|
|
2387
|
-
paginationMsg = ` (Showing ${start}-${end} of ${paginationTotal})`;
|
|
2388
|
-
if (paginationHasMore) {
|
|
2389
|
-
paginationMsg += ` — Use --offset ${offset + limit} to see more`;
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
console.log(`\n ${message}${paginationMsg}`);
|
|
2395
|
-
console.log('');
|
|
2396
|
-
|
|
2397
|
-
// Track if columns were explicitly specified
|
|
2398
|
-
const columnsExplicit = columns !== null;
|
|
2399
|
-
|
|
2400
|
-
for (let i = 0; i < previewObjects.length; i++) {
|
|
2401
|
-
const obj = previewObjects[i];
|
|
2402
|
-
const objName = obj.NAME || obj.name || `Object ${i + 1}`;
|
|
2403
|
-
const objType = obj.TYPE || obj.type || '';
|
|
2404
|
-
const objTypeText = obj.TYPE_TEXT || obj.type_text || '';
|
|
2405
|
-
// Parse rows - could be a JSON string or array
|
|
2406
|
-
let rows = obj.ROWS || obj.rows || [];
|
|
2407
|
-
if (typeof rows === 'string') {
|
|
2408
|
-
try {
|
|
2409
|
-
rows = JSON.parse(rows);
|
|
2410
|
-
} catch (e) {
|
|
2411
|
-
rows = [];
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
const fields = obj.FIELDS || obj.fields || [];
|
|
2415
|
-
const rowCount = obj.ROW_COUNT || obj.row_count || 0;
|
|
2416
|
-
const totalRows = obj.TOTAL_ROWS || obj.total_rows || 0;
|
|
2417
|
-
const notFound = obj.NOT_FOUND || obj.not_found || false;
|
|
2418
|
-
const accessDenied = obj.ACCESS_DENIED || obj.access_denied || false;
|
|
2419
|
-
|
|
2420
|
-
// Check if object was not found
|
|
2421
|
-
if (notFound) {
|
|
2422
|
-
console.log(` ❌ ${objName} (${objTypeText})`);
|
|
2423
|
-
console.log(` Object not found: ${objName}`);
|
|
2424
|
-
continue;
|
|
2425
|
-
}
|
|
2426
|
-
|
|
2427
|
-
// Check if access denied
|
|
2428
|
-
if (accessDenied) {
|
|
2429
|
-
console.log(` ❌ ${objName} (${objTypeText})`);
|
|
2430
|
-
console.log(` Access denied to: ${objName}`);
|
|
2431
|
-
continue;
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
console.log(` 📊 Preview: ${objName} (${objTypeText})`);
|
|
2435
|
-
|
|
2436
|
-
// Check for errors first
|
|
2437
|
-
const objError = obj.ERROR || obj.error;
|
|
2438
|
-
if (objError) {
|
|
2439
|
-
console.log(` ❌ Error: ${objError}`);
|
|
2440
|
-
continue;
|
|
2441
|
-
}
|
|
2442
|
-
|
|
2443
|
-
if (rows.length === 0) {
|
|
2444
|
-
console.log(' No data found');
|
|
2445
|
-
continue;
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
// Get all unique field names from all rows
|
|
2449
|
-
const allFields = new Set();
|
|
2450
|
-
rows.forEach(row => {
|
|
2451
|
-
Object.keys(row).forEach(key => allFields.add(key));
|
|
2452
|
-
});
|
|
2453
|
-
const allFieldNames = Array.from(allFields);
|
|
2454
|
-
|
|
2455
|
-
// Display as table - use fields metadata only if --columns was explicitly specified
|
|
2456
|
-
let fieldNames;
|
|
2457
|
-
let columnsAutoSelected = false;
|
|
2458
|
-
if (columnsExplicit && fields && fields.length > 0) {
|
|
2459
|
-
// Use fields from metadata (filtered by explicit --columns)
|
|
2460
|
-
fieldNames = fields.map(f => f.FIELD || f.field);
|
|
2461
|
-
} else {
|
|
2462
|
-
// Use all fields - let terminal handle wrapping if needed
|
|
2463
|
-
// Terminal width detection is unreliable without a proper TTY
|
|
2464
|
-
fieldNames = allFieldNames;
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
// Calculate column widths - use reasonable defaults
|
|
2468
|
-
const colWidths = {};
|
|
2469
|
-
const maxColWidth = compactOutput ? 10 : 20;
|
|
2470
|
-
fieldNames.forEach(field => {
|
|
2471
|
-
let maxWidth = field.length;
|
|
2472
|
-
rows.forEach(row => {
|
|
2473
|
-
const value = String(row[field] || '');
|
|
2474
|
-
maxWidth = Math.max(maxWidth, value.length);
|
|
2475
|
-
});
|
|
2476
|
-
// Cap at maxColWidth (truncates both headers and data in compact mode)
|
|
2477
|
-
colWidths[field] = Math.min(maxWidth, maxColWidth);
|
|
2478
|
-
});
|
|
2479
|
-
|
|
2480
|
-
// Render output - either vertical or table
|
|
2481
|
-
if (verticalOutput) {
|
|
2482
|
-
// Vertical format: each field on its own line
|
|
2483
|
-
rows.forEach((row, rowIndex) => {
|
|
2484
|
-
if (rows.length > 1) {
|
|
2485
|
-
console.log(`\n Row ${rowIndex + 1}:`);
|
|
2486
|
-
console.log(' ' + '─'.repeat(30));
|
|
2487
|
-
}
|
|
2488
|
-
fieldNames.forEach(field => {
|
|
2489
|
-
const value = String(row[field] || '');
|
|
2490
|
-
console.log(` ${field}: ${value}`);
|
|
2491
|
-
});
|
|
2492
|
-
});
|
|
2493
|
-
console.log('');
|
|
2494
|
-
continue;
|
|
2495
|
-
}
|
|
2496
|
-
|
|
2497
|
-
// Build table header
|
|
2498
|
-
let headerLine = ' ┌';
|
|
2499
|
-
let separatorLine = ' ├';
|
|
2500
|
-
fieldNames.forEach(field => {
|
|
2501
|
-
const width = colWidths[field];
|
|
2502
|
-
headerLine += '─'.repeat(width + 2) + '┬';
|
|
2503
|
-
separatorLine += '─'.repeat(width + 2) + '┼';
|
|
2504
|
-
});
|
|
2505
|
-
headerLine = headerLine.slice(0, -1) + '┐';
|
|
2506
|
-
separatorLine = separatorLine.slice(0, -1) + '┤';
|
|
2507
|
-
|
|
2508
|
-
// Build header row
|
|
2509
|
-
let headerRow = ' │';
|
|
2510
|
-
fieldNames.forEach(field => {
|
|
2511
|
-
const width = colWidths[field];
|
|
2512
|
-
let displayField = String(field);
|
|
2513
|
-
if (displayField.length > width) {
|
|
2514
|
-
displayField = displayField.slice(0, width - 3) + '...';
|
|
2515
|
-
}
|
|
2516
|
-
headerRow += ' ' + displayField.padEnd(width) + ' │';
|
|
2517
|
-
});
|
|
2518
|
-
|
|
2519
|
-
console.log(headerLine);
|
|
2520
|
-
console.log(headerRow);
|
|
2521
|
-
console.log(separatorLine);
|
|
2522
|
-
|
|
2523
|
-
// Build data rows
|
|
2524
|
-
rows.forEach(row => {
|
|
2525
|
-
let dataRow = ' │';
|
|
2526
|
-
fieldNames.forEach(field => {
|
|
2527
|
-
const width = colWidths[field];
|
|
2528
|
-
const value = String(row[field] || '');
|
|
2529
|
-
const displayValue = value.length > width ? value.slice(0, width - 3) + '...' : value;
|
|
2530
|
-
dataRow += ' ' + displayValue.padEnd(width) + ' │';
|
|
2531
|
-
});
|
|
2532
|
-
console.log(dataRow);
|
|
2533
|
-
});
|
|
2534
|
-
|
|
2535
|
-
// Build bottom border
|
|
2536
|
-
let bottomLine = ' └';
|
|
2537
|
-
fieldNames.forEach(field => {
|
|
2538
|
-
const width = colWidths[field];
|
|
2539
|
-
bottomLine += '─'.repeat(width + 2) + '┴';
|
|
2540
|
-
});
|
|
2541
|
-
bottomLine = bottomLine.slice(0, -1) + '┘';
|
|
2542
|
-
console.log(bottomLine);
|
|
2543
|
-
|
|
2544
|
-
// Show row count
|
|
2545
|
-
if (totalRows > rowCount) {
|
|
2546
|
-
console.log(`\n Showing ${rowCount} of ${totalRows} rows`);
|
|
2547
|
-
} else {
|
|
2548
|
-
console.log(`\n ${rowCount} row(s)`);
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
// Note about hidden columns only when --columns was explicitly specified
|
|
2552
|
-
const columnsDisplayed = fieldNames.length;
|
|
2553
|
-
let columnsHidden = [];
|
|
2554
|
-
|
|
2555
|
-
if (columnsExplicit) {
|
|
2556
|
-
columnsHidden = obj.COLUMNS_HIDDEN || obj.columns_hidden || [];
|
|
2557
|
-
if (columnsHidden.length > 0) {
|
|
2558
|
-
console.log(`\n ⚠️ ${columnsHidden.length} more columns hidden (${columnsHidden.join(', ')})`);
|
|
2559
|
-
console.log(' Use --json for full data');
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
console.log('');
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
break;
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
case 'where': {
|
|
2570
|
-
const objectsArgIndex = args.indexOf('--objects');
|
|
2571
|
-
if (objectsArgIndex === -1 || objectsArgIndex + 1 >= args.length) {
|
|
2572
|
-
console.error('Error: --objects parameter required');
|
|
2573
|
-
console.error('Usage: abapgit-agent where --objects <obj1>,<obj2>,... [--type <type>] [--limit <n>] [--offset <n>] [--json]');
|
|
2574
|
-
console.error('Example: abapgit-agent where --objects ZCL_MY_CLASS');
|
|
2575
|
-
console.error('Example: abapgit-agent where --objects ZIF_MY_INTERFACE');
|
|
2576
|
-
console.error('Example: abapgit-agent where --objects CL_SUT_AUNIT_RUNNER --limit 20');
|
|
2577
|
-
console.error('Example: abapgit-agent where --objects CL_SUT_AUNIT_RUNNER --offset 100');
|
|
2578
|
-
process.exit(1);
|
|
2579
|
-
}
|
|
2580
|
-
|
|
2581
|
-
const objects = args[objectsArgIndex + 1].split(',').map(o => o.trim().toUpperCase());
|
|
2582
|
-
const typeArg = args.indexOf('--type');
|
|
2583
|
-
const type = typeArg !== -1 ? args[typeArg + 1].toUpperCase() : null;
|
|
2584
|
-
const limitArg = args.indexOf('--limit');
|
|
2585
|
-
const limitRaw = limitArg !== -1 ? args[limitArg + 1] : null;
|
|
2586
|
-
const limitParsed = parseInt(limitRaw, 10);
|
|
2587
|
-
const limit = limitRaw && !limitRaw.startsWith('--') && !isNaN(limitParsed) ? Math.max(1, limitParsed) : 100;
|
|
2588
|
-
const offsetArg = args.indexOf('--offset');
|
|
2589
|
-
const offsetRaw = offsetArg !== -1 ? args[offsetArg + 1] : null;
|
|
2590
|
-
const offsetParsed = parseInt(offsetRaw, 10);
|
|
2591
|
-
const offset = offsetRaw && !offsetRaw.startsWith('--') && !isNaN(offsetParsed) ? Math.max(0, offsetParsed) : 0;
|
|
2592
|
-
const jsonOutput = args.includes('--json');
|
|
2593
|
-
|
|
2594
|
-
console.log(`\n Where-used list for ${objects.length} object(s)`);
|
|
2595
|
-
|
|
2596
|
-
const config = loadConfig();
|
|
2597
|
-
const csrfToken = await fetchCsrfToken(config);
|
|
2598
|
-
|
|
2599
|
-
const data = {
|
|
2600
|
-
objects: objects,
|
|
2601
|
-
limit: Math.min(Math.max(1, limit), 500),
|
|
2602
|
-
offset: Math.max(0, offset)
|
|
2603
|
-
};
|
|
2604
|
-
|
|
2605
|
-
if (type) {
|
|
2606
|
-
data.type = type;
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
const result = await request('POST', '/sap/bc/z_abapgit_agent/where', data, { csrfToken });
|
|
2610
|
-
|
|
2611
|
-
// Handle uppercase keys from ABAP
|
|
2612
|
-
const success = result.SUCCESS || result.success;
|
|
2613
|
-
const whereObjects = result.OBJECTS || result.objects || [];
|
|
2614
|
-
const message = result.MESSAGE || result.message || '';
|
|
2615
|
-
const error = result.ERROR || result.error;
|
|
2616
|
-
const pagination = result.PAGINATION || result.pagination || null;
|
|
2617
|
-
|
|
2618
|
-
if (!success || error) {
|
|
2619
|
-
console.error(`\n Error: ${error || 'Failed to get where-used list'}`);
|
|
2620
|
-
break;
|
|
2621
|
-
}
|
|
2622
|
-
|
|
2623
|
-
if (jsonOutput) {
|
|
2624
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2625
|
-
} else {
|
|
2626
|
-
// Build pagination message
|
|
2627
|
-
let paginationMsg = '';
|
|
2628
|
-
const paginationTotal = pagination.TOTAL || pagination.total || 0;
|
|
2629
|
-
const paginationHasMore = pagination.HAS_MORE || pagination.has_more || false;
|
|
2630
|
-
if (pagination && paginationTotal > 0) {
|
|
2631
|
-
// Handle case where offset exceeds total
|
|
2632
|
-
if (offset >= paginationTotal) {
|
|
2633
|
-
paginationMsg = ` (Offset ${offset} exceeds total ${paginationTotal})`;
|
|
2634
|
-
} else {
|
|
2635
|
-
const start = offset + 1;
|
|
2636
|
-
const end = Math.min(offset + limit, paginationTotal);
|
|
2637
|
-
paginationMsg = ` (Showing ${start}-${end} of ${paginationTotal})`;
|
|
2638
|
-
if (paginationHasMore) {
|
|
2639
|
-
paginationMsg += ` — Use --offset ${offset + limit} to see more`;
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
|
-
console.log(`\n ${message}${paginationMsg}`);
|
|
2645
|
-
console.log('');
|
|
2646
|
-
|
|
2647
|
-
for (let i = 0; i < whereObjects.length; i++) {
|
|
2648
|
-
const obj = whereObjects[i];
|
|
2649
|
-
const objName = obj.NAME || obj.name || `Object ${i + 1}`;
|
|
2650
|
-
const objType = obj.TYPE || obj.type || '';
|
|
2651
|
-
const error = obj.ERROR || obj.error || '';
|
|
2652
|
-
const references = obj.REFERENCES || obj.references || [];
|
|
2653
|
-
const count = obj.COUNT || obj.count || 0;
|
|
2654
|
-
|
|
2655
|
-
// Handle object not found error
|
|
2656
|
-
if (error) {
|
|
2657
|
-
console.log(` ❌ ${objName} (${objType})`);
|
|
2658
|
-
console.log(` ${error}`);
|
|
2659
|
-
console.log('');
|
|
2660
|
-
continue;
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
if (count === 0) {
|
|
2664
|
-
console.log(` ❌ ${objName} (${objType})`);
|
|
2665
|
-
console.log(` No references found`);
|
|
2666
|
-
console.log('');
|
|
2667
|
-
continue;
|
|
2668
|
-
}
|
|
2669
|
-
|
|
2670
|
-
console.log(` 🔍 ${objName} (${objType})`);
|
|
2671
|
-
console.log(` Found ${count} reference(s):`);
|
|
2672
|
-
console.log('');
|
|
2673
|
-
|
|
2674
|
-
// Display references - one line format: include → method (type) or include (type)
|
|
2675
|
-
for (let j = 0; j < references.length; j++) {
|
|
2676
|
-
const ref = references[j];
|
|
2677
|
-
const includeName = ref.INCLUDE_NAME || ref.include_name || '';
|
|
2678
|
-
const includeType = ref.INCLUDE_TYPE || ref.include_type || '';
|
|
2679
|
-
const methodName = ref.METHOD_NAME || ref.method_name || '';
|
|
2680
|
-
|
|
2681
|
-
let line;
|
|
2682
|
-
if (methodName) {
|
|
2683
|
-
line = ` ${j + 1}. ${includeName} → ${methodName} (${includeType})`;
|
|
2684
|
-
} else {
|
|
2685
|
-
line = ` ${j + 1}. ${includeName} (${includeType})`;
|
|
2686
|
-
}
|
|
2687
|
-
console.log(line);
|
|
2688
|
-
}
|
|
2689
|
-
console.log('');
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
break;
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
|
-
case 'ref': {
|
|
2696
|
-
const refSearch = require('../src/ref-search');
|
|
2697
|
-
const topicIndex = args.indexOf('--topic');
|
|
2698
|
-
const cloneIndex = args.indexOf('--clone');
|
|
2699
|
-
const nameIndex = args.indexOf('--name');
|
|
2700
|
-
const listTopics = args.includes('--list-topics') || args.includes('-l');
|
|
2701
|
-
const listRepos = args.includes('--list-repos') || args.includes('-r');
|
|
2702
|
-
const jsonOutput = args.includes('--json');
|
|
2703
|
-
|
|
2704
|
-
// Handle --clone option
|
|
2705
|
-
if (cloneIndex !== -1 && cloneIndex + 1 < args.length) {
|
|
2706
|
-
const repoUrl = args[cloneIndex + 1];
|
|
2707
|
-
const name = nameIndex !== -1 && nameIndex + 1 < args.length ? args[nameIndex + 1] : null;
|
|
2708
|
-
const result = refSearch.cloneRepository(repoUrl, name);
|
|
2709
|
-
if (jsonOutput) {
|
|
2710
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2711
|
-
} else {
|
|
2712
|
-
refSearch.displayCloneResult(result);
|
|
2713
|
-
}
|
|
2714
|
-
break;
|
|
2715
|
-
}
|
|
2716
|
-
|
|
2717
|
-
if (listRepos) {
|
|
2718
|
-
const result = await refSearch.listRepositories();
|
|
2719
|
-
if (jsonOutput) {
|
|
2720
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2721
|
-
} else {
|
|
2722
|
-
refSearch.displayRepositories(result);
|
|
2723
|
-
}
|
|
2724
|
-
break;
|
|
2725
|
-
}
|
|
2726
|
-
|
|
2727
|
-
if (listTopics) {
|
|
2728
|
-
const result = await refSearch.listTopics();
|
|
2729
|
-
if (jsonOutput) {
|
|
2730
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2731
|
-
} else {
|
|
2732
|
-
refSearch.displayTopics(result);
|
|
2733
|
-
}
|
|
2734
|
-
break;
|
|
2735
|
-
}
|
|
2736
|
-
|
|
2737
|
-
if (topicIndex !== -1 && topicIndex + 1 < args.length) {
|
|
2738
|
-
const topic = args[topicIndex + 1];
|
|
2739
|
-
const result = await refSearch.getTopic(topic);
|
|
2740
|
-
if (jsonOutput) {
|
|
2741
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2742
|
-
} else {
|
|
2743
|
-
refSearch.displayTopic(result);
|
|
2744
|
-
}
|
|
2745
|
-
break;
|
|
2746
|
-
}
|
|
2747
|
-
|
|
2748
|
-
// Pattern search (default)
|
|
2749
|
-
const patternIndex = args.findIndex((arg, idx) => idx > 0 && !arg.startsWith('--'));
|
|
2750
|
-
if (patternIndex === -1) {
|
|
2751
|
-
console.error('Error: No pattern specified');
|
|
2752
|
-
console.error('');
|
|
2753
|
-
console.error('Usage:');
|
|
2754
|
-
console.error(' abapgit-agent ref <pattern> Search for pattern');
|
|
2755
|
-
console.error(' abapgit-agent ref --topic <name> View specific topic');
|
|
2756
|
-
console.error(' abapgit-agent ref --list-topics List available topics');
|
|
2757
|
-
console.error(' abapgit-agent ref --list-repos List reference repositories');
|
|
2758
|
-
console.error(' abapgit-agent ref --clone <repo> Clone a repository');
|
|
2759
|
-
console.error('');
|
|
2760
|
-
console.error('Examples:');
|
|
2761
|
-
console.error(' abapgit-agent ref "CORRESPONDING"');
|
|
2762
|
-
console.error(' abapgit-agent ref --topic exceptions');
|
|
2763
|
-
console.error(' abapgit-agent ref --list-topics');
|
|
2764
|
-
console.error(' abapgit-agent ref --list-repos');
|
|
2765
|
-
console.error(' abapgit-agent ref --clone SAP-samples/abap-cheat-sheets');
|
|
2766
|
-
console.error(' abapgit-agent ref --clone https://github.com/abapGit/abapGit.git');
|
|
2767
|
-
process.exit(1);
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
const pattern = args[patternIndex];
|
|
2771
|
-
const result = await refSearch.searchPattern(pattern);
|
|
2772
|
-
|
|
2773
|
-
if (jsonOutput) {
|
|
2774
|
-
console.log(JSON.stringify(result, null, 2));
|
|
2775
|
-
} else {
|
|
2776
|
-
refSearch.displaySearchResults(result);
|
|
2777
|
-
}
|
|
2778
|
-
break;
|
|
2779
|
-
}
|
|
2780
|
-
|
|
2781
|
-
case 'help':
|
|
2782
|
-
case '--help':
|
|
2783
|
-
case '-h':
|
|
2784
|
-
console.log(`
|
|
2785
|
-
ABAP Git Agent
|
|
2786
|
-
|
|
2787
|
-
Usage:
|
|
2788
|
-
abapgit-agent <command> [options]
|
|
2789
|
-
|
|
2790
|
-
Commands:
|
|
2791
|
-
init --folder <folder> --package <package>
|
|
2792
|
-
Initialize local configuration for an existing git repository.
|
|
2793
|
-
init --update
|
|
2794
|
-
Update existing files (CLAUDE.md, copilot-instructions.md, guidelines) to latest version.
|
|
2795
|
-
|
|
2796
|
-
create
|
|
2797
|
-
Create abapGit online repository in ABAP system.
|
|
2798
|
-
Auto-detects URL from git remote and package from .abapGitAgent.
|
|
2799
|
-
|
|
2800
|
-
import [--message <message>]
|
|
2801
|
-
Import existing objects from package to git repository.
|
|
2802
|
-
Uses the git remote URL to find the abapGit online repository.
|
|
2803
|
-
|
|
2804
|
-
pull [--url <git-url>] [--branch <branch>] [--files <file1,file2,...>] [--transport <request>]
|
|
2805
|
-
Pull and activate repository in ABAP system.
|
|
2806
|
-
Auto-detects git remote and branch from current directory.
|
|
2807
|
-
Use --files to pull only specific files.
|
|
2808
|
-
Use --transport to specify a transport request.
|
|
2809
|
-
|
|
2810
|
-
inspect --files <file1>,<file2>,...
|
|
2811
|
-
Inspect ABAP source file(s) for issues. Currently runs syntax check.
|
|
2812
|
-
|
|
2813
|
-
unit --files <file1>,<file2>,...
|
|
2814
|
-
Run AUnit tests for ABAP test class files (.testclasses.abap)
|
|
2815
|
-
|
|
2816
|
-
tree --package <package> [--depth <n>] [--include-types] [--json]
|
|
2817
|
-
Display package hierarchy tree from ABAP system
|
|
2818
|
-
|
|
2819
|
-
list --package <package> [--type <types>] [--name <pattern>] [--limit <n>] [--offset <n>] [--json]
|
|
2820
|
-
List ABAP objects in a package with filtering and pagination
|
|
2821
|
-
|
|
2822
|
-
view --objects <obj1>,<obj2>,... [--type <type>] [--json]
|
|
2823
|
-
View ABAP object definitions from the ABAP system
|
|
2824
|
-
|
|
2825
|
-
where --objects <obj1>,<obj2>,... [--type <type>] [--limit <n>] [--json]
|
|
2826
|
-
Find where-used list for ABAP objects (classes, interfaces, programs)
|
|
2827
|
-
|
|
2828
|
-
ref <pattern> [--json]
|
|
2829
|
-
Search ABAP reference repositories for patterns. Requires referenceFolder in .abapGitAgent.
|
|
2830
|
-
|
|
2831
|
-
ref --topic <topic> [--json]
|
|
2832
|
-
View specific topic from cheat sheets (exceptions, sql, unit-tests, etc.)
|
|
2833
|
-
|
|
2834
|
-
ref --list-topics
|
|
2835
|
-
List available topics for reference search
|
|
2836
|
-
|
|
2837
|
-
ref --list-repos
|
|
2838
|
-
List all reference repositories in the reference folder
|
|
2839
|
-
|
|
2840
|
-
ref --clone <repo> [--name <folder>]
|
|
2841
|
-
Clone a GitHub repository to the reference folder
|
|
2842
|
-
- Use full URL: https://github.com/user/repo.git
|
|
2843
|
-
- Or short name: user/repo or user/repo (assumes github.com)
|
|
2844
|
-
|
|
2845
|
-
health
|
|
2846
|
-
Check if ABAP REST API is healthy
|
|
2847
|
-
|
|
2848
|
-
status
|
|
2849
|
-
Check if ABAP integration is configured for this repo
|
|
87
|
+
// Version check if needed
|
|
88
|
+
if (cmd.requiresVersionCheck && config) {
|
|
89
|
+
await versionCheck.checkCompatibility(config);
|
|
90
|
+
}
|
|
2850
91
|
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
abapgit-agent inspect --files zcl_my_class.clas.abap # Syntax check
|
|
2863
|
-
abapgit-agent unit --files zcl_my_test.clas.testclasses.abap # Run tests
|
|
2864
|
-
abapgit-agent tree --package \$ZMY_PACKAGE # Show package tree
|
|
2865
|
-
abapgit-agent tree --package \$ZMY_PACKAGE --depth 2 # Shallow tree
|
|
2866
|
-
abapgit-agent tree --package \$ZMY_PACKAGE --include-types # With type counts
|
|
2867
|
-
abapgit-agent tree --package \$ZMY_PACKAGE --json # JSON output
|
|
2868
|
-
abapgit-agent list --package $ZMY_PACKAGE # List all objects
|
|
2869
|
-
abapgit-agent list --package $ZMY_PACKAGE --type CLAS,INTF # Filter by type
|
|
2870
|
-
abapgit-agent list --package $ZMY_PACKAGE --name ZCL_* # Filter by name
|
|
2871
|
-
abapgit-agent list --package $ZMY_PACKAGE --limit 50 # Limit results
|
|
2872
|
-
abapgit-agent list --package $ZMY_PACKAGE --json # JSON output
|
|
2873
|
-
abapgit-agent view --objects ZCL_MY_CLASS # View class definition
|
|
2874
|
-
abapgit-agent view --objects ZIF_MY_INTERFACE --type INTF # View interface
|
|
2875
|
-
abapgit-agent view --objects ZMY_TABLE --type TABL # View table structure
|
|
2876
|
-
abapgit-agent view --objects ZCL_CLASS1,ZCL_CLASS2 --json # Multiple objects
|
|
2877
|
-
abapgit-agent where --objects ZCL_SUT_AUNIT_RUNNER # Find where class is used
|
|
2878
|
-
abapgit-agent where --objects ZIF_MY_INTERFACE # Find interface implementations
|
|
2879
|
-
abapgit-agent where --objects CL_SUT_AUNIT_RUNNER --limit 20 # Limit results
|
|
2880
|
-
abapgit-agent where --objects ZIF_MY_INTERFACE --json # JSON output
|
|
2881
|
-
abapgit-agent ref "CORRESPONDING" # Search all reference repos
|
|
2882
|
-
abapgit-agent ref "CX_SY_" # Search exceptions
|
|
2883
|
-
abapgit-agent ref --topic exceptions # View exception topic
|
|
2884
|
-
abapgit-agent ref --topic sql # View SQL topic
|
|
2885
|
-
abapgit-agent ref --topic unit-tests # View unit test topic
|
|
2886
|
-
abapgit-agent ref --list-topics # List all topics
|
|
2887
|
-
abapgit-agent ref --list-repos # List reference repositories
|
|
2888
|
-
abapgit-agent ref --clone SAP-samples/abap-cheat-sheets # Clone repo to reference folder
|
|
2889
|
-
abapgit-agent ref --clone https://github.com/user/repo.git # Clone from URL
|
|
2890
|
-
abapgit-agent health
|
|
2891
|
-
abapgit-agent status
|
|
2892
|
-
`);
|
|
2893
|
-
break;
|
|
92
|
+
// Build context for command
|
|
93
|
+
const context = {
|
|
94
|
+
config,
|
|
95
|
+
gitUtils,
|
|
96
|
+
validators,
|
|
97
|
+
versionCheck,
|
|
98
|
+
isAbapIntegrationEnabled,
|
|
99
|
+
loadConfig,
|
|
100
|
+
AbapHttp,
|
|
101
|
+
getTransport
|
|
102
|
+
};
|
|
2894
103
|
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
process.exit(1);
|
|
2899
|
-
}
|
|
2900
|
-
} catch (error) {
|
|
2901
|
-
console.error(`Error: ${error.message}`);
|
|
2902
|
-
process.exit(1);
|
|
104
|
+
// Execute command
|
|
105
|
+
await cmd.execute(args.slice(1), context);
|
|
106
|
+
return;
|
|
2903
107
|
}
|
|
108
|
+
|
|
109
|
+
// Unknown command - show error
|
|
110
|
+
console.error(`Unknown command: ${command}`);
|
|
111
|
+
console.error('Run "abapgit-agent help" for usage information');
|
|
112
|
+
process.exit(1);
|
|
2904
113
|
}
|
|
2905
114
|
|
|
2906
115
|
main();
|