abapgit-agent 1.7.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/abap/CLAUDE.md +145 -26
- package/abap/guidelines/00_index.md +8 -0
- package/abap/guidelines/01_sql.md +8 -0
- package/abap/guidelines/02_exceptions.md +8 -0
- package/abap/guidelines/03_testing.md +8 -0
- package/abap/guidelines/04_cds.md +8 -0
- package/abap/guidelines/05_classes.md +8 -0
- package/abap/guidelines/06_objects.md +8 -0
- package/abap/guidelines/07_json.md +8 -0
- package/abap/guidelines/08_abapgit.md +8 -0
- package/abap/guidelines/09_unit_testable_code.md +8 -0
- package/bin/abapgit-agent +61 -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 +290 -0
- package/src/commands/tree.js +209 -0
- package/src/commands/unit.js +133 -0
- package/src/commands/view.js +215 -0
- package/src/commands/where.js +138 -0
- package/src/config.js +11 -1
- package/src/utils/abap-http.js +347 -0
- package/src/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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List command - List ABAP objects in a package
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
name: 'list',
|
|
7
|
+
description: 'List ABAP objects in a package',
|
|
8
|
+
requiresAbapConfig: true,
|
|
9
|
+
requiresVersionCheck: true,
|
|
10
|
+
|
|
11
|
+
async execute(args, context) {
|
|
12
|
+
const { loadConfig, AbapHttp } = context;
|
|
13
|
+
|
|
14
|
+
const packageArgIndex = args.indexOf('--package');
|
|
15
|
+
if (packageArgIndex === -1) {
|
|
16
|
+
console.error('Error: --package parameter required');
|
|
17
|
+
console.error('Usage: abapgit-agent list --package <package> [--type <types>] [--name <pattern>] [--limit <n>] [--offset <n>] [--json]');
|
|
18
|
+
console.error('Example: abapgit-agent list --package \'$ZMY_PACKAGE\'');
|
|
19
|
+
console.error('Example: abapgit-agent list --package \'$ZMY_PACKAGE\' --type CLAS,INTF');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if package value is missing
|
|
24
|
+
if (packageArgIndex + 1 >= args.length) {
|
|
25
|
+
console.error('Error: --package parameter value is missing');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const packageName = args[packageArgIndex + 1];
|
|
30
|
+
|
|
31
|
+
// Validate package name
|
|
32
|
+
if (!packageName || packageName.trim() === '') {
|
|
33
|
+
console.error('Error: --package parameter cannot be empty');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Optional type parameter
|
|
38
|
+
const typeArgIndex = args.indexOf('--type');
|
|
39
|
+
const type = typeArgIndex !== -1 && typeArgIndex + 1 < args.length ? args[typeArgIndex + 1] : null;
|
|
40
|
+
|
|
41
|
+
// Optional name pattern
|
|
42
|
+
const nameArgIndex = args.indexOf('--name');
|
|
43
|
+
const name = nameArgIndex !== -1 && nameArgIndex + 1 < args.length ? args[nameArgIndex + 1] : null;
|
|
44
|
+
|
|
45
|
+
// Optional limit
|
|
46
|
+
const limitArgIndex = args.indexOf('--limit');
|
|
47
|
+
let limit = 100;
|
|
48
|
+
if (limitArgIndex !== -1 && limitArgIndex + 1 < args.length) {
|
|
49
|
+
limit = parseInt(args[limitArgIndex + 1], 10);
|
|
50
|
+
if (isNaN(limit) || limit < 1) {
|
|
51
|
+
console.error('Error: --limit must be a positive number');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
if (limit > 1000) {
|
|
55
|
+
console.error('Error: --limit value too high (max: 1000)');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Optional offset
|
|
61
|
+
const offsetArgIndex = args.indexOf('--offset');
|
|
62
|
+
let offset = 0;
|
|
63
|
+
if (offsetArgIndex !== -1 && offsetArgIndex + 1 < args.length) {
|
|
64
|
+
offset = parseInt(args[offsetArgIndex + 1], 10);
|
|
65
|
+
if (isNaN(offset) || offset < 0) {
|
|
66
|
+
console.error('Error: --offset must be a non-negative number');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Optional json parameter
|
|
72
|
+
const jsonOutput = args.includes('--json');
|
|
73
|
+
|
|
74
|
+
const config = loadConfig();
|
|
75
|
+
const http = new AbapHttp(config);
|
|
76
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
77
|
+
|
|
78
|
+
const data = {
|
|
79
|
+
package: packageName,
|
|
80
|
+
limit: limit,
|
|
81
|
+
offset: offset
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (type) {
|
|
85
|
+
data.type = type;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (name) {
|
|
89
|
+
data.name = name;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const result = await http.post('/sap/bc/z_abapgit_agent/list', data, { csrfToken });
|
|
93
|
+
|
|
94
|
+
// Handle uppercase keys from ABAP
|
|
95
|
+
const success = result.SUCCESS || result.success;
|
|
96
|
+
const error = result.ERROR || result.error;
|
|
97
|
+
const objects = result.OBJECTS || result.objects || [];
|
|
98
|
+
const byType = result.BY_TYPE || result.by_type || [];
|
|
99
|
+
const total = result.TOTAL || result.total || 0;
|
|
100
|
+
|
|
101
|
+
if (!success || error) {
|
|
102
|
+
console.error(`\n Error: ${error || 'Failed to list objects'}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (jsonOutput) {
|
|
107
|
+
console.log(JSON.stringify(result, null, 2));
|
|
108
|
+
} else {
|
|
109
|
+
// Display human-readable output
|
|
110
|
+
let title = `Objects in ${packageName}`;
|
|
111
|
+
if (type) {
|
|
112
|
+
title += ` (${type} only`;
|
|
113
|
+
if (total !== objects.length) {
|
|
114
|
+
title += `, Total: ${total}`;
|
|
115
|
+
}
|
|
116
|
+
title += ')';
|
|
117
|
+
} else if (total !== objects.length) {
|
|
118
|
+
title += ` (Total: ${total})`;
|
|
119
|
+
}
|
|
120
|
+
console.log(`\n${title}\n`);
|
|
121
|
+
|
|
122
|
+
// Group objects by type
|
|
123
|
+
const objectsByType = {};
|
|
124
|
+
for (const obj of objects) {
|
|
125
|
+
const objType = (obj.TYPE || obj.type || '???').toUpperCase();
|
|
126
|
+
if (!objectsByType[objType]) {
|
|
127
|
+
objectsByType[objType] = [];
|
|
128
|
+
}
|
|
129
|
+
objectsByType[objType].push(obj.NAME || obj.name);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Display grouped objects
|
|
133
|
+
for (const objType of Object.keys(objectsByType).sort()) {
|
|
134
|
+
const objNames = objectsByType[objType];
|
|
135
|
+
console.log(` ${objType} (${objNames.length})`);
|
|
136
|
+
for (const objName of objNames) {
|
|
137
|
+
console.log(` ${objName}`);
|
|
138
|
+
}
|
|
139
|
+
console.log('');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview command - Preview data from ABAP tables or CDS views
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
name: 'preview',
|
|
7
|
+
description: 'Preview data from ABAP tables or CDS views',
|
|
8
|
+
requiresAbapConfig: true,
|
|
9
|
+
requiresVersionCheck: true,
|
|
10
|
+
|
|
11
|
+
async execute(args, context) {
|
|
12
|
+
const { loadConfig, AbapHttp, validators } = context;
|
|
13
|
+
|
|
14
|
+
const objectsArgIndex = args.indexOf('--objects');
|
|
15
|
+
if (objectsArgIndex === -1 || objectsArgIndex + 1 >= args.length) {
|
|
16
|
+
console.error('Error: --objects parameter required');
|
|
17
|
+
console.error('Usage: abapgit-agent preview --objects <table1>,<view1>,... [--type <type>] [--limit <n>] [--offset <n>] [--where <condition>] [--columns <cols>] [--vertical] [--compact] [--json]');
|
|
18
|
+
console.error('Example: abapgit-agent preview --objects SFLIGHT');
|
|
19
|
+
console.error('Example: abapgit-agent preview --objects ZC_MY_CDS_VIEW --type DDLS');
|
|
20
|
+
console.error('Example: abapgit-agent preview --objects SFLIGHT --where "CARRID = \'AA\'"');
|
|
21
|
+
console.error('Example: abapgit-agent preview --objects SFLIGHT --offset 10 --limit 20');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const objects = args[objectsArgIndex + 1].split(',').map(o => o.trim().toUpperCase());
|
|
26
|
+
const typeArg = args.indexOf('--type');
|
|
27
|
+
const type = typeArg !== -1 ? args[typeArg + 1].toUpperCase() : null;
|
|
28
|
+
const limitArg = args.indexOf('--limit');
|
|
29
|
+
const limitRaw = limitArg !== -1 ? args[limitArg + 1] : null;
|
|
30
|
+
const limitParsed = parseInt(limitRaw, 10);
|
|
31
|
+
const limit = limitRaw && !limitRaw.startsWith('--') && !isNaN(limitParsed) ? Math.max(1, limitParsed) : 100;
|
|
32
|
+
const offsetArg = args.indexOf('--offset');
|
|
33
|
+
const offsetRaw = offsetArg !== -1 ? args[offsetArg + 1] : null;
|
|
34
|
+
const offsetParsed = parseInt(offsetRaw, 10);
|
|
35
|
+
const offset = offsetRaw && !offsetRaw.startsWith('--') && !isNaN(offsetParsed) ? Math.max(0, offsetParsed) : 0;
|
|
36
|
+
const whereArg = args.indexOf('--where');
|
|
37
|
+
const where = whereArg !== -1 ? args[whereArg + 1] : null;
|
|
38
|
+
const columnsArg = args.indexOf('--columns');
|
|
39
|
+
const columns = columnsArg !== -1 ? args[columnsArg + 1].split(',').map(c => c.trim().toUpperCase()) : null;
|
|
40
|
+
const verticalOutput = args.includes('--vertical');
|
|
41
|
+
const compactOutput = args.includes('--compact');
|
|
42
|
+
const jsonOutput = args.includes('--json');
|
|
43
|
+
|
|
44
|
+
console.log(`\n Previewing ${objects.length} object(s)`);
|
|
45
|
+
|
|
46
|
+
const config = loadConfig();
|
|
47
|
+
const http = new AbapHttp(config);
|
|
48
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
49
|
+
|
|
50
|
+
const data = {
|
|
51
|
+
objects: objects,
|
|
52
|
+
limit: Math.min(Math.max(1, limit), 500),
|
|
53
|
+
offset: Math.max(0, offset)
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (type) {
|
|
57
|
+
data.type = type;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (where) {
|
|
61
|
+
data.where = validators.convertDatesInWhereClause(where);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (columns) {
|
|
65
|
+
data.columns = columns;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = await http.post('/sap/bc/z_abapgit_agent/preview', data, { csrfToken });
|
|
69
|
+
|
|
70
|
+
// Handle uppercase keys from ABAP
|
|
71
|
+
const success = result.SUCCESS || result.success;
|
|
72
|
+
const previewObjects = result.OBJECTS || result.objects || [];
|
|
73
|
+
const message = result.MESSAGE || result.message || '';
|
|
74
|
+
const error = result.ERROR || result.error;
|
|
75
|
+
|
|
76
|
+
if (!success || error) {
|
|
77
|
+
console.error(`\n Error: ${error || 'Failed to preview objects'}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const pagination = result.PAGINATION || result.pagination || null;
|
|
82
|
+
|
|
83
|
+
if (jsonOutput) {
|
|
84
|
+
console.log(JSON.stringify(result, null, 2));
|
|
85
|
+
} else {
|
|
86
|
+
// Build pagination message
|
|
87
|
+
let paginationMsg = '';
|
|
88
|
+
const paginationTotal = pagination ? (pagination.TOTAL || pagination.total || 0) : 0;
|
|
89
|
+
const paginationHasMore = pagination ? (pagination.HAS_MORE || pagination.has_more || false) : false;
|
|
90
|
+
if (pagination && paginationTotal > 0) {
|
|
91
|
+
// Handle case where offset exceeds total
|
|
92
|
+
if (offset >= paginationTotal) {
|
|
93
|
+
paginationMsg = ` (Offset ${offset} exceeds total ${paginationTotal})`;
|
|
94
|
+
} else {
|
|
95
|
+
const start = offset + 1;
|
|
96
|
+
const end = Math.min(offset + limit, paginationTotal);
|
|
97
|
+
paginationMsg = ` (Showing ${start}-${end} of ${paginationTotal})`;
|
|
98
|
+
if (paginationHasMore) {
|
|
99
|
+
paginationMsg += ` — Use --offset ${offset + limit} to see more`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.log(`\n ${message}${paginationMsg}`);
|
|
105
|
+
console.log('');
|
|
106
|
+
|
|
107
|
+
// Track if columns were explicitly specified
|
|
108
|
+
const columnsExplicit = columns !== null;
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < previewObjects.length; i++) {
|
|
111
|
+
const obj = previewObjects[i];
|
|
112
|
+
const objName = obj.NAME || obj.name || `Object ${i + 1}`;
|
|
113
|
+
const objType = obj.TYPE || obj.type || '';
|
|
114
|
+
const objTypeText = obj.TYPE_TEXT || obj.type_text || '';
|
|
115
|
+
// Parse rows - could be a JSON string or array
|
|
116
|
+
let rows = obj.ROWS || obj.rows || [];
|
|
117
|
+
if (typeof rows === 'string') {
|
|
118
|
+
try {
|
|
119
|
+
rows = JSON.parse(rows);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
rows = [];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const fields = obj.FIELDS || obj.fields || [];
|
|
125
|
+
const rowCount = obj.ROW_COUNT || obj.row_count || 0;
|
|
126
|
+
const totalRows = obj.TOTAL_ROWS || obj.total_rows || 0;
|
|
127
|
+
const notFound = obj.NOT_FOUND || obj.not_found || false;
|
|
128
|
+
const accessDenied = obj.ACCESS_DENIED || obj.access_denied || false;
|
|
129
|
+
|
|
130
|
+
// Check if object was not found
|
|
131
|
+
if (notFound) {
|
|
132
|
+
console.log(` ❌ ${objName} (${objTypeText})`);
|
|
133
|
+
console.log(` Object not found: ${objName}`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if access denied
|
|
138
|
+
if (accessDenied) {
|
|
139
|
+
console.log(` ❌ ${objName} (${objTypeText})`);
|
|
140
|
+
console.log(` Access denied to: ${objName}`);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(` 📊 Preview: ${objName} (${objTypeText})`);
|
|
145
|
+
|
|
146
|
+
// Check for errors first
|
|
147
|
+
const objError = obj.ERROR || obj.error;
|
|
148
|
+
if (objError) {
|
|
149
|
+
console.log(` ❌ Error: ${objError}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (rows.length === 0) {
|
|
154
|
+
console.log(' No data found');
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Get all unique field names from all rows
|
|
159
|
+
const allFields = new Set();
|
|
160
|
+
rows.forEach(row => {
|
|
161
|
+
Object.keys(row).forEach(key => allFields.add(key));
|
|
162
|
+
});
|
|
163
|
+
const allFieldNames = Array.from(allFields);
|
|
164
|
+
|
|
165
|
+
// Display as table - use fields metadata only if --columns was explicitly specified
|
|
166
|
+
let fieldNames;
|
|
167
|
+
let columnsAutoSelected = false;
|
|
168
|
+
if (columnsExplicit && fields && fields.length > 0) {
|
|
169
|
+
// Use fields from metadata (filtered by explicit --columns)
|
|
170
|
+
fieldNames = fields.map(f => f.FIELD || f.field);
|
|
171
|
+
} else {
|
|
172
|
+
// Use all fields - let terminal handle wrapping if needed
|
|
173
|
+
// Terminal width detection is unreliable without a proper TTY
|
|
174
|
+
fieldNames = allFieldNames;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Calculate column widths - use reasonable defaults
|
|
178
|
+
const colWidths = {};
|
|
179
|
+
const maxColWidth = compactOutput ? 10 : 20;
|
|
180
|
+
fieldNames.forEach(field => {
|
|
181
|
+
let maxWidth = field.length;
|
|
182
|
+
rows.forEach(row => {
|
|
183
|
+
const value = String(row[field] || '');
|
|
184
|
+
maxWidth = Math.max(maxWidth, value.length);
|
|
185
|
+
});
|
|
186
|
+
// Cap at maxColWidth (truncates both headers and data in compact mode)
|
|
187
|
+
colWidths[field] = Math.min(maxWidth, maxColWidth);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Render output - either vertical or table
|
|
191
|
+
if (verticalOutput) {
|
|
192
|
+
// Vertical format: each field on its own line
|
|
193
|
+
rows.forEach((row, rowIndex) => {
|
|
194
|
+
if (rows.length > 1) {
|
|
195
|
+
console.log(`\n Row ${rowIndex + 1}:`);
|
|
196
|
+
console.log(' ' + '─'.repeat(30));
|
|
197
|
+
}
|
|
198
|
+
fieldNames.forEach(field => {
|
|
199
|
+
const value = String(row[field] || '');
|
|
200
|
+
console.log(` ${field}: ${value}`);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
console.log('');
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Build table header
|
|
208
|
+
let headerLine = ' ┌';
|
|
209
|
+
let separatorLine = ' ├';
|
|
210
|
+
fieldNames.forEach(field => {
|
|
211
|
+
const width = colWidths[field];
|
|
212
|
+
headerLine += '─'.repeat(width + 2) + '┬';
|
|
213
|
+
separatorLine += '─'.repeat(width + 2) + '┼';
|
|
214
|
+
});
|
|
215
|
+
headerLine = headerLine.slice(0, -1) + '┐';
|
|
216
|
+
separatorLine = separatorLine.slice(0, -1) + '┤';
|
|
217
|
+
|
|
218
|
+
// Build header row
|
|
219
|
+
let headerRow = ' │';
|
|
220
|
+
fieldNames.forEach(field => {
|
|
221
|
+
const width = colWidths[field];
|
|
222
|
+
let displayField = String(field);
|
|
223
|
+
if (displayField.length > width) {
|
|
224
|
+
displayField = displayField.slice(0, width - 3) + '...';
|
|
225
|
+
}
|
|
226
|
+
headerRow += ' ' + displayField.padEnd(width) + ' │';
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
console.log(headerLine);
|
|
230
|
+
console.log(headerRow);
|
|
231
|
+
console.log(separatorLine);
|
|
232
|
+
|
|
233
|
+
// Build data rows
|
|
234
|
+
rows.forEach(row => {
|
|
235
|
+
let dataRow = ' │';
|
|
236
|
+
fieldNames.forEach(field => {
|
|
237
|
+
const width = colWidths[field];
|
|
238
|
+
const value = String(row[field] || '');
|
|
239
|
+
const displayValue = value.length > width ? value.slice(0, width - 3) + '...' : value;
|
|
240
|
+
dataRow += ' ' + displayValue.padEnd(width) + ' │';
|
|
241
|
+
});
|
|
242
|
+
console.log(dataRow);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Build bottom border
|
|
246
|
+
let bottomLine = ' └';
|
|
247
|
+
fieldNames.forEach(field => {
|
|
248
|
+
const width = colWidths[field];
|
|
249
|
+
bottomLine += '─'.repeat(width + 2) + '┴';
|
|
250
|
+
});
|
|
251
|
+
bottomLine = bottomLine.slice(0, -1) + '┘';
|
|
252
|
+
console.log(bottomLine);
|
|
253
|
+
|
|
254
|
+
// Show row count
|
|
255
|
+
if (totalRows > rowCount) {
|
|
256
|
+
console.log(`\n Showing ${rowCount} of ${totalRows} rows`);
|
|
257
|
+
} else {
|
|
258
|
+
console.log(`\n ${rowCount} row(s)`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Note about hidden columns only when --columns was explicitly specified
|
|
262
|
+
const columnsDisplayed = fieldNames.length;
|
|
263
|
+
let columnsHidden = [];
|
|
264
|
+
|
|
265
|
+
if (columnsExplicit) {
|
|
266
|
+
columnsHidden = obj.COLUMNS_HIDDEN || obj.columns_hidden || [];
|
|
267
|
+
if (columnsHidden.length > 0) {
|
|
268
|
+
console.log(`\n ⚠️ ${columnsHidden.length} more columns hidden (${columnsHidden.join(', ')})`);
|
|
269
|
+
console.log(' Use --json for full data');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
console.log('');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull command - Pull and activate ABAP objects from git repository
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
name: 'pull',
|
|
7
|
+
description: 'Pull and activate ABAP objects from git repository',
|
|
8
|
+
requiresAbapConfig: true,
|
|
9
|
+
requiresVersionCheck: true,
|
|
10
|
+
|
|
11
|
+
async execute(args, context) {
|
|
12
|
+
const { loadConfig, AbapHttp, gitUtils, getTransport } = context;
|
|
13
|
+
|
|
14
|
+
const urlArgIndex = args.indexOf('--url');
|
|
15
|
+
const branchArgIndex = args.indexOf('--branch');
|
|
16
|
+
const filesArgIndex = args.indexOf('--files');
|
|
17
|
+
const transportArgIndex = args.indexOf('--transport');
|
|
18
|
+
|
|
19
|
+
// Auto-detect git remote URL if not provided
|
|
20
|
+
let gitUrl = urlArgIndex !== -1 ? args[urlArgIndex + 1] : null;
|
|
21
|
+
let branch = branchArgIndex !== -1 ? args[branchArgIndex + 1] : gitUtils.getBranch();
|
|
22
|
+
let files = null;
|
|
23
|
+
|
|
24
|
+
// Transport: CLI arg takes priority, then config/environment, then null
|
|
25
|
+
let transportRequest = null;
|
|
26
|
+
if (transportArgIndex !== -1 && transportArgIndex + 1 < args.length) {
|
|
27
|
+
// Explicit --transport argument
|
|
28
|
+
transportRequest = args[transportArgIndex + 1];
|
|
29
|
+
} else {
|
|
30
|
+
// Fall back to config or environment variable
|
|
31
|
+
transportRequest = getTransport();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (filesArgIndex !== -1 && filesArgIndex + 1 < args.length) {
|
|
35
|
+
files = args[filesArgIndex + 1].split(',').map(f => f.trim());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!gitUrl) {
|
|
39
|
+
gitUrl = gitUtils.getRemoteUrl();
|
|
40
|
+
if (!gitUrl) {
|
|
41
|
+
console.error('Error: Not in a git repository or no remote configured.');
|
|
42
|
+
console.error('Either run from a git repo, or specify --url <git-url>');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
console.log(`📌 Auto-detected git remote: ${gitUrl}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await this.pull(gitUrl, branch, files, transportRequest, loadConfig, AbapHttp);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async pull(gitUrl, branch = 'main', files = null, transportRequest = null, loadConfig, AbapHttp) {
|
|
52
|
+
const TERM_WIDTH = process.stdout.columns || 80;
|
|
53
|
+
|
|
54
|
+
console.log(`\n🚀 Starting pull for: ${gitUrl}`);
|
|
55
|
+
console.log(` Branch: ${branch}`);
|
|
56
|
+
if (files && files.length > 0) {
|
|
57
|
+
console.log(` Files: ${files.join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
if (transportRequest) {
|
|
60
|
+
console.log(` Transport Request: ${transportRequest}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const config = loadConfig();
|
|
65
|
+
|
|
66
|
+
// Fetch CSRF token first
|
|
67
|
+
const http = new AbapHttp(config);
|
|
68
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
69
|
+
|
|
70
|
+
// Prepare request data with git credentials
|
|
71
|
+
const data = {
|
|
72
|
+
url: gitUrl,
|
|
73
|
+
branch: branch,
|
|
74
|
+
username: config.gitUsername,
|
|
75
|
+
password: config.gitPassword
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Add files array if specified
|
|
79
|
+
if (files && files.length > 0) {
|
|
80
|
+
data.files = files;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add transport request if specified
|
|
84
|
+
if (transportRequest) {
|
|
85
|
+
data.transport_request = transportRequest;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result = await http.post('/sap/bc/z_abapgit_agent/pull', data, { csrfToken });
|
|
89
|
+
|
|
90
|
+
console.log('\n');
|
|
91
|
+
|
|
92
|
+
// Display raw result for debugging
|
|
93
|
+
if (process.env.DEBUG) {
|
|
94
|
+
console.log('Raw result:', JSON.stringify(result, null, 2));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Handle uppercase keys from ABAP
|
|
98
|
+
const success = result.SUCCESS || result.success;
|
|
99
|
+
const jobId = result.JOB_ID || result.job_id;
|
|
100
|
+
const message = result.MESSAGE || result.message;
|
|
101
|
+
const errorDetail = result.ERROR_DETAIL || result.error_detail;
|
|
102
|
+
const transportRequestUsed = result.TRANSPORT_REQUEST || result.transport_request;
|
|
103
|
+
const activatedCount = result.ACTIVATED_COUNT || result.activated_count || 0;
|
|
104
|
+
const failedCount = result.FAILED_COUNT || result.failed_count || 0;
|
|
105
|
+
const logMessages = result.LOG_MESSAGES || result.log_messages || [];
|
|
106
|
+
const activatedObjects = result.ACTIVATED_OBJECTS || result.activated_objects || [];
|
|
107
|
+
const failedObjects = result.FAILED_OBJECTS || result.failed_objects || [];
|
|
108
|
+
|
|
109
|
+
// Icon mapping for message types
|
|
110
|
+
const getIcon = (type) => {
|
|
111
|
+
const icons = {
|
|
112
|
+
'S': '✅', // Success
|
|
113
|
+
'E': '❌', // Error
|
|
114
|
+
'W': '⚠️', // Warning
|
|
115
|
+
'A': '🛑' // Abort
|
|
116
|
+
};
|
|
117
|
+
return icons[type] || '';
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Calculate display width accounting for emoji (2 cells) vs ASCII (1 cell)
|
|
121
|
+
const calcWidth = (str) => {
|
|
122
|
+
if (!str) return 0;
|
|
123
|
+
let width = 0;
|
|
124
|
+
let i = 0;
|
|
125
|
+
while (i < str.length) {
|
|
126
|
+
const code = str.codePointAt(i);
|
|
127
|
+
if (!code) break;
|
|
128
|
+
// Variation selectors (FE00-FE0F) and ZWJ (200D) take 0 width
|
|
129
|
+
if (code >= 0xFE00 && code <= 0xFE0F) {
|
|
130
|
+
i += 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (code === 0x200D) { // Zero width joiner
|
|
134
|
+
i += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Emoji and wide characters take 2 cells
|
|
138
|
+
if (code > 0xFFFF) {
|
|
139
|
+
width += 2;
|
|
140
|
+
i += 2; // Skip surrogate pair
|
|
141
|
+
} else if (code > 127) {
|
|
142
|
+
width += 2;
|
|
143
|
+
i += 1;
|
|
144
|
+
} else {
|
|
145
|
+
width += 1;
|
|
146
|
+
i += 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return width;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Pad string to display width
|
|
153
|
+
const padToWidth = (str, width) => {
|
|
154
|
+
const s = str || '';
|
|
155
|
+
const currentWidth = calcWidth(s);
|
|
156
|
+
return s + ' '.repeat(Math.max(0, width - currentWidth));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (success === 'X' || success === true) {
|
|
160
|
+
console.log(`✅ Pull completed successfully!`);
|
|
161
|
+
console.log(` Job ID: ${jobId || 'N/A'}`);
|
|
162
|
+
console.log(` Message: ${message || 'N/A'}`);
|
|
163
|
+
} else {
|
|
164
|
+
console.log(`❌ Pull completed with errors!`);
|
|
165
|
+
console.log(` Job ID: ${jobId || 'N/A'}`);
|
|
166
|
+
console.log(` Message: ${message || 'N/A'}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Display error detail if available
|
|
170
|
+
if (errorDetail && errorDetail.trim()) {
|
|
171
|
+
console.log(`\n📋 Error Details:`);
|
|
172
|
+
console.log('─'.repeat(TERM_WIDTH));
|
|
173
|
+
// Handle escaped newlines from ABAP JSON
|
|
174
|
+
const formattedDetail = errorDetail.replace(/\\n/g, '\n');
|
|
175
|
+
console.log(formattedDetail);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Display all messages as table (from log_messages)
|
|
179
|
+
if (logMessages && logMessages.length > 0) {
|
|
180
|
+
console.log(`\n📋 Pull Log (${logMessages.length} messages):`);
|
|
181
|
+
|
|
182
|
+
// Calculate column widths based on terminal width
|
|
183
|
+
const tableWidth = Math.min(TERM_WIDTH, 120);
|
|
184
|
+
const iconCol = 4; // Fixed width for icon column
|
|
185
|
+
const objCol = 28;
|
|
186
|
+
const msgCol = tableWidth - iconCol - objCol - 6; // Account for vertical lines (3 chars)
|
|
187
|
+
|
|
188
|
+
const headerLine = '─'.repeat(iconCol) + '┼' + '─'.repeat(objCol) + '┼' + '─'.repeat(msgCol);
|
|
189
|
+
const headerText = padToWidth('Icon', iconCol) + '│' + padToWidth('Object', objCol) + '│' + 'Message'.substring(0, msgCol);
|
|
190
|
+
const borderLine = '─'.repeat(tableWidth);
|
|
191
|
+
|
|
192
|
+
console.log(borderLine);
|
|
193
|
+
console.log(headerText);
|
|
194
|
+
console.log(headerLine);
|
|
195
|
+
|
|
196
|
+
for (const msg of logMessages) {
|
|
197
|
+
const icon = getIcon(msg.TYPE);
|
|
198
|
+
const objType = msg.OBJ_TYPE || '';
|
|
199
|
+
const objName = msg.OBJ_NAME || '';
|
|
200
|
+
const obj = objType && objName ? `${objType} ${objName}` : '';
|
|
201
|
+
const text = msg.TEXT || '';
|
|
202
|
+
|
|
203
|
+
// Truncate long text
|
|
204
|
+
const displayText = text.length > msgCol ? text.substring(0, msgCol - 3) + '...' : text;
|
|
205
|
+
|
|
206
|
+
console.log(padToWidth(icon, iconCol) + '│' + padToWidth(obj.substring(0, objCol), objCol) + '│' + displayText);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Extract objects with errors from log messages (type 'E' = Error)
|
|
211
|
+
const failedObjectsFromLog = [];
|
|
212
|
+
const objectsWithErrors = new Set();
|
|
213
|
+
|
|
214
|
+
for (const msg of logMessages) {
|
|
215
|
+
if (msg.TYPE === 'E' && msg.OBJ_TYPE && msg.OBJ_NAME) {
|
|
216
|
+
const objKey = `${msg.OBJ_TYPE} ${msg.OBJ_NAME}`;
|
|
217
|
+
objectsWithErrors.add(objKey);
|
|
218
|
+
failedObjectsFromLog.push({
|
|
219
|
+
OBJ_TYPE: msg.OBJ_TYPE,
|
|
220
|
+
OBJ_NAME: msg.OBJ_NAME,
|
|
221
|
+
TEXT: msg.TEXT || 'Activation failed',
|
|
222
|
+
EXCEPTION: msg.EXCEPTION || ''
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Filter activated objects - only include objects without errors
|
|
228
|
+
const filteredActivatedObjects = activatedObjects.filter(obj => {
|
|
229
|
+
const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
|
|
230
|
+
return objKey && !objectsWithErrors.has(objKey);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Display unique activated objects (excluding objects with errors)
|
|
234
|
+
if (filteredActivatedObjects && filteredActivatedObjects.length > 0) {
|
|
235
|
+
console.log(`\n📦 Activated Objects (${filteredActivatedObjects.length}):`);
|
|
236
|
+
console.log('─'.repeat(TERM_WIDTH));
|
|
237
|
+
for (const obj of filteredActivatedObjects) {
|
|
238
|
+
console.log(`✅ ${obj.OBJ_TYPE} ${obj.OBJ_NAME}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Display failed objects log (all error messages, duplicates allowed)
|
|
243
|
+
if (failedObjectsFromLog.length > 0) {
|
|
244
|
+
console.log(`\n❌ Failed Objects Log (${failedObjectsFromLog.length} entries):`);
|
|
245
|
+
console.log('─'.repeat(TERM_WIDTH));
|
|
246
|
+
for (const obj of failedObjectsFromLog) {
|
|
247
|
+
const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
|
|
248
|
+
let text = obj.TEXT || 'Activation failed';
|
|
249
|
+
// Include exception text if available
|
|
250
|
+
if (obj.EXCEPTION && obj.EXCEPTION.trim()) {
|
|
251
|
+
text = `${text}\nException: ${obj.EXCEPTION}`;
|
|
252
|
+
}
|
|
253
|
+
console.log(`❌ ${objKey}: ${text}`);
|
|
254
|
+
}
|
|
255
|
+
} else if (failedObjects && failedObjects.length > 0) {
|
|
256
|
+
// Fallback to API failed_objects if no errors in log
|
|
257
|
+
console.log(`\n❌ Failed Objects Log (${failedObjects.length}):`);
|
|
258
|
+
console.log('─'.repeat(TERM_WIDTH));
|
|
259
|
+
for (const obj of failedObjects) {
|
|
260
|
+
const objKey = obj.OBJ_TYPE && obj.OBJ_NAME ? `${obj.OBJ_TYPE} ${obj.OBJ_NAME}` : '';
|
|
261
|
+
let text = obj.TEXT || 'Activation failed';
|
|
262
|
+
// Include exception text if available
|
|
263
|
+
if (obj.EXCEPTION && obj.EXCEPTION.trim()) {
|
|
264
|
+
text = `${text}\nException: ${obj.EXCEPTION}`;
|
|
265
|
+
}
|
|
266
|
+
console.log(`❌ ${objKey}: ${text}`);
|
|
267
|
+
}
|
|
268
|
+
} else if (failedCount > 0) {
|
|
269
|
+
console.log(`\n❌ Failed Objects Log (${failedCount})`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return result;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error(`\n❌ Error: ${error.message}`);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|