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.
Files changed (39) hide show
  1. package/README.md +7 -7
  2. package/abap/CLAUDE.md +145 -26
  3. package/abap/guidelines/00_index.md +8 -0
  4. package/abap/guidelines/01_sql.md +8 -0
  5. package/abap/guidelines/02_exceptions.md +8 -0
  6. package/abap/guidelines/03_testing.md +8 -0
  7. package/abap/guidelines/04_cds.md +8 -0
  8. package/abap/guidelines/05_classes.md +8 -0
  9. package/abap/guidelines/06_objects.md +8 -0
  10. package/abap/guidelines/07_json.md +8 -0
  11. package/abap/guidelines/08_abapgit.md +8 -0
  12. package/abap/guidelines/09_unit_testable_code.md +8 -0
  13. package/bin/abapgit-agent +61 -2852
  14. package/package.json +21 -5
  15. package/src/agent.js +205 -16
  16. package/src/commands/create.js +102 -0
  17. package/src/commands/delete.js +72 -0
  18. package/src/commands/health.js +24 -0
  19. package/src/commands/help.js +111 -0
  20. package/src/commands/import.js +99 -0
  21. package/src/commands/init.js +321 -0
  22. package/src/commands/inspect.js +184 -0
  23. package/src/commands/list.js +143 -0
  24. package/src/commands/preview.js +277 -0
  25. package/src/commands/pull.js +278 -0
  26. package/src/commands/ref.js +96 -0
  27. package/src/commands/status.js +52 -0
  28. package/src/commands/syntax.js +290 -0
  29. package/src/commands/tree.js +209 -0
  30. package/src/commands/unit.js +133 -0
  31. package/src/commands/view.js +215 -0
  32. package/src/commands/where.js +138 -0
  33. package/src/config.js +11 -1
  34. package/src/utils/abap-http.js +347 -0
  35. package/src/utils/git-utils.js +58 -0
  36. package/src/utils/validators.js +72 -0
  37. package/src/utils/version-check.js +80 -0
  38. package/src/abap-client.js +0 -526
  39. /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
+ };