abapgit-agent 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.abapGitAgent.example +11 -0
  2. package/README.md +7 -7
  3. package/abap/.github/copilot-instructions.md +254 -0
  4. package/abap/CLAUDE.md +432 -0
  5. package/abap/guidelines/00_index.md +8 -0
  6. package/abap/guidelines/01_sql.md +8 -0
  7. package/abap/guidelines/02_exceptions.md +8 -0
  8. package/abap/guidelines/03_testing.md +8 -0
  9. package/abap/guidelines/04_cds.md +8 -0
  10. package/abap/guidelines/05_classes.md +8 -0
  11. package/abap/guidelines/06_objects.md +8 -0
  12. package/abap/guidelines/07_json.md +8 -0
  13. package/abap/guidelines/08_abapgit.md +8 -0
  14. package/abap/guidelines/09_unit_testable_code.md +8 -0
  15. package/bin/abapgit-agent +61 -2789
  16. package/package.json +25 -5
  17. package/src/agent.js +213 -20
  18. package/src/commands/create.js +102 -0
  19. package/src/commands/delete.js +72 -0
  20. package/src/commands/health.js +24 -0
  21. package/src/commands/help.js +111 -0
  22. package/src/commands/import.js +99 -0
  23. package/src/commands/init.js +321 -0
  24. package/src/commands/inspect.js +184 -0
  25. package/src/commands/list.js +143 -0
  26. package/src/commands/preview.js +277 -0
  27. package/src/commands/pull.js +278 -0
  28. package/src/commands/ref.js +96 -0
  29. package/src/commands/status.js +52 -0
  30. package/src/commands/syntax.js +290 -0
  31. package/src/commands/tree.js +209 -0
  32. package/src/commands/unit.js +133 -0
  33. package/src/commands/view.js +215 -0
  34. package/src/commands/where.js +138 -0
  35. package/src/config.js +11 -1
  36. package/src/utils/abap-http.js +347 -0
  37. package/src/{ref-search.js → utils/abap-reference.js} +119 -1
  38. package/src/utils/git-utils.js +58 -0
  39. package/src/utils/validators.js +72 -0
  40. package/src/utils/version-check.js +80 -0
  41. package/src/abap-client.js +0 -523
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Inspect command - Syntax check for ABAP files
3
+ */
4
+
5
+ const pathModule = require('path');
6
+
7
+ /**
8
+ * Inspect all files in one request
9
+ */
10
+ async function inspectAllFiles(files, csrfToken, config, variant, http) {
11
+ // Convert files to uppercase names
12
+ const fileNames = files.map(f => {
13
+ const baseName = pathModule.basename(f).toUpperCase();
14
+ return baseName;
15
+ });
16
+
17
+ try {
18
+ // Send all files in one request
19
+ const data = {
20
+ files: fileNames
21
+ };
22
+
23
+ // Add variant if specified
24
+ if (variant) {
25
+ data.variant = variant;
26
+ }
27
+
28
+ const result = await http.post('/sap/bc/z_abapgit_agent/inspect', data, { csrfToken });
29
+
30
+ // Handle both table result and old single result
31
+ let results = [];
32
+ if (Array.isArray(result)) {
33
+ results = result;
34
+ } else {
35
+ // Convert single result to array format
36
+ results = [{
37
+ OBJECT_TYPE: 'UNKNOWN',
38
+ OBJECT_NAME: files.join(', '),
39
+ SUCCESS: result.SUCCESS !== undefined ? result.SUCCESS === 'X' || result.SUCCESS === true : result.success,
40
+ ERROR_COUNT: result.ERROR_COUNT || result.error_count || 0,
41
+ ERRORS: result.ERRORS || result.errors || [],
42
+ WARNINGS: result.warnings || []
43
+ }];
44
+ }
45
+
46
+ return results;
47
+ } catch (error) {
48
+ console.error(`\n Error: ${error.message}`);
49
+ process.exit(1);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Process a single inspect result
55
+ */
56
+ async function processInspectResult(res) {
57
+ // Handle both uppercase and lowercase keys
58
+ const success = res.SUCCESS !== undefined ? res.SUCCESS : res.success;
59
+ const objectType = res.OBJECT_TYPE !== undefined ? res.OBJECT_TYPE : res.object_type;
60
+ const objectName = res.OBJECT_NAME !== undefined ? res.OBJECT_NAME : res.object_name;
61
+ const errorCount = res.ERROR_COUNT !== undefined ? res.ERROR_COUNT : (res.error_count || 0);
62
+ const errors = res.ERRORS !== undefined ? res.ERRORS : (res.errors || []);
63
+ const warnings = res.WARNINGS !== undefined ? res.WARNINGS : (res.warnings || []);
64
+ const infos = res.INFOS !== undefined ? res.INFOS : (res.infos || []);
65
+
66
+ if (errorCount > 0 || warnings.length > 0 || infos.length > 0) {
67
+ if (errorCount > 0) {
68
+ console.log(`❌ ${objectType} ${objectName} - Syntax check failed (${errorCount} error(s)):`);
69
+ } else {
70
+ const total = warnings.length + infos.length;
71
+ console.log(`⚠️ ${objectType} ${objectName} - Syntax check passed with warnings (${total}):`);
72
+ }
73
+ console.log('\nErrors:');
74
+ console.log('─'.repeat(60));
75
+
76
+ for (const err of errors) {
77
+ const line = err.LINE || err.line || '?';
78
+ const column = err.COLUMN || err.column || '?';
79
+ const text = err.TEXT || err.text || 'Unknown error';
80
+ const methodName = err.METHOD_NAME || err.method_name;
81
+ const sobjname = err.SOBJNAME || err.sobjname;
82
+
83
+ if (methodName) {
84
+ console.log(` Method: ${methodName}`);
85
+ }
86
+ console.log(` Line ${line}, Column ${column}:`);
87
+ if (sobjname && sobjname.includes('====')) {
88
+ console.log(` Include: ${sobjname}`);
89
+ }
90
+ console.log(` ${text}`);
91
+ console.log('');
92
+ }
93
+
94
+ // Show warnings if any
95
+ if (warnings.length > 0) {
96
+ console.log('Warnings:');
97
+ console.log('─'.repeat(60));
98
+ for (const warn of warnings) {
99
+ const line = warn.LINE || warn.line || '?';
100
+ const text = warn.MESSAGE || warn.message || 'Unknown warning';
101
+ const methodName = warn.METHOD_NAME || warn.method_name;
102
+ const sobjname = warn.SOBJNAME || warn.sobjname;
103
+
104
+ if (methodName) {
105
+ console.log(` Method: ${methodName}`);
106
+ }
107
+ console.log(` Line ${line}:`);
108
+ if (sobjname && sobjname.includes('====')) {
109
+ console.log(` Include: ${sobjname}`);
110
+ }
111
+ console.log(` ${text}`);
112
+ }
113
+ }
114
+
115
+ // Show infos if any
116
+ if (infos.length > 0) {
117
+ console.log('Info:');
118
+ console.log('─'.repeat(60));
119
+ for (const info of infos) {
120
+ const line = info.LINE || info.line || '?';
121
+ const text = info.MESSAGE || info.message || 'Unknown info';
122
+ const methodName = info.METHOD_NAME || info.method_name;
123
+ const sobjname = info.SOBJNAME || info.sobjname;
124
+
125
+ if (methodName) {
126
+ console.log(` Method: ${methodName}`);
127
+ }
128
+ console.log(` Line ${line}:`);
129
+ if (sobjname && sobjname.includes('====')) {
130
+ console.log(` Include: ${sobjname}`);
131
+ }
132
+ console.log(` ${text}`);
133
+ }
134
+ }
135
+ } else if (success === true || success === 'X') {
136
+ console.log(`✅ ${objectType} ${objectName} - Syntax check passed`);
137
+ } else {
138
+ console.log(`⚠️ ${objectType} ${objectName} - Syntax check returned unexpected status`);
139
+ }
140
+ }
141
+
142
+ module.exports = {
143
+ name: 'inspect',
144
+ description: 'Inspect ABAP source files for syntax issues',
145
+ requiresAbapConfig: true,
146
+ requiresVersionCheck: true,
147
+
148
+ async execute(args, context) {
149
+ const { loadConfig, AbapHttp } = context;
150
+
151
+ const filesArgIndex = args.indexOf('--files');
152
+ if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
153
+ console.error('Error: --files parameter required');
154
+ console.error('Usage: abapgit-agent inspect --files <file1>,<file2>,... [--variant <check-variant>]');
155
+ console.error('Example: abapgit-agent inspect --files src/zcl_my_class.clas.abap');
156
+ console.error('Example: abapgit-agent inspect --files src/zcl_my_class.clas.abap --variant ALL_CHECKS');
157
+ process.exit(1);
158
+ }
159
+
160
+ const filesSyntaxCheck = args[filesArgIndex + 1].split(',').map(f => f.trim());
161
+
162
+ // Parse optional --variant parameter
163
+ const variantArgIndex = args.indexOf('--variant');
164
+ const variant = variantArgIndex !== -1 ? args[variantArgIndex + 1] : null;
165
+
166
+ console.log(`\n Inspect for ${filesSyntaxCheck.length} file(s)`);
167
+ if (variant) {
168
+ console.log(` Using variant: ${variant}`);
169
+ }
170
+ console.log('');
171
+
172
+ const config = loadConfig();
173
+ const http = new AbapHttp(config);
174
+ const csrfToken = await http.fetchCsrfToken();
175
+
176
+ // Send all files in one request
177
+ const results = await inspectAllFiles(filesSyntaxCheck, csrfToken, config, variant, http);
178
+
179
+ // Process results
180
+ for (const result of results) {
181
+ await processInspectResult(result);
182
+ }
183
+ }
184
+ };
@@ -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
+ };