jtcsv 2.1.3 → 2.2.2

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 (52) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +60 -341
  3. package/bin/jtcsv.js +2462 -1372
  4. package/csv-to-json.js +35 -26
  5. package/dist/jtcsv.cjs.js +807 -133
  6. package/dist/jtcsv.cjs.js.map +1 -1
  7. package/dist/jtcsv.esm.js +800 -134
  8. package/dist/jtcsv.esm.js.map +1 -1
  9. package/dist/jtcsv.umd.js +807 -133
  10. package/dist/jtcsv.umd.js.map +1 -1
  11. package/errors.js +20 -0
  12. package/examples/browser-vanilla.html +37 -0
  13. package/examples/cli-batch-processing.js +38 -0
  14. package/examples/error-handling.js +324 -0
  15. package/examples/ndjson-processing.js +434 -0
  16. package/examples/react-integration.jsx +637 -0
  17. package/examples/schema-validation.js +640 -0
  18. package/examples/simple-usage.js +10 -7
  19. package/examples/typescript-example.ts +486 -0
  20. package/examples/web-workers-advanced.js +28 -0
  21. package/index.d.ts +2 -0
  22. package/json-save.js +2 -1
  23. package/json-to-csv.js +171 -131
  24. package/package.json +20 -4
  25. package/plugins/README.md +41 -467
  26. package/plugins/express-middleware/README.md +32 -274
  27. package/plugins/hono/README.md +16 -13
  28. package/plugins/nestjs/README.md +13 -11
  29. package/plugins/nextjs-api/README.md +28 -423
  30. package/plugins/nextjs-api/index.js +1 -2
  31. package/plugins/nextjs-api/route.js +1 -2
  32. package/plugins/nuxt/README.md +6 -7
  33. package/plugins/remix/README.md +9 -9
  34. package/plugins/sveltekit/README.md +8 -8
  35. package/plugins/trpc/README.md +8 -5
  36. package/src/browser/browser-functions.js +33 -3
  37. package/src/browser/csv-to-json-browser.js +269 -11
  38. package/src/browser/errors-browser.js +19 -1
  39. package/src/browser/index.js +39 -5
  40. package/src/browser/streams.js +393 -0
  41. package/src/browser/workers/csv-parser.worker.js +20 -2
  42. package/src/browser/workers/worker-pool.js +507 -447
  43. package/src/core/plugin-system.js +4 -0
  44. package/src/engines/fast-path-engine.js +31 -23
  45. package/src/errors.js +26 -0
  46. package/src/formats/ndjson-parser.js +54 -5
  47. package/src/formats/tsv-parser.js +4 -1
  48. package/src/utils/schema-validator.js +594 -0
  49. package/src/utils/transform-loader.js +205 -0
  50. package/src/web-server/index.js +683 -0
  51. package/stream-csv-to-json.js +16 -87
  52. package/stream-json-to-csv.js +18 -86
package/bin/jtcsv.js CHANGED
@@ -1,1372 +1,2462 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * jtcsv CLI - Complete Command Line Interface
5
- *
6
- * Full-featured command-line interface for JSON↔CSV conversion
7
- * with streaming, batch processing, and all security features.
8
- */
9
-
10
- const fs = require('fs');
11
- const path = require('path');
12
- const { pipeline } = require('stream/promises');
13
- const jtcsv = require('../index.js');
14
-
15
- const VERSION = require('../package.json').version;
16
-
17
- // ANSI colors for terminal output
18
- const colors = {
19
- reset: '\x1b[0m',
20
- bright: '\x1b[1m',
21
- dim: '\x1b[2m',
22
- red: '\x1b[31m',
23
- green: '\x1b[32m',
24
- yellow: '\x1b[33m',
25
- blue: '\x1b[34m',
26
- magenta: '\x1b[35m',
27
- cyan: '\x1b[36m',
28
- white: '\x1b[37m'
29
- };
30
-
31
- function color(text, colorName) {
32
- return colors[colorName] + text + colors.reset;
33
- }
34
-
35
- function showHelp() {
36
- console.log(`
37
- ${color('jtcsv CLI v' + VERSION, 'cyan')}
38
- ${color('The Complete JSON↔CSV Converter for Node.js', 'dim')}
39
-
40
- ${color('USAGE:', 'bright')}
41
- jtcsv [command] [options] [file...]
42
-
43
- ${color('MAIN COMMANDS:', 'bright')}
44
- ${color('json-to-csv', 'green')} Convert JSON to CSV (alias: json2csv)
45
- ${color('csv-to-json', 'green')} Convert CSV to JSON (alias: csv2json)
46
- ${color('save-json', 'yellow')} Save data as JSON file
47
- ${color('stream', 'yellow')} Streaming conversion for large files
48
- ${color('batch', 'yellow')} Batch process multiple files
49
- ${color('preprocess', 'magenta')} Preprocess JSON with deep unwrapping
50
- ${color('tui', 'magenta')} Launch Terminal User Interface (@jtcsv/tui)
51
- ${color('help', 'blue')} Show this help message
52
- ${color('version', 'blue')} Show version information
53
-
54
- ${color('STREAMING SUBCOMMANDS:', 'bright')}
55
- ${color('stream json-to-csv', 'dim')} Stream JSON to CSV
56
- ${color('stream csv-to-json', 'dim')} Stream CSV to JSON
57
- ${color('stream file-to-csv', 'dim')} Stream file to CSV
58
- ${color('stream file-to-json', 'dim')} Stream file to JSON
59
-
60
- ${color('BATCH SUBCOMMANDS:', 'bright')}
61
- ${color('batch json-to-csv', 'dim')} Batch convert JSON files to CSV
62
- ${color('batch csv-to-json', 'dim')} Batch convert CSV files to JSON
63
- ${color('batch process', 'dim')} Process mixed file types
64
-
65
- ${color('EXAMPLES:', 'bright')}
66
- ${color('Convert JSON file to CSV:', 'dim')}
67
- jtcsv json-to-csv input.json output.csv --delimiter=,
68
-
69
- ${color('Convert CSV file to JSON:', 'dim')}
70
- jtcsv csv-to-json input.csv output.json --parse-numbers --auto-detect
71
-
72
- ${color('Save data as JSON file:', 'dim')}
73
- jtcsv save-json data.json output.json --pretty
74
-
75
- ${color('Stream large JSON file to CSV:', 'dim')}
76
- jtcsv stream json-to-csv large.json output.csv --max-records=1000000
77
-
78
- ${color('Stream CSV file to JSON:', 'dim')}
79
- jtcsv stream csv-to-json large.csv output.json --max-rows=500000
80
-
81
- ${color('Preprocess complex JSON:', 'dim')}
82
- jtcsv preprocess complex.json simplified.json --max-depth=3
83
-
84
- ${color('Batch convert JSON files:', 'dim')}
85
- jtcsv batch json-to-csv "data/*.json" "output/" --delimiter=;
86
-
87
- ${color('Launch TUI interface:', 'dim')}
88
- jtcsv tui
89
-
90
- ${color('CONVERSION OPTIONS:', 'bright')}
91
- ${color('--delimiter=', 'cyan')}CHAR CSV delimiter (default: ;)
92
- ${color('--auto-detect', 'cyan')} Auto-detect delimiter (default: true)
93
- ${color('--candidates=', 'cyan')}LIST Delimiter candidates (default: ;,\t|)
94
- ${color('--no-headers', 'cyan')} Exclude headers from CSV output
95
- ${color('--parse-numbers', 'cyan')} Parse numeric values in CSV
96
- ${color('--parse-booleans', 'cyan')} Parse boolean values in CSV
97
- ${color('--no-trim', 'cyan')} Don't trim whitespace from CSV values
98
- ${color('--no-fast-path', 'cyan')} Disable fast-path parser (force quote-aware)
99
- ${color('--fast-path-mode=', 'cyan')}MODE Fast path output mode (objects|compact)
100
- ${color('--rename=', 'cyan')}JSON Rename columns (JSON map)
101
- ${color('--template=', 'cyan')}JSON Column order template (JSON object)
102
- ${color('--no-injection-protection', 'cyan')} Disable CSV injection protection
103
- ${color('--no-rfc4180', 'cyan')} Disable RFC 4180 compliance
104
- ${color('--max-records=', 'cyan')}N Maximum records to process
105
- ${color('--max-rows=', 'cyan')}N Maximum rows to process
106
- ${color('--pretty', 'cyan')} Pretty print JSON output
107
- ${color('--schema=', 'cyan')}JSON JSON schema for validation
108
- ${color('--transform=', 'cyan')}JS Custom transform function (JavaScript file)
109
-
110
- ${color('PREPROCESS OPTIONS:', 'bright')}
111
- ${color('--max-depth=', 'cyan')}N Maximum recursion depth (default: 5)
112
- ${color('--unwrap-arrays', 'cyan')} Unwrap arrays to strings
113
- ${color('--stringify-objects', 'cyan')} Stringify complex objects
114
-
115
- ${color('STREAMING OPTIONS:', 'bright')}
116
- ${color('--chunk-size=', 'cyan')}N Chunk size in bytes (default: 65536)
117
- ${color('--buffer-size=', 'cyan')}N Buffer size in records (default: 1000)
118
- ${color('--add-bom', 'cyan')} Add UTF-8 BOM for Excel compatibility
119
-
120
- ${color('BATCH OPTIONS:', 'bright')}
121
- ${color('--recursive', 'cyan')} Process directories recursively
122
- ${color('--pattern=', 'cyan')}GLOB File pattern to match
123
- ${color('--output-dir=', 'cyan')}DIR Output directory for batch processing
124
- ${color('--overwrite', 'cyan')} Overwrite existing files
125
- ${color('--parallel=', 'cyan')}N Parallel processing limit (default: 4)
126
-
127
- ${color('GENERAL OPTIONS:', 'bright')}
128
- ${color('--silent', 'cyan')} Suppress all output except errors
129
- ${color('--verbose', 'cyan')} Show detailed progress information
130
- ${color('--debug', 'cyan')} Show debug information
131
- ${color('--dry-run', 'cyan')} Show what would be done without actually doing it
132
-
133
- ${color('SECURITY FEATURES:', 'bright')}
134
- CSV injection protection (enabled by default)
135
- Path traversal protection
136
- Input validation and sanitization
137
- Size limits to prevent DoS attacks
138
- Schema validation support
139
-
140
- ${color('PERFORMANCE FEATURES:', 'bright')}
141
- Streaming for files >100MB
142
- Batch processing with parallel execution
143
- • Memory-efficient preprocessing
144
- Configurable buffer sizes
145
-
146
- ${color('LEARN MORE:', 'dim')}
147
- GitHub: https://github.com/Linol-Hamelton/jtcsv
148
- Issues: https://github.com/Linol-Hamelton/jtcsv/issues
149
- Documentation: https://github.com/Linol-Hamelton/jtcsv#readme
150
- `);
151
- }
152
-
153
- function showVersion() {
154
- console.log(`jtcsv v${VERSION}`);
155
- console.log(`Node.js ${process.version}`);
156
- console.log(`Platform: ${process.platform} ${process.arch}`);
157
- }
158
-
159
- // ============================================================================
160
- // CONVERSION FUNCTIONS
161
- // ============================================================================
162
-
163
- async function convertJsonToCsv(inputFile, outputFile, options) {
164
- const startTime = Date.now();
165
-
166
- try {
167
- // Read input file
168
- const inputData = await fs.promises.readFile(inputFile, 'utf8');
169
- const jsonData = JSON.parse(inputData);
170
-
171
- if (!Array.isArray(jsonData)) {
172
- throw new Error('JSON data must be an array of objects');
173
- }
174
-
175
- if (!options.silent) {
176
- console.log(color(`Converting ${jsonData.length.toLocaleString()} records...`, 'dim'));
177
- }
178
-
179
- // Prepare options for jtcsv
180
- const jtcsvOptions = {
181
- delimiter: options.delimiter,
182
- includeHeaders: options.includeHeaders,
183
- renameMap: options.renameMap,
184
- template: options.template,
185
- maxRecords: options.maxRecords,
186
- preventCsvInjection: options.preventCsvInjection,
187
- rfc4180Compliant: options.rfc4180Compliant
188
- };
189
-
190
- // Convert to CSV
191
- const csvData = jtcsv.jsonToCsv(jsonData, jtcsvOptions);
192
-
193
- // Write output file
194
- await fs.promises.writeFile(outputFile, csvData, 'utf8');
195
-
196
- const elapsed = Date.now() - startTime;
197
- if (!options.silent) {
198
- console.log(color(`✓ Converted ${jsonData.length.toLocaleString()} records in ${elapsed}ms`, 'green'));
199
- console.log(color(` Output: ${outputFile} (${csvData.length.toLocaleString()} bytes)`, 'dim'));
200
- }
201
-
202
- return { records: jsonData.length, bytes: csvData.length, time: elapsed };
203
-
204
- } catch (error) {
205
- console.error(color(`✗ Error: ${error.message}`, 'red'));
206
- if (options.debug) {
207
- console.error(error.stack);
208
- }
209
- process.exit(1);
210
- }
211
- }
212
-
213
- async function convertCsvToJson(inputFile, outputFile, options) {
214
- const startTime = Date.now();
215
-
216
- try {
217
- if (!options.silent) {
218
- console.log(color('Reading CSV file...', 'dim'));
219
- }
220
-
221
- // Prepare options for jtcsv
222
- const jtcsvOptions = {
223
- delimiter: options.delimiter,
224
- autoDetect: options.autoDetect,
225
- candidates: options.candidates,
226
- hasHeaders: options.hasHeaders,
227
- renameMap: options.renameMap,
228
- trim: options.trim,
229
- parseNumbers: options.parseNumbers,
230
- parseBooleans: options.parseBooleans,
231
- maxRows: options.maxRows,
232
- useFastPath: options.useFastPath,
233
- fastPathMode: options.fastPathMode
234
- };
235
-
236
- // Read and convert CSV
237
- const jsonData = await jtcsv.readCsvAsJson(inputFile, jtcsvOptions);
238
-
239
- // Format JSON
240
- const jsonOutput = options.pretty
241
- ? JSON.stringify(jsonData, null, 2)
242
- : JSON.stringify(jsonData);
243
-
244
- // Write output file
245
- await fs.promises.writeFile(outputFile, jsonOutput, 'utf8');
246
-
247
- const elapsed = Date.now() - startTime;
248
- if (!options.silent) {
249
- console.log(color(`✓ Converted ${jsonData.length.toLocaleString()} rows in ${elapsed}ms`, 'green'));
250
- console.log(color(` Output: ${outputFile} (${jsonOutput.length.toLocaleString()} bytes)`, 'dim'));
251
- }
252
-
253
- return { rows: jsonData.length, bytes: jsonOutput.length, time: elapsed };
254
-
255
- } catch (error) {
256
- console.error(color(`✗ Error: ${error.message}`, 'red'));
257
- if (options.debug) {
258
- console.error(error.stack);
259
- }
260
- process.exit(1);
261
- }
262
- }
263
-
264
- async function saveAsJson(inputFile, outputFile, options) {
265
- const startTime = Date.now();
266
-
267
- try {
268
- // Read input file
269
- const inputData = await fs.promises.readFile(inputFile, 'utf8');
270
- const jsonData = JSON.parse(inputData);
271
-
272
- if (!options.silent) {
273
- console.log(color(`Saving ${Array.isArray(jsonData) ? jsonData.length.toLocaleString() + ' records' : 'object'}...`, 'dim'));
274
- }
275
-
276
- // Prepare options for jtcsv
277
- const jtcsvOptions = {
278
- prettyPrint: options.pretty
279
- };
280
-
281
- // Save as JSON
282
- await jtcsv.saveAsJson(jsonData, outputFile, jtcsvOptions);
283
-
284
- const elapsed = Date.now() - startTime;
285
- if (!options.silent) {
286
- console.log(color(`✓ Saved JSON in ${elapsed}ms`, 'green'));
287
- console.log(color(` Output: ${outputFile}`, 'dim'));
288
- }
289
-
290
- return { time: elapsed };
291
-
292
- } catch (error) {
293
- console.error(color(`✗ Error: ${error.message}`, 'red'));
294
- if (options.debug) {
295
- console.error(error.stack);
296
- }
297
- process.exit(1);
298
- }
299
- }
300
-
301
- async function preprocessJson(inputFile, outputFile, options) {
302
- const startTime = Date.now();
303
-
304
- try {
305
- // Read input file
306
- const inputData = await fs.promises.readFile(inputFile, 'utf8');
307
- const jsonData = JSON.parse(inputData);
308
-
309
- if (!Array.isArray(jsonData)) {
310
- throw new Error('JSON data must be an array of objects for preprocessing');
311
- }
312
-
313
- if (!options.silent) {
314
- console.log(color(`Preprocessing ${jsonData.length.toLocaleString()} records...`, 'dim'));
315
- }
316
-
317
- // Preprocess data
318
- const processedData = jtcsv.preprocessData(jsonData);
319
-
320
- // Apply deep unwrap if needed
321
- if (options.unwrapArrays || options.stringifyObjects) {
322
- const maxDepth = options.maxDepth || 5;
323
- processedData.forEach(item => {
324
- for (const key in item) {
325
- if (item[key] && typeof item[key] === 'object') {
326
- item[key] = jtcsv.deepUnwrap(item[key], 0, maxDepth);
327
- }
328
- }
329
- });
330
- }
331
-
332
- // Format JSON
333
- const jsonOutput = options.pretty
334
- ? JSON.stringify(processedData, null, 2)
335
- : JSON.stringify(processedData);
336
-
337
- // Write output file
338
- await fs.promises.writeFile(outputFile, jsonOutput, 'utf8');
339
-
340
- const elapsed = Date.now() - startTime;
341
- if (!options.silent) {
342
- console.log(color(`✓ Preprocessed ${jsonData.length.toLocaleString()} records in ${elapsed}ms`, 'green'));
343
- console.log(color(` Output: ${outputFile} (${jsonOutput.length.toLocaleString()} bytes)`, 'dim'));
344
- }
345
-
346
- return { records: jsonData.length, bytes: jsonOutput.length, time: elapsed };
347
-
348
- } catch (error) {
349
- console.error(color(`✗ Error: ${error.message}`, 'red'));
350
- if (options.debug) {
351
- console.error(error.stack);
352
- }
353
- process.exit(1);
354
- }
355
- }
356
-
357
- // ============================================================================
358
- // STREAMING FUNCTIONS
359
- // ============================================================================
360
-
361
- async function streamJsonToCsv(inputFile, outputFile, options) {
362
- const startTime = Date.now();
363
- let recordCount = 0;
364
-
365
- try {
366
- if (!options.silent) {
367
- console.log(color('Streaming JSON to CSV...', 'dim'));
368
- }
369
-
370
- // Create streams
371
- const readStream = fs.createReadStream(inputFile, 'utf8');
372
- const writeStream = fs.createWriteStream(outputFile, 'utf8');
373
-
374
- // Add UTF-8 BOM if requested
375
- if (options.addBOM) {
376
- writeStream.write('\uFEFF');
377
- }
378
-
379
- // Parse JSON stream
380
- let buffer = '';
381
- let isFirstChunk = true;
382
- let headersWritten = false;
383
-
384
- readStream.on('data', (chunk) => {
385
- buffer += chunk;
386
-
387
- // Try to parse complete JSON objects
388
- const lines = buffer.split('\n');
389
- buffer = lines.pop() || '';
390
-
391
- for (const line of lines) {
392
- if (line.trim()) {
393
- try {
394
- const obj = JSON.parse(line);
395
- recordCount++;
396
-
397
- // Write headers on first object
398
- if (!headersWritten && options.includeHeaders !== false) {
399
- const headers = Object.keys(obj);
400
- writeStream.write(headers.join(options.delimiter || ';') + '\n');
401
- headersWritten = true;
402
- }
403
-
404
- // Write CSV row
405
- const row = Object.values(obj).map(value => {
406
- const str = String(value);
407
- if (str.includes(options.delimiter || ';') || str.includes('"') || str.includes('\n')) {
408
- return `"${str.replace(/"/g, '""')}"`;
409
- }
410
- return str;
411
- }).join(options.delimiter || ';') + '\n';
412
-
413
- writeStream.write(row);
414
-
415
- // Show progress
416
- if (options.verbose && recordCount % 10000 === 0) {
417
- process.stdout.write(color(` Processed ${recordCount.toLocaleString()} records\r`, 'dim'));
418
- }
419
-
420
- } catch (error) {
421
- // Skip invalid JSON lines
422
- if (options.debug) {
423
- console.warn(color(` Warning: Skipping invalid JSON line: ${error.message}`, 'yellow'));
424
- }
425
- }
426
- }
427
- }
428
- });
429
-
430
- readStream.on('end', async () => {
431
- // Process remaining buffer
432
- if (buffer.trim()) {
433
- try {
434
- const obj = JSON.parse(buffer);
435
- recordCount++;
436
-
437
- if (!headersWritten && options.includeHeaders !== false) {
438
- const headers = Object.keys(obj);
439
- writeStream.write(headers.join(options.delimiter || ';') + '\n');
440
- }
441
-
442
- const row = Object.values(obj).map(value => {
443
- const str = String(value);
444
- if (str.includes(options.delimiter || ';') || str.includes('"') || str.includes('\n')) {
445
- return `"${str.replace(/"/g, '""')}"`;
446
- }
447
- return str;
448
- }).join(options.delimiter || ';') + '\n';
449
-
450
- writeStream.write(row);
451
- } catch (error) {
452
- // Skip invalid JSON
453
- }
454
- }
455
-
456
- writeStream.end();
457
-
458
- // Wait for write stream to finish
459
- await new Promise(resolve => writeStream.on('finish', resolve));
460
-
461
- const elapsed = Date.now() - startTime;
462
- if (!options.silent) {
463
- console.log(color(`\n✓ Streamed ${recordCount.toLocaleString()} records in ${elapsed}ms`, 'green'));
464
- console.log(color(` Output: ${outputFile}`, 'dim'));
465
- }
466
- });
467
-
468
- readStream.on('error', (error) => {
469
- console.error(color(`✗ Stream error: ${error.message}`, 'red'));
470
- process.exit(1);
471
- });
472
-
473
- writeStream.on('error', (error) => {
474
- console.error(color(`✗ Write error: ${error.message}`, 'red'));
475
- process.exit(1);
476
- });
477
-
478
- } catch (error) {
479
- console.error(color(`✗ Error: ${error.message}`, 'red'));
480
- if (options.debug) {
481
- console.error(error.stack);
482
- }
483
- process.exit(1);
484
- }
485
- }
486
-
487
- async function streamCsvToJson(inputFile, outputFile, options) {
488
- const startTime = Date.now();
489
- let rowCount = 0;
490
-
491
- try {
492
- if (!options.silent) {
493
- console.log(color('Streaming CSV to JSON...', 'dim'));
494
- }
495
-
496
- // Create streams
497
- const readStream = fs.createReadStream(inputFile, 'utf8');
498
- const writeStream = fs.createWriteStream(outputFile, 'utf8');
499
-
500
- // Write JSON array opening bracket
501
- writeStream.write('[\n');
502
-
503
- let buffer = '';
504
- let isFirstRow = true;
505
- let headers = [];
506
-
507
- readStream.on('data', (chunk) => {
508
- buffer += chunk;
509
-
510
- // Process complete lines
511
- const lines = buffer.split('\n');
512
- buffer = lines.pop() || '';
513
-
514
- for (let i = 0; i < lines.length; i++) {
515
- const line = lines[i].trim();
516
- if (!line) continue;
517
-
518
- rowCount++;
519
-
520
- // Parse CSV line
521
- const fields = parseCsvLineSimple(line, options.delimiter || ';');
522
-
523
- // First row might be headers
524
- if (rowCount === 1 && options.hasHeaders !== false) {
525
- headers = fields;
526
- continue;
527
- }
528
-
529
- // Create JSON object
530
- const obj = {};
531
- const fieldCount = Math.min(fields.length, headers.length);
532
-
533
- for (let j = 0; j < fieldCount; j++) {
534
- const header = headers[j] || `column${j + 1}`;
535
- let value = fields[j];
536
-
537
- // Parse numbers if enabled
538
- if (options.parseNumbers && /^-?\d+(\.\d+)?$/.test(value)) {
539
- const num = parseFloat(value);
540
- if (!isNaN(num)) {
541
- value = num;
542
- }
543
- }
544
-
545
- // Parse booleans if enabled
546
- if (options.parseBooleans) {
547
- const lowerValue = value.toLowerCase();
548
- if (lowerValue === 'true') value = true;
549
- if (lowerValue === 'false') value = false;
550
- }
551
-
552
- obj[header] = value;
553
- }
554
-
555
- // Write JSON object
556
- const jsonStr = JSON.stringify(obj);
557
- if (!isFirstRow) {
558
- writeStream.write(',\n');
559
- }
560
- writeStream.write(' ' + jsonStr);
561
- isFirstRow = false;
562
-
563
- // Show progress
564
- if (options.verbose && rowCount % 10000 === 0) {
565
- process.stdout.write(color(` Processed ${rowCount.toLocaleString()} rows\r`, 'dim'));
566
- }
567
- }
568
- });
569
-
570
- readStream.on('end', async () => {
571
- // Process remaining buffer
572
- if (buffer.trim()) {
573
- const fields = parseCsvLineSimple(buffer.trim(), options.delimiter || ';');
574
-
575
- if (fields.length > 0) {
576
- rowCount++;
577
-
578
- // Skip if it's headers
579
- if (!(rowCount === 1 && options.hasHeaders !== false)) {
580
- const obj = {};
581
- const fieldCount = Math.min(fields.length, headers.length);
582
-
583
- for (let j = 0; j < fieldCount; j++) {
584
- const header = headers[j] || `column${j + 1}`;
585
- obj[header] = fields[j];
586
- }
587
-
588
- const jsonStr = JSON.stringify(obj);
589
- if (!isFirstRow) {
590
- writeStream.write(',\n');
591
- }
592
- writeStream.write(' ' + jsonStr);
593
- }
594
- }
595
- }
596
-
597
- // Write JSON array closing bracket
598
- writeStream.write('\n]');
599
- writeStream.end();
600
-
601
- // Wait for write stream to finish
602
- await new Promise(resolve => writeStream.on('finish', resolve));
603
-
604
- const elapsed = Date.now() - startTime;
605
- if (!options.silent) {
606
- console.log(color(`\n✓ Streamed ${(rowCount - (options.hasHeaders !== false ? 1 : 0)).toLocaleString()} rows in ${elapsed}ms`, 'green'));
607
- console.log(color(` Output: ${outputFile}`, 'dim'));
608
- }
609
- });
610
-
611
- readStream.on('error', (error) => {
612
- console.error(color(`✗ Stream error: ${error.message}`, 'red'));
613
- process.exit(1);
614
- });
615
-
616
- writeStream.on('error', (error) => {
617
- console.error(color(`✗ Write error: ${error.message}`, 'red'));
618
- process.exit(1);
619
- });
620
-
621
- } catch (error) {
622
- console.error(color(`✗ Error: ${error.message}`, 'red'));
623
- if (options.debug) {
624
- console.error(error.stack);
625
- }
626
- process.exit(1);
627
- }
628
- }
629
-
630
- // Simple CSV line parser for streaming
631
- function parseCsvLineSimple(line, delimiter) {
632
- const fields = [];
633
- let currentField = '';
634
- let inQuotes = false;
635
-
636
- for (let i = 0; i < line.length; i++) {
637
- const char = line[i];
638
-
639
- if (char === '"') {
640
- if (inQuotes && i + 1 < line.length && line[i + 1] === '"') {
641
- // Escaped quote
642
- currentField += '"';
643
- i++;
644
- } else {
645
- // Toggle quotes
646
- inQuotes = !inQuotes;
647
- }
648
- } else if (char === delimiter && !inQuotes) {
649
- fields.push(currentField);
650
- currentField = '';
651
- } else {
652
- currentField += char;
653
- }
654
- }
655
-
656
- fields.push(currentField);
657
- return fields;
658
- }
659
-
660
- // ============================================================================
661
- // BATCH PROCESSING FUNCTIONS
662
- // ============================================================================
663
-
664
- async function batchJsonToCsv(inputPattern, outputDir, options) {
665
- const startTime = Date.now();
666
-
667
- try {
668
- const glob = require('glob');
669
- const files = glob.sync(inputPattern, {
670
- absolute: true,
671
- nodir: true
672
- });
673
-
674
- if (files.length === 0) {
675
- console.error(color(`✗ No files found matching pattern: ${inputPattern}`, 'red'));
676
- process.exit(1);
677
- }
678
-
679
- if (!options.silent) {
680
- console.log(color(`Found ${files.length} files to process...`, 'dim'));
681
- }
682
-
683
- // Create output directory if it doesn't exist
684
- await fs.promises.mkdir(outputDir, { recursive: true });
685
-
686
- const results = [];
687
- const parallelLimit = options.parallel || 4;
688
-
689
- // Process files in parallel batches
690
- for (let i = 0; i < files.length; i += parallelLimit) {
691
- const batch = files.slice(i, i + parallelLimit);
692
- const promises = batch.map(async (file) => {
693
- const fileName = path.basename(file, '.json');
694
- const outputFile = path.join(outputDir, `${fileName}.csv`);
695
-
696
- if (!options.silent && options.verbose) {
697
- console.log(color(` Processing: ${file}`, 'dim'));
698
- }
699
-
700
- try {
701
- const result = await convertJsonToCsv(file, outputFile, {
702
- ...options,
703
- silent: true // Suppress individual file output
704
- });
705
-
706
- if (!options.silent) {
707
- console.log(color(` ✓ ${fileName}.json → ${fileName}.csv (${result.records} records)`, 'green'));
708
- }
709
-
710
- return { file, success: true, ...result };
711
- } catch (error) {
712
- if (!options.silent) {
713
- console.log(color(` ✗ ${fileName}.json: ${error.message}`, 'red'));
714
- }
715
- return { file, success: false, error: error.message };
716
- }
717
- });
718
-
719
- const batchResults = await Promise.all(promises);
720
- results.push(...batchResults);
721
-
722
- if (!options.silent) {
723
- const processed = i + batch.length;
724
- const percent = Math.round((processed / files.length) * 100);
725
- console.log(color(` Progress: ${processed}/${files.length} (${percent}%)`, 'dim'));
726
- }
727
- }
728
-
729
- const elapsed = Date.now() - startTime;
730
- const successful = results.filter(r => r.success).length;
731
- const totalRecords = results.filter(r => r.success).reduce((sum, r) => sum + (r.records || 0), 0);
732
-
733
- if (!options.silent) {
734
- console.log(color(`\n✓ Batch processing completed in ${elapsed}ms`, 'green'));
735
- console.log(color(` Successful: ${successful}/${files.length} files`, 'dim'));
736
- console.log(color(` Total records: ${totalRecords.toLocaleString()}`, 'dim'));
737
- console.log(color(` Output directory: ${outputDir}`, 'dim'));
738
- }
739
-
740
- return {
741
- totalFiles: files.length,
742
- successful,
743
- totalRecords,
744
- time: elapsed,
745
- results
746
- };
747
-
748
- } catch (error) {
749
- console.error(color(`✗ Batch processing error: ${error.message}`, 'red'));
750
- if (options.debug) {
751
- console.error(error.stack);
752
- }
753
- process.exit(1);
754
- }
755
- }
756
-
757
- async function batchCsvToJson(inputPattern, outputDir, options) {
758
- const startTime = Date.now();
759
-
760
- try {
761
- const glob = require('glob');
762
- const files = glob.sync(inputPattern, {
763
- absolute: true,
764
- nodir: true
765
- });
766
-
767
- if (files.length === 0) {
768
- console.error(color(`✗ No files found matching pattern: ${inputPattern}`, 'red'));
769
- process.exit(1);
770
- }
771
-
772
- if (!options.silent) {
773
- console.log(color(`Found ${files.length} files to process...`, 'dim'));
774
- }
775
-
776
- // Create output directory if it doesn't exist
777
- await fs.promises.mkdir(outputDir, { recursive: true });
778
-
779
- const results = [];
780
- const parallelLimit = options.parallel || 4;
781
-
782
- // Process files in parallel batches
783
- for (let i = 0; i < files.length; i += parallelLimit) {
784
- const batch = files.slice(i, i + parallelLimit);
785
- const promises = batch.map(async (file) => {
786
- const fileName = path.basename(file, '.csv');
787
- const outputFile = path.join(outputDir, `${fileName}.json`);
788
-
789
- if (!options.silent && options.verbose) {
790
- console.log(color(` Processing: ${file}`, 'dim'));
791
- }
792
-
793
- try {
794
- const result = await convertCsvToJson(file, outputFile, {
795
- ...options,
796
- silent: true // Suppress individual file output
797
- });
798
-
799
- if (!options.silent) {
800
- console.log(color(` ✓ ${fileName}.csv → ${fileName}.json (${result.rows} rows)`, 'green'));
801
- }
802
-
803
- return { file, success: true, ...result };
804
- } catch (error) {
805
- if (!options.silent) {
806
- console.log(color(` ✗ ${fileName}.csv: ${error.message}`, 'red'));
807
- }
808
- return { file, success: false, error: error.message };
809
- }
810
- });
811
-
812
- const batchResults = await Promise.all(promises);
813
- results.push(...batchResults);
814
-
815
- if (!options.silent) {
816
- const processed = i + batch.length;
817
- const percent = Math.round((processed / files.length) * 100);
818
- console.log(color(` Progress: ${processed}/${files.length} (${percent}%)`, 'dim'));
819
- }
820
- }
821
-
822
- const elapsed = Date.now() - startTime;
823
- const successful = results.filter(r => r.success).length;
824
- const totalRows = results.filter(r => r.success).reduce((sum, r) => sum + (r.rows || 0), 0);
825
-
826
- if (!options.silent) {
827
- console.log(color(`\n✓ Batch processing completed in ${elapsed}ms`, 'green'));
828
- console.log(color(` Successful: ${successful}/${files.length} files`, 'dim'));
829
- console.log(color(` Total rows: ${totalRows.toLocaleString()}`, 'dim'));
830
- console.log(color(` Output directory: ${outputDir}`, 'dim'));
831
- }
832
-
833
- return {
834
- totalFiles: files.length,
835
- successful,
836
- totalRows,
837
- time: elapsed,
838
- results
839
- };
840
-
841
- } catch (error) {
842
- console.error(color(`✗ Batch processing error: ${error.message}`, 'red'));
843
- if (options.debug) {
844
- console.error(error.stack);
845
- }
846
- process.exit(1);
847
- }
848
- }
849
-
850
- // ============================================================================
851
- // OPTIONS PARSING
852
- // ============================================================================
853
-
854
- function parseOptions(args) {
855
- const options = {
856
- delimiter: ';',
857
- autoDetect: true,
858
- candidates: [';', ',', '\t', '|'],
859
- hasHeaders: true,
860
- includeHeaders: true,
861
- renameMap: undefined,
862
- template: undefined,
863
- trim: true,
864
- parseNumbers: false,
865
- parseBooleans: false,
866
- useFastPath: true,
867
- fastPathMode: 'objects',
868
- preventCsvInjection: true,
869
- rfc4180Compliant: true,
870
- maxRecords: undefined,
871
- maxRows: undefined,
872
- maxDepth: 5,
873
- pretty: false,
874
- silent: false,
875
- verbose: false,
876
- debug: false,
877
- dryRun: false,
878
- addBOM: false,
879
- unwrapArrays: false,
880
- stringifyObjects: false,
881
- recursive: false,
882
- pattern: '**/*',
883
- outputDir: './output',
884
- overwrite: false,
885
- parallel: 4,
886
- chunkSize: 65536,
887
- bufferSize: 1000,
888
- schema: undefined,
889
- transform: undefined
890
- };
891
-
892
- const files = [];
893
-
894
- for (let i = 0; i < args.length; i++) {
895
- const arg = args[i];
896
-
897
- if (arg.startsWith('--')) {
898
- const [key, value] = arg.slice(2).split('=');
899
-
900
- switch (key) {
901
- case 'delimiter':
902
- options.delimiter = value || ',';
903
- options.autoDetect = false;
904
- break;
905
- case 'auto-detect':
906
- options.autoDetect = value !== 'false';
907
- break;
908
- case 'candidates':
909
- options.candidates = value ? value.split(',') : [';', ',', '\t', '|'];
910
- break;
911
- case 'no-headers':
912
- options.includeHeaders = false;
913
- options.hasHeaders = false;
914
- break;
915
- case 'parse-numbers':
916
- options.parseNumbers = true;
917
- break;
918
- case 'parse-booleans':
919
- options.parseBooleans = true;
920
- break;
921
- case 'no-trim':
922
- options.trim = false;
923
- break;
924
- case 'no-fast-path':
925
- options.useFastPath = false;
926
- break;
927
- case 'fast-path':
928
- options.useFastPath = value !== 'false';
929
- break;
930
- case 'fast-path-mode':
931
- options.fastPathMode = value || 'objects';
932
- if (options.fastPathMode !== 'objects' && options.fastPathMode !== 'compact') {
933
- throw new Error('Invalid --fast-path-mode value (objects|compact)');
934
- }
935
- break;
936
- case 'rename':
937
- try {
938
- const jsonStr = value || '{}';
939
- const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
940
- options.renameMap = JSON.parse(cleanStr);
941
- } catch (e) {
942
- throw new Error(`Invalid JSON in --rename option: ${e.message}`);
943
- }
944
- break;
945
- case 'template':
946
- try {
947
- const jsonStr = value || '{}';
948
- const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
949
- options.template = JSON.parse(cleanStr);
950
- } catch (e) {
951
- throw new Error(`Invalid JSON in --template option: ${e.message}`);
952
- }
953
- break;
954
- case 'no-injection-protection':
955
- options.preventCsvInjection = false;
956
- break;
957
- case 'no-rfc4180':
958
- options.rfc4180Compliant = false;
959
- break;
960
- case 'max-records':
961
- options.maxRecords = parseInt(value, 10);
962
- break;
963
- case 'max-rows':
964
- options.maxRows = parseInt(value, 10);
965
- break;
966
- case 'max-depth':
967
- options.maxDepth = parseInt(value, 10) || 5;
968
- break;
969
- case 'pretty':
970
- options.pretty = true;
971
- break;
972
- case 'silent':
973
- options.silent = true;
974
- break;
975
- case 'verbose':
976
- options.verbose = true;
977
- break;
978
- case 'debug':
979
- options.debug = true;
980
- break;
981
- case 'dry-run':
982
- options.dryRun = true;
983
- break;
984
- case 'add-bom':
985
- options.addBOM = true;
986
- break;
987
- case 'unwrap-arrays':
988
- options.unwrapArrays = true;
989
- break;
990
- case 'stringify-objects':
991
- options.stringifyObjects = true;
992
- break;
993
- case 'recursive':
994
- options.recursive = true;
995
- break;
996
- case 'pattern':
997
- options.pattern = value || '**/*';
998
- break;
999
- case 'output-dir':
1000
- options.outputDir = value || './output';
1001
- break;
1002
- case 'overwrite':
1003
- options.overwrite = true;
1004
- break;
1005
- case 'parallel':
1006
- options.parallel = parseInt(value, 10) || 4;
1007
- break;
1008
- case 'chunk-size':
1009
- options.chunkSize = parseInt(value, 10) || 65536;
1010
- break;
1011
- case 'buffer-size':
1012
- options.bufferSize = parseInt(value, 10) || 1000;
1013
- break;
1014
- case 'schema':
1015
- try {
1016
- const jsonStr = value || '{}';
1017
- const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
1018
- options.schema = JSON.parse(cleanStr);
1019
- } catch (e) {
1020
- throw new Error(`Invalid JSON in --schema option: ${e.message}`);
1021
- }
1022
- break;
1023
- case 'transform':
1024
- options.transform = value;
1025
- break;
1026
- }
1027
- } else if (!arg.startsWith('-')) {
1028
- files.push(arg);
1029
- }
1030
- }
1031
-
1032
- return { options, files };
1033
- }
1034
-
1035
- // ============================================================================
1036
- // TUI LAUNCHER
1037
- // ============================================================================
1038
-
1039
- async function launchTUI() {
1040
- try {
1041
- console.log(color('Launching Terminal User Interface...', 'cyan'));
1042
- console.log(color('Press Ctrl+Q to exit', 'dim'));
1043
-
1044
- const JtcsvTUI = require('@jtcsv/tui');
1045
- const tui = new JtcsvTUI();
1046
- tui.start();
1047
- } catch (error) {
1048
- if (error.code === 'MODULE_NOT_FOUND') {
1049
- console.error(color('Error: @jtcsv/tui is not installed', 'red'));
1050
- console.log(color('Install it with:', 'dim'));
1051
- console.log(color(' npm install @jtcsv/tui', 'cyan'));
1052
- console.log(color('\nOr use the CLI interface instead:', 'dim'));
1053
- console.log(color(' jtcsv help', 'cyan'));
1054
- } else {
1055
- console.error(color(`Error: ${error.message}`, 'red'));
1056
- }
1057
- process.exit(1);
1058
- }
1059
- }
1060
-
1061
- async function startBasicTUI() {
1062
- const readline = require('readline');
1063
-
1064
- const rl = readline.createInterface({
1065
- input: process.stdin,
1066
- output: process.stdout
1067
- });
1068
-
1069
- console.clear();
1070
- console.log(color('╔══════════════════════════════════════╗', 'cyan'));
1071
- console.log(color('║ JTCSV Terminal Interface ║', 'cyan'));
1072
- console.log(color('╚══════════════════════════════════════╝', 'cyan'));
1073
- console.log();
1074
- console.log(color('Select operation:', 'bright'));
1075
- console.log(' 1. JSON CSV');
1076
- console.log(' 2. CSV → JSON');
1077
- console.log(' 3. Preprocess JSON');
1078
- console.log(' 4. Batch Processing');
1079
- console.log(' 5. Exit');
1080
- console.log();
1081
-
1082
- rl.question(color('Enter choice (1-5): ', 'cyan'), async (choice) => {
1083
- switch (choice) {
1084
- case '1':
1085
- await runJsonToCsvTUI(rl);
1086
- break;
1087
- case '2':
1088
- await runCsvToJsonTUI(rl);
1089
- break;
1090
- case '3':
1091
- console.log(color('Preprocess feature coming soon...', 'yellow'));
1092
- rl.close();
1093
- break;
1094
- case '4':
1095
- console.log(color('Batch processing coming soon...', 'yellow'));
1096
- rl.close();
1097
- break;
1098
- case '5':
1099
- console.log(color('Goodbye!', 'green'));
1100
- rl.close();
1101
- process.exit(0);
1102
- break;
1103
- default:
1104
- console.log(color('Invalid choice', 'red'));
1105
- rl.close();
1106
- process.exit(1);
1107
- }
1108
- });
1109
- }
1110
-
1111
- async function runJsonToCsvTUI(rl) {
1112
- console.clear();
1113
- console.log(color('JSON → CSV Conversion', 'cyan'));
1114
- console.log();
1115
-
1116
- rl.question('Input JSON file: ', (inputFile) => {
1117
- rl.question('Output CSV file: ', async (outputFile) => {
1118
- rl.question('Delimiter (default: ;): ', async (delimiter) => {
1119
- try {
1120
- console.log(color('\nConverting...', 'dim'));
1121
-
1122
- const result = await convertJsonToCsv(inputFile, outputFile, {
1123
- delimiter: delimiter || ';',
1124
- silent: false
1125
- });
1126
-
1127
- console.log(color('\n✓ Conversion complete!', 'green'));
1128
- rl.question('\nPress Enter to continue...', () => {
1129
- rl.close();
1130
- startBasicTUI();
1131
- });
1132
- } catch (error) {
1133
- console.error(color(`✗ Error: ${error.message}`, 'red'));
1134
- rl.close();
1135
- process.exit(1);
1136
- }
1137
- });
1138
- });
1139
- });
1140
- }
1141
-
1142
- async function runCsvToJsonTUI(rl) {
1143
- console.clear();
1144
- console.log(color('CSV → JSON Conversion', 'cyan'));
1145
- console.log();
1146
-
1147
- rl.question('Input CSV file: ', (inputFile) => {
1148
- rl.question('Output JSON file: ', async (outputFile) => {
1149
- rl.question('Delimiter (default: ;): ', async (delimiter) => {
1150
- rl.question('Pretty print? (y/n): ', async (pretty) => {
1151
- try {
1152
- console.log(color('\nConverting...', 'dim'));
1153
-
1154
- const result = await convertCsvToJson(inputFile, outputFile, {
1155
- delimiter: delimiter || ';',
1156
- pretty: pretty.toLowerCase() === 'y',
1157
- silent: false
1158
- });
1159
-
1160
- console.log(color('\n✓ Conversion complete!', 'green'));
1161
- rl.question('\nPress Enter to continue...', () => {
1162
- rl.close();
1163
- startBasicTUI();
1164
- });
1165
- } catch (error) {
1166
- console.error(color(`✗ Error: ${error.message}`, 'red'));
1167
- rl.close();
1168
- process.exit(1);
1169
- }
1170
- });
1171
- });
1172
- });
1173
- });
1174
- }
1175
-
1176
- // ============================================================================
1177
- // MAIN FUNCTION
1178
- // ============================================================================
1179
-
1180
- async function main() {
1181
- const args = process.argv.slice(2);
1182
-
1183
- if (args.length === 0) {
1184
- showHelp();
1185
- return;
1186
- }
1187
-
1188
- const command = args[0].toLowerCase();
1189
- const { options, files } = parseOptions(args.slice(1));
1190
-
1191
- // Handle dry run
1192
- if (options.dryRun) {
1193
- console.log(color('DRY RUN - No files will be modified', 'yellow'));
1194
- console.log(`Command: ${command}`);
1195
- console.log(`Files: ${files.join(', ')}`);
1196
- console.log(`Options:`, options);
1197
- return;
1198
- }
1199
-
1200
- // Suppress output if silent mode
1201
- if (options.silent) {
1202
- console.log = () => {};
1203
- console.info = () => {};
1204
- }
1205
-
1206
- switch (command) {
1207
- // Main conversion commands
1208
- case 'json-to-csv':
1209
- case 'json2csv':
1210
- if (files.length < 2) {
1211
- console.error(color('Error: Input and output files required', 'red'));
1212
- console.log(color('Usage: jtcsv json-to-csv input.json output.csv', 'cyan'));
1213
- process.exit(1);
1214
- }
1215
- await convertJsonToCsv(files[0], files[1], options);
1216
- break;
1217
-
1218
- case 'csv-to-json':
1219
- case 'csv2json':
1220
- if (files.length < 2) {
1221
- console.error(color('Error: Input and output files required', 'red'));
1222
- console.log(color('Usage: jtcsv csv-to-json input.csv output.json', 'cyan'));
1223
- process.exit(1);
1224
- }
1225
- await convertCsvToJson(files[0], files[1], options);
1226
- break;
1227
-
1228
- case 'save-json':
1229
- if (files.length < 2) {
1230
- console.error(color('Error: Input and output files required', 'red'));
1231
- console.log(color('Usage: jtcsv save-json input.json output.json', 'cyan'));
1232
- process.exit(1);
1233
- }
1234
- await saveAsJson(files[0], files[1], options);
1235
- break;
1236
-
1237
- case 'preprocess':
1238
- if (files.length < 2) {
1239
- console.error(color('Error: Input and output files required', 'red'));
1240
- console.log(color('Usage: jtcsv preprocess input.json output.json', 'cyan'));
1241
- process.exit(1);
1242
- }
1243
- await preprocessJson(files[0], files[1], options);
1244
- break;
1245
-
1246
- // Streaming commands
1247
- case 'stream':
1248
- if (args.length < 2) {
1249
- console.error(color('Error: Streaming mode requires subcommand', 'red'));
1250
- console.log(color('Usage: jtcsv stream [json-to-csv|csv-to-json|file-to-csv|file-to-json]', 'cyan'));
1251
- process.exit(1);
1252
- }
1253
-
1254
- const streamCommand = args[1].toLowerCase();
1255
- if (streamCommand === 'json-to-csv' && files.length >= 2) {
1256
- await streamJsonToCsv(files[0], files[1], options);
1257
- } else if (streamCommand === 'csv-to-json' && files.length >= 2) {
1258
- await streamCsvToJson(files[0], files[1], options);
1259
- } else if (streamCommand === 'file-to-csv' && files.length >= 2) {
1260
- // Use jtcsv streaming API if available
1261
- try {
1262
- const readStream = fs.createReadStream(files[0], 'utf8');
1263
- const writeStream = fs.createWriteStream(files[1], 'utf8');
1264
-
1265
- if (options.addBOM) {
1266
- writeStream.write('\uFEFF');
1267
- }
1268
-
1269
- const transformStream = jtcsv.createJsonToCsvStream(options);
1270
- await pipeline(readStream, transformStream, writeStream);
1271
-
1272
- console.log(color('✓ File streamed successfully', 'green'));
1273
- } catch (error) {
1274
- console.error(color(`✗ Streaming error: ${error.message}`, 'red'));
1275
- process.exit(1);
1276
- }
1277
- } else if (streamCommand === 'file-to-json' && files.length >= 2) {
1278
- // Use jtcsv streaming API if available
1279
- try {
1280
- const readStream = fs.createReadStream(files[0], 'utf8');
1281
- const writeStream = fs.createWriteStream(files[1], 'utf8');
1282
-
1283
- const transformStream = jtcsv.createCsvToJsonStream(options);
1284
- await pipeline(readStream, transformStream, writeStream);
1285
-
1286
- console.log(color('✓ File streamed successfully', 'green'));
1287
- } catch (error) {
1288
- console.error(color(`✗ Streaming error: ${error.message}`, 'red'));
1289
- process.exit(1);
1290
- }
1291
- } else {
1292
- console.error(color('Error: Invalid streaming command or missing files', 'red'));
1293
- process.exit(1);
1294
- }
1295
- break;
1296
-
1297
- // Batch processing commands
1298
- case 'batch':
1299
- if (args.length < 2) {
1300
- console.error(color('Error: Batch mode requires subcommand', 'red'));
1301
- console.log(color('Usage: jtcsv batch [json-to-csv|csv-to-json|process]', 'cyan'));
1302
- process.exit(1);
1303
- }
1304
-
1305
- const batchCommand = args[1].toLowerCase();
1306
- if (batchCommand === 'json-to-csv' && files.length >= 2) {
1307
- await batchJsonToCsv(files[0], files[1], options);
1308
- } else if (batchCommand === 'csv-to-json' && files.length >= 2) {
1309
- await batchCsvToJson(files[0], files[1], options);
1310
- } else if (batchCommand === 'process' && files.length >= 2) {
1311
- console.log(color('Mixed batch processing coming soon...', 'yellow'));
1312
- // TODO: Implement mixed batch processing
1313
- } else {
1314
- console.error(color('Error: Invalid batch command or missing files', 'red'));
1315
- process.exit(1);
1316
- }
1317
- break;
1318
-
1319
- // TUI command
1320
- case 'tui':
1321
- await launchTUI();
1322
- break;
1323
-
1324
- // Help and version
1325
- case 'help':
1326
- case '--help':
1327
- case '-h':
1328
- showHelp();
1329
- break;
1330
-
1331
- case 'version':
1332
- case '-v':
1333
- case '--version':
1334
- showVersion();
1335
- break;
1336
-
1337
- default:
1338
- console.error(color(`Error: Unknown command '${command}'`, 'red'));
1339
- console.log(color('Use jtcsv help for available commands', 'cyan'));
1340
- process.exit(1);
1341
- }
1342
- }
1343
-
1344
- // ============================================================================
1345
- // ERROR HANDLING
1346
- // ============================================================================
1347
-
1348
- process.on('uncaughtException', (error) => {
1349
- console.error(color(`\n✗ Uncaught error: ${error.message}`, 'red'));
1350
- if (process.env.DEBUG) {
1351
- console.error(error.stack);
1352
- }
1353
- process.exit(1);
1354
- });
1355
-
1356
- process.on('unhandledRejection', (error) => {
1357
- console.error(color(`\n✗ Unhandled promise rejection: ${error.message}`, 'red'));
1358
- if (process.env.DEBUG) {
1359
- console.error(error.stack);
1360
- }
1361
- process.exit(1);
1362
- });
1363
-
1364
- // Run main function
1365
- if (require.main === module) {
1366
- main().catch((error) => {
1367
- console.error(color(`\n✗ Fatal error: ${error.message}`, 'red'));
1368
- process.exit(1);
1369
- });
1370
- }
1371
-
1372
- module.exports = { main };
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * jtcsv CLI - Complete Command Line Interface
5
+ *
6
+ * Full-featured command-line interface for JSON↔CSV conversion
7
+ * with streaming, batch processing, and all security features.
8
+ */
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const { pipeline } = require("stream/promises");
13
+ const jtcsv = require("../index.js");
14
+ const transformLoader = require("../src/utils/transform-loader");
15
+ const schemaValidator = require("../src/utils/schema-validator");
16
+
17
+ const VERSION = require("../package.json").version;
18
+
19
+ // Function to apply transform from JavaScript file
20
+ async function applyTransform(data, transformFile) {
21
+ try {
22
+ const transformPath = path.resolve(process.cwd(), transformFile);
23
+ const transformModule = require(transformPath);
24
+
25
+ // Check if module exports a function
26
+ if (typeof transformModule === "function") {
27
+ return transformModule(data);
28
+ } else if (typeof transformModule.default === "function") {
29
+ return transformModule.default(data);
30
+ } else if (typeof transformModule.transform === "function") {
31
+ return transformModule.transform(data);
32
+ } else {
33
+ throw new Error(
34
+ `Transform file must export a function. Found: ${typeof transformModule}`,
35
+ );
36
+ }
37
+ } catch (error) {
38
+ throw new Error(
39
+ `Failed to apply transform from ${transformFile}: ${error.message}`,
40
+ );
41
+ }
42
+ }
43
+
44
+ // ANSI colors for terminal output
45
+ const colors = {
46
+ reset: "\x1b[0m",
47
+ bright: "\x1b[1m",
48
+ dim: "\x1b[2m",
49
+ red: "\x1b[31m",
50
+ green: "\x1b[32m",
51
+ yellow: "\x1b[33m",
52
+ blue: "\x1b[34m",
53
+ magenta: "\x1b[35m",
54
+ cyan: "\x1b[36m",
55
+ white: "\x1b[37m",
56
+ };
57
+
58
+ function color(text, colorName) {
59
+ return colors[colorName] + text + colors.reset;
60
+ }
61
+
62
+ function showHelp() {
63
+ console.log(`
64
+ ${color("jtcsv CLI v" + VERSION, "cyan")}
65
+ ${color("The Complete JSON↔CSV Converter for Node.js", "dim")}
66
+
67
+ ${color("USAGE:", "bright")}
68
+ jtcsv [command] [options] [file...]
69
+
70
+ ${color("MAIN COMMANDS:", "bright")}
71
+ ${color("json-to-csv", "green")} Convert JSON to CSV (alias: json2csv)
72
+ ${color("csv-to-json", "green")} Convert CSV to JSON (alias: csv2json)
73
+ ${color("ndjson-to-csv", "green")} Convert NDJSON to CSV
74
+ ${color("csv-to-ndjson", "green")} Convert CSV to NDJSON
75
+ ${color("ndjson-to-json", "green")} Convert NDJSON to JSON array
76
+ ${color("json-to-ndjson", "green")} Convert JSON array to NDJSON
77
+ ${color("save-json", "yellow")} Save data as JSON file
78
+ ${color("save-csv", "yellow")} Save data as CSV file
79
+ ${color("stream", "yellow")} Streaming conversion for large files
80
+ ${color("batch", "yellow")} Batch process multiple files
81
+ ${color("preprocess", "magenta")} Preprocess JSON with deep unwrapping
82
+ ${color("unwrap", "magenta")} Flatten nested JSON structures (alias: flatten)
83
+ ${color("tui", "magenta")} Launch Terminal User Interface (@jtcsv/tui)
84
+ ${color("web", "magenta")} Launch Web Interface (http://localhost:3000)
85
+ ${color("help", "blue")} Show this help message
86
+ ${color("version", "blue")} Show version information
87
+
88
+ ${color("STREAMING SUBCOMMANDS:", "bright")}
89
+ ${color("stream json-to-csv", "dim")} Stream JSON to CSV
90
+ ${color("stream csv-to-json", "dim")} Stream CSV to JSON
91
+ ${color("stream file-to-csv", "dim")} Stream file to CSV
92
+ ${color("stream file-to-json", "dim")} Stream file to JSON
93
+
94
+ ${color("BATCH SUBCOMMANDS:", "bright")}
95
+ ${color("batch json-to-csv", "dim")} Batch convert JSON files to CSV
96
+ ${color("batch csv-to-json", "dim")} Batch convert CSV files to JSON
97
+ ${color("batch process", "dim")} Process mixed file types
98
+
99
+ ${color("EXAMPLES:", "bright")}
100
+ ${color("Convert JSON file to CSV:", "dim")}
101
+ jtcsv json-to-csv input.json output.csv --delimiter=,
102
+
103
+ ${color("Convert CSV file to JSON:", "dim")}
104
+ jtcsv csv-to-json input.csv output.json --parse-numbers --auto-detect
105
+
106
+ ${color("Save data as JSON file:", "dim")}
107
+ jtcsv save-json data.json output.json --pretty
108
+
109
+ ${color("Save data as CSV file:", "dim")}
110
+ jtcsv save-csv data.csv output.csv --delimiter=, --transform=transform.js
111
+
112
+ ${color("Stream large JSON file to CSV:", "dim")}
113
+ jtcsv stream json-to-csv large.json output.csv --max-records=1000000
114
+
115
+ ${color("Stream CSV file to JSON:", "dim")}
116
+ jtcsv stream csv-to-json large.csv output.json --max-rows=500000
117
+
118
+ ${color("Preprocess complex JSON:", "dim")}
119
+ jtcsv preprocess complex.json simplified.json --max-depth=3
120
+
121
+ ${color("Batch convert JSON files:", "dim")}
122
+ jtcsv batch json-to-csv "data/*.json" "output/" --delimiter=;
123
+
124
+ ${color("Launch TUI interface:", "dim")}
125
+ jtcsv tui
126
+
127
+ ${color("Launch Web interface:", "dim")}
128
+ jtcsv web --port=3000
129
+
130
+ ${color("CONVERSION OPTIONS:", "bright")}
131
+ ${color("--delimiter=", "cyan")}CHAR CSV delimiter (default: ;)
132
+ ${color("--auto-detect", "cyan")} Auto-detect delimiter (default: true)
133
+ ${color("--candidates=", "cyan")}LIST Delimiter candidates (default: ;,\t|)
134
+ ${color("--no-headers", "cyan")} Exclude headers from CSV output
135
+ ${color("--parse-numbers", "cyan")} Parse numeric values in CSV
136
+ ${color("--parse-booleans", "cyan")} Parse boolean values in CSV
137
+ ${color("--no-trim", "cyan")} Don't trim whitespace from CSV values
138
+ ${color("--no-fast-path", "cyan")} Disable fast-path parser (force quote-aware)
139
+ ${color("--fast-path-mode=", "cyan")}MODE Fast path output mode (objects|compact)
140
+ ${color("--rename=", "cyan")}JSON Rename columns (JSON map)
141
+ ${color("--template=", "cyan")}JSON Column order template (JSON object)
142
+ ${color("--no-injection-protection", "cyan")} Disable CSV injection protection
143
+ ${color("--no-rfc4180", "cyan")} Disable RFC 4180 compliance
144
+ ${color("--max-records=", "cyan")}N Maximum records to process
145
+ ${color("--max-rows=", "cyan")}N Maximum rows to process
146
+ ${color("--pretty", "cyan")} Pretty print JSON output
147
+ ${color("--schema=", "cyan")}JSON JSON schema for validation and formatting
148
+ ${color("--transform=", "cyan")}JS Custom transform function (JavaScript file)
149
+ ${color("PREPROCESS OPTIONS:", "bright")}
150
+ ${color("--max-depth=", "cyan")}N Maximum recursion depth (default: 5)
151
+ ${color("--unwrap-arrays", "cyan")} Unwrap arrays to strings
152
+ ${color("--stringify-objects", "cyan")} Stringify complex objects
153
+ ${color("STREAMING OPTIONS:", "bright")}
154
+ ${color("--chunk-size=", "cyan")}N Chunk size in bytes (default: 65536)
155
+ ${color("--buffer-size=", "cyan")}N Buffer size in records (default: 1000)
156
+ ${color("--add-bom", "cyan")} Add UTF-8 BOM for Excel compatibility
157
+ ${color("BATCH OPTIONS:", "bright")}
158
+ ${color("--recursive", "cyan")} Process directories recursively
159
+ ${color("--pattern=", "cyan")}GLOB File pattern to match
160
+ ${color("--output-dir=", "cyan")}DIR Output directory for batch processing
161
+ ${color("--overwrite", "cyan")} Overwrite existing files
162
+ ${color("--parallel=", "cyan")}N Parallel processing limit (default: 4)
163
+ ${color("GENERAL OPTIONS:", "bright")}
164
+ ${color("--silent", "cyan")} Suppress all output except errors
165
+ ${color("--verbose", "cyan")} Show detailed progress information
166
+ ${color("--debug", "cyan")} Show debug information
167
+ ${color("--dry-run", "cyan")} Show what would be done without actually doing it
168
+ ${color("SECURITY FEATURES:", "bright")}
169
+ CSV injection protection (enabled by default)
170
+ • Path traversal protection
171
+ Input validation and sanitization
172
+ Size limits to prevent DoS attacks
173
+ • Schema validation support
174
+
175
+ ${color("PERFORMANCE FEATURES:", "bright")}
176
+ Streaming for files >100MB
177
+ • Batch processing with parallel execution
178
+ • Memory-efficient preprocessing
179
+ Configurable buffer sizes
180
+
181
+ ${color("LEARN MORE:", "dim")}
182
+ GitHub: https://github.com/Linol-Hamelton/jtcsv
183
+ Issues: https://github.com/Linol-Hamelton/jtcsv/issues
184
+ Documentation: https://github.com/Linol-Hamelton/jtcsv#readme
185
+ `);
186
+ }
187
+
188
+ function showVersion() {
189
+ console.log(`jtcsv v${VERSION}`);
190
+ console.log(`Node.js ${process.version}`);
191
+ console.log(`Platform: ${process.platform} ${process.arch}`);
192
+ }
193
+
194
+ // ============================================================================
195
+ // CONVERSION FUNCTIONS
196
+ // ============================================================================
197
+
198
+ async function convertJsonToCsv(inputFile, outputFile, options) {
199
+ const startTime = Date.now();
200
+
201
+ try {
202
+ // Read input file
203
+ const inputData = await fs.promises.readFile(inputFile, "utf8");
204
+ const jsonData = JSON.parse(inputData);
205
+
206
+ if (!Array.isArray(jsonData)) {
207
+ throw new Error("JSON data must be an array of objects");
208
+ }
209
+
210
+ if (!options.silent) {
211
+ console.log(
212
+ color(
213
+ `Converting ${jsonData.length.toLocaleString()} records...`,
214
+ "dim",
215
+ ),
216
+ );
217
+ }
218
+
219
+ if (options.transform) {
220
+ if (!options.silent) {
221
+ console.log(
222
+ color(`Applying transform from: ${options.transform}`, "dim"),
223
+ );
224
+ }
225
+ try {
226
+ transformedData = transformLoader.applyTransform(
227
+ jsonData,
228
+ options.transform,
229
+ );
230
+ if (!options.silent) {
231
+ console.log(
232
+ color(
233
+ `✓ Transform applied to ${transformedData.length} records`,
234
+ "green",
235
+ ),
236
+ );
237
+ }
238
+ } catch (transformError) {
239
+ console.error(
240
+ color(`✗ Transform error: ${transformError.message}`, "red"),
241
+ );
242
+ if (options.debug) {
243
+ console.error(transformError.stack);
244
+ }
245
+ process.exit(1);
246
+ }
247
+ }
248
+
249
+ // Prepare options for jtcsv
250
+ const jtcsvOptions = {
251
+ delimiter: options.delimiter,
252
+ includeHeaders: options.includeHeaders,
253
+ renameMap: options.renameMap,
254
+ template: options.template,
255
+ maxRecords: options.maxRecords,
256
+ preventCsvInjection: options.preventCsvInjection,
257
+ rfc4180Compliant: options.rfc4180Compliant,
258
+ schema: options.schema, // Add schema option
259
+ };
260
+
261
+ // Apply transform function if provided
262
+ let transformedData = jsonData;
263
+ if (options.transform) {
264
+ transformedData = await applyTransform(jsonData, options.transform);
265
+ }
266
+
267
+ // Convert to CSV
268
+ const csvData = jtcsv.jsonToCsv(transformedData, jtcsvOptions);
269
+
270
+ // Write output file
271
+ await fs.promises.writeFile(outputFile, csvData, "utf8");
272
+
273
+ const elapsed = Date.now() - startTime;
274
+ if (!options.silent) {
275
+ console.log(
276
+ color(
277
+ `✓ Converted ${transformedData.length.toLocaleString()} records in ${elapsed}ms`,
278
+ "green",
279
+ ),
280
+ );
281
+ console.log(
282
+ color(
283
+ ` Output: ${outputFile} (${csvData.length.toLocaleString()} bytes)`,
284
+ "dim",
285
+ ),
286
+ );
287
+ }
288
+
289
+ return {
290
+ records: transformedData.length,
291
+ bytes: csvData.length,
292
+ time: elapsed,
293
+ };
294
+ } catch (error) {
295
+ console.error(color(`✗ Error: ${error.message}`, "red"));
296
+ if (options.debug) {
297
+ console.error(error.stack);
298
+ }
299
+ process.exit(1);
300
+ }
301
+ }
302
+
303
+ async function convertCsvToJson(inputFile, outputFile, options) {
304
+ const startTime = Date.now();
305
+
306
+ try {
307
+ if (!options.silent) {
308
+ console.log(color("Reading CSV file...", "dim"));
309
+ }
310
+
311
+ // Prepare options for jtcsv
312
+ const jtcsvOptions = {
313
+ delimiter: options.delimiter,
314
+ autoDetect: options.autoDetect,
315
+ candidates: options.candidates,
316
+ hasHeaders: options.hasHeaders,
317
+ renameMap: options.renameMap,
318
+ trim: options.trim,
319
+ parseNumbers: options.parseNumbers,
320
+ parseBooleans: options.parseBooleans,
321
+ maxRows: options.maxRows,
322
+ useFastPath: options.useFastPath,
323
+ fastPathMode: options.fastPathMode,
324
+ schema: options.schema, // Add schema option if supported
325
+ };
326
+
327
+ // Read and convert CSV
328
+ const jsonData = await jtcsv.readCsvAsJson(inputFile, jtcsvOptions);
329
+
330
+ // Apply transform if specified
331
+ let transformedData = jsonData;
332
+ if (options.transform) {
333
+ if (!options.silent) {
334
+ console.log(
335
+ color(`Applying transform from: ${options.transform}`, "dim"),
336
+ );
337
+ }
338
+ try {
339
+ transformedData = transformLoader.applyTransform(
340
+ jsonData,
341
+ options.transform,
342
+ );
343
+ if (!options.silent) {
344
+ console.log(
345
+ color(
346
+ `✓ Transform applied to ${transformedData.length} rows`,
347
+ "green",
348
+ ),
349
+ );
350
+ }
351
+ } catch (transformError) {
352
+ console.error(
353
+ color(`✗ Transform error: ${transformError.message}`, "red"),
354
+ );
355
+ if (options.debug) {
356
+ console.error(transformError.stack);
357
+ }
358
+ process.exit(1);
359
+ }
360
+ }
361
+
362
+ // Format JSON
363
+ const jsonOutput = options.pretty
364
+ ? JSON.stringify(transformedData, null, 2)
365
+ : JSON.stringify(transformedData);
366
+
367
+ // Write output file
368
+ await fs.promises.writeFile(outputFile, jsonOutput, "utf8");
369
+
370
+ const elapsed = Date.now() - startTime;
371
+ if (!options.silent) {
372
+ console.log(
373
+ color(
374
+ `✓ Converted ${transformedData.length.toLocaleString()} rows in ${elapsed}ms`,
375
+ "green",
376
+ ),
377
+ );
378
+ console.log(
379
+ color(
380
+ ` Output: ${outputFile} (${jsonOutput.length.toLocaleString()} bytes)`,
381
+ "dim",
382
+ ),
383
+ );
384
+ }
385
+
386
+ return {
387
+ rows: transformedData.length,
388
+ bytes: jsonOutput.length,
389
+ time: elapsed,
390
+ };
391
+ } catch (error) {
392
+ console.error(color(`✗ Error: ${error.message}`, "red"));
393
+ if (options.debug) {
394
+ console.error(error.stack);
395
+ }
396
+ process.exit(1);
397
+ }
398
+ }
399
+
400
+ async function saveAsCsv(inputFile, outputFile, options) {
401
+ const startTime = Date.now();
402
+
403
+ try {
404
+ // Read input file
405
+ const inputData = await fs.promises.readFile(inputFile, "utf8");
406
+
407
+ if (!options.silent) {
408
+ console.log(color(`Saving CSV file...`, "dim"));
409
+ }
410
+
411
+ // Apply transform if specified
412
+ let transformedData = inputData;
413
+ if (options.transform) {
414
+ if (!options.silent) {
415
+ console.log(
416
+ color(`Applying transform from: ${options.transform}`, "dim"),
417
+ );
418
+ }
419
+ try {
420
+ // Для CSV нужно сначала распарсить, применить трансформацию, затем снова сериализовать
421
+ const parsedData = jtcsv.csvToJson(inputData, {
422
+ delimiter: options.delimiter,
423
+ autoDetect: options.autoDetect,
424
+ hasHeaders: options.hasHeaders,
425
+ trim: options.trim,
426
+ parseNumbers: options.parseNumbers,
427
+ parseBooleans: options.parseBooleans,
428
+ });
429
+
430
+ const transformedJson = transformLoader.applyTransform(
431
+ parsedData,
432
+ options.transform,
433
+ );
434
+
435
+ // Конвертировать обратно в CSV
436
+ transformedData = jtcsv.jsonToCsv(transformedJson, {
437
+ delimiter: options.delimiter,
438
+ includeHeaders: options.includeHeaders,
439
+ });
440
+
441
+ if (!options.silent) {
442
+ console.log(color(`✓ Transform applied`, "green"));
443
+ }
444
+ } catch (transformError) {
445
+ console.error(
446
+ color(`✗ Transform error: ${transformError.message}`, "red"),
447
+ );
448
+ if (options.debug) {
449
+ console.error(transformError.stack);
450
+ }
451
+ process.exit(1);
452
+ }
453
+ }
454
+
455
+ // Write output file
456
+ await fs.promises.writeFile(outputFile, transformedData, "utf8");
457
+
458
+ const elapsed = Date.now() - startTime;
459
+ if (!options.silent) {
460
+ console.log(color(`✓ Saved CSV in ${elapsed}ms`, "green"));
461
+ console.log(color(` Output: ${outputFile}`, "dim"));
462
+ }
463
+
464
+ return { time: elapsed };
465
+ } catch (error) {
466
+ console.error(color(`✗ Error: ${error.message}`, "red"));
467
+ if (options.debug) {
468
+ console.error(error.stack);
469
+ }
470
+ process.exit(1);
471
+ }
472
+ }
473
+
474
+ async function saveAsJson(inputFile, outputFile, options) {
475
+ const startTime = Date.now();
476
+
477
+ try {
478
+ // Read input file
479
+ const inputData = await fs.promises.readFile(inputFile, "utf8");
480
+ const jsonData = JSON.parse(inputData);
481
+
482
+ if (!options.silent) {
483
+ console.log(
484
+ color(
485
+ `Saving ${Array.isArray(jsonData) ? jsonData.length.toLocaleString() + " records" : "object"}...`,
486
+ "dim",
487
+ ),
488
+ );
489
+ }
490
+
491
+ // Apply transform if specified
492
+ let transformedData = jsonData;
493
+ if (options.transform) {
494
+ if (!options.silent) {
495
+ console.log(
496
+ color(`Applying transform from: ${options.transform}`, "dim"),
497
+ );
498
+ }
499
+ try {
500
+ transformedData = transformLoader.applyTransform(
501
+ jsonData,
502
+ options.transform,
503
+ );
504
+ if (!options.silent) {
505
+ console.log(color(`✓ Transform applied`, "green"));
506
+ }
507
+ } catch (transformError) {
508
+ console.error(
509
+ color(`✗ Transform error: ${transformError.message}`, "red"),
510
+ );
511
+ if (options.debug) {
512
+ console.error(transformError.stack);
513
+ }
514
+ process.exit(1);
515
+ }
516
+ }
517
+
518
+ // Prepare options for jtcsv
519
+ const jtcsvOptions = {
520
+ prettyPrint: options.pretty,
521
+ };
522
+
523
+ // Save as JSON
524
+ await jtcsv.saveAsJson(transformedData, outputFile, jtcsvOptions);
525
+
526
+ const elapsed = Date.now() - startTime;
527
+ if (!options.silent) {
528
+ console.log(color(`✓ Saved JSON in ${elapsed}ms`, "green"));
529
+ console.log(color(` Output: ${outputFile}`, "dim"));
530
+ }
531
+
532
+ return { time: elapsed };
533
+ } catch (error) {
534
+ console.error(color(`✗ Error: ${error.message}`, "red"));
535
+ if (options.debug) {
536
+ console.error(error.stack);
537
+ }
538
+ process.exit(1);
539
+ }
540
+ }
541
+
542
+ // ============================================================================
543
+ // NDJSON CONVERSION FUNCTIONS
544
+ // ============================================================================
545
+
546
+ async function convertNdjsonToCsv(inputFile, outputFile, options) {
547
+ const startTime = Date.now();
548
+
549
+ try {
550
+ if (!options.silent) {
551
+ console.log(color("Converting NDJSON to CSV...", "dim"));
552
+ }
553
+
554
+ // Read NDJSON file
555
+ const inputData = await fs.promises.readFile(inputFile, "utf8");
556
+ const jsonData = jtcsv.ndjsonToJson(inputData);
557
+
558
+ if (!options.silent) {
559
+ console.log(
560
+ color(`Parsed ${jsonData.length.toLocaleString()} records from NDJSON`, "dim"),
561
+ );
562
+ }
563
+
564
+ // Prepare options for jtcsv
565
+ const jtcsvOptions = {
566
+ delimiter: options.delimiter,
567
+ includeHeaders: options.includeHeaders,
568
+ renameMap: options.renameMap,
569
+ template: options.template,
570
+ preventCsvInjection: options.preventCsvInjection,
571
+ rfc4180Compliant: options.rfc4180Compliant,
572
+ };
573
+
574
+ // Convert to CSV
575
+ const csvData = jtcsv.jsonToCsv(jsonData, jtcsvOptions);
576
+
577
+ // Write output file
578
+ await fs.promises.writeFile(outputFile, csvData, "utf8");
579
+
580
+ const elapsed = Date.now() - startTime;
581
+ if (!options.silent) {
582
+ console.log(
583
+ color(
584
+ `✓ Converted ${jsonData.length.toLocaleString()} records in ${elapsed}ms`,
585
+ "green",
586
+ ),
587
+ );
588
+ console.log(
589
+ color(
590
+ ` Output: ${outputFile} (${csvData.length.toLocaleString()} bytes)`,
591
+ "dim",
592
+ ),
593
+ );
594
+ }
595
+
596
+ return {
597
+ records: jsonData.length,
598
+ bytes: csvData.length,
599
+ time: elapsed,
600
+ };
601
+ } catch (error) {
602
+ console.error(color(`✗ Error: ${error.message}`, "red"));
603
+ if (options.debug) {
604
+ console.error(error.stack);
605
+ }
606
+ process.exit(1);
607
+ }
608
+ }
609
+
610
+ async function convertCsvToNdjson(inputFile, outputFile, options) {
611
+ const startTime = Date.now();
612
+
613
+ try {
614
+ if (!options.silent) {
615
+ console.log(color("Converting CSV to NDJSON...", "dim"));
616
+ }
617
+
618
+ // Prepare options for jtcsv
619
+ const jtcsvOptions = {
620
+ delimiter: options.delimiter,
621
+ autoDetect: options.autoDetect,
622
+ candidates: options.candidates,
623
+ hasHeaders: options.hasHeaders,
624
+ renameMap: options.renameMap,
625
+ trim: options.trim,
626
+ parseNumbers: options.parseNumbers,
627
+ parseBooleans: options.parseBooleans,
628
+ };
629
+
630
+ // Read and convert CSV
631
+ const jsonData = await jtcsv.readCsvAsJson(inputFile, jtcsvOptions);
632
+
633
+ // Convert to NDJSON
634
+ const ndjsonData = jtcsv.jsonToNdjson(jsonData);
635
+
636
+ // Write output file
637
+ await fs.promises.writeFile(outputFile, ndjsonData, "utf8");
638
+
639
+ const elapsed = Date.now() - startTime;
640
+ if (!options.silent) {
641
+ console.log(
642
+ color(
643
+ `✓ Converted ${jsonData.length.toLocaleString()} rows in ${elapsed}ms`,
644
+ "green",
645
+ ),
646
+ );
647
+ console.log(
648
+ color(
649
+ ` Output: ${outputFile} (${ndjsonData.length.toLocaleString()} bytes)`,
650
+ "dim",
651
+ ),
652
+ );
653
+ }
654
+
655
+ return {
656
+ rows: jsonData.length,
657
+ bytes: ndjsonData.length,
658
+ time: elapsed,
659
+ };
660
+ } catch (error) {
661
+ console.error(color(`✗ Error: ${error.message}`, "red"));
662
+ if (options.debug) {
663
+ console.error(error.stack);
664
+ }
665
+ process.exit(1);
666
+ }
667
+ }
668
+
669
+ async function convertNdjsonToJson(inputFile, outputFile, options) {
670
+ const startTime = Date.now();
671
+
672
+ try {
673
+ if (!options.silent) {
674
+ console.log(color("Converting NDJSON to JSON array...", "dim"));
675
+ }
676
+
677
+ // Read NDJSON file
678
+ const inputData = await fs.promises.readFile(inputFile, "utf8");
679
+ const jsonData = jtcsv.ndjsonToJson(inputData);
680
+
681
+ // Format JSON
682
+ const jsonOutput = options.pretty
683
+ ? JSON.stringify(jsonData, null, 2)
684
+ : JSON.stringify(jsonData);
685
+
686
+ // Write output file
687
+ await fs.promises.writeFile(outputFile, jsonOutput, "utf8");
688
+
689
+ const elapsed = Date.now() - startTime;
690
+ if (!options.silent) {
691
+ console.log(
692
+ color(
693
+ `✓ Converted ${jsonData.length.toLocaleString()} records in ${elapsed}ms`,
694
+ "green",
695
+ ),
696
+ );
697
+ console.log(
698
+ color(
699
+ ` Output: ${outputFile} (${jsonOutput.length.toLocaleString()} bytes)`,
700
+ "dim",
701
+ ),
702
+ );
703
+ }
704
+
705
+ return {
706
+ records: jsonData.length,
707
+ bytes: jsonOutput.length,
708
+ time: elapsed,
709
+ };
710
+ } catch (error) {
711
+ console.error(color(`✗ Error: ${error.message}`, "red"));
712
+ if (options.debug) {
713
+ console.error(error.stack);
714
+ }
715
+ process.exit(1);
716
+ }
717
+ }
718
+
719
+ async function convertJsonToNdjson(inputFile, outputFile, options) {
720
+ const startTime = Date.now();
721
+
722
+ try {
723
+ if (!options.silent) {
724
+ console.log(color("Converting JSON array to NDJSON...", "dim"));
725
+ }
726
+
727
+ // Read JSON file
728
+ const inputData = await fs.promises.readFile(inputFile, "utf8");
729
+ const jsonData = JSON.parse(inputData);
730
+
731
+ if (!Array.isArray(jsonData)) {
732
+ throw new Error("JSON data must be an array of objects");
733
+ }
734
+
735
+ // Convert to NDJSON
736
+ const ndjsonData = jtcsv.jsonToNdjson(jsonData);
737
+
738
+ // Write output file
739
+ await fs.promises.writeFile(outputFile, ndjsonData, "utf8");
740
+
741
+ const elapsed = Date.now() - startTime;
742
+ if (!options.silent) {
743
+ console.log(
744
+ color(
745
+ `✓ Converted ${jsonData.length.toLocaleString()} records in ${elapsed}ms`,
746
+ "green",
747
+ ),
748
+ );
749
+ console.log(
750
+ color(
751
+ ` Output: ${outputFile} (${ndjsonData.length.toLocaleString()} bytes)`,
752
+ "dim",
753
+ ),
754
+ );
755
+ }
756
+
757
+ return {
758
+ records: jsonData.length,
759
+ bytes: ndjsonData.length,
760
+ time: elapsed,
761
+ };
762
+ } catch (error) {
763
+ console.error(color(`✗ Error: ${error.message}`, "red"));
764
+ if (options.debug) {
765
+ console.error(error.stack);
766
+ }
767
+ process.exit(1);
768
+ }
769
+ }
770
+
771
+ // ============================================================================
772
+ // UNWRAP/FLATTEN FUNCTION
773
+ // ============================================================================
774
+
775
+ async function unwrapJson(inputFile, outputFile, options) {
776
+ const startTime = Date.now();
777
+
778
+ try {
779
+ if (!options.silent) {
780
+ console.log(color("Unwrapping/flattening nested JSON...", "dim"));
781
+ }
782
+
783
+ // Read JSON file
784
+ const inputData = await fs.promises.readFile(inputFile, "utf8");
785
+ const jsonData = JSON.parse(inputData);
786
+
787
+ const maxDepth = options.maxDepth || 10;
788
+ const separator = options.flattenPrefix || "_";
789
+
790
+ // Flatten function
791
+ function flattenObject(obj, prefix = "", depth = 0) {
792
+ if (depth >= maxDepth) {
793
+ return { [prefix.slice(0, -1)]: JSON.stringify(obj) };
794
+ }
795
+
796
+ const result = {};
797
+
798
+ for (const [key, value] of Object.entries(obj)) {
799
+ const newKey = prefix + key;
800
+
801
+ if (value && typeof value === "object" && !Array.isArray(value)) {
802
+ Object.assign(result, flattenObject(value, newKey + separator, depth + 1));
803
+ } else if (Array.isArray(value)) {
804
+ // Flatten arrays
805
+ if (options.unwrapArrays) {
806
+ result[newKey] = value.join(", ");
807
+ } else {
808
+ result[newKey] = JSON.stringify(value);
809
+ }
810
+ } else {
811
+ result[newKey] = value;
812
+ }
813
+ }
814
+
815
+ return result;
816
+ }
817
+
818
+ let unwrappedData;
819
+ if (Array.isArray(jsonData)) {
820
+ unwrappedData = jsonData.map(item => flattenObject(item));
821
+ if (!options.silent) {
822
+ console.log(
823
+ color(`Processing ${jsonData.length.toLocaleString()} records...`, "dim"),
824
+ );
825
+ }
826
+ } else {
827
+ unwrappedData = flattenObject(jsonData);
828
+ }
829
+
830
+ // Format JSON
831
+ const jsonOutput = options.pretty
832
+ ? JSON.stringify(unwrappedData, null, 2)
833
+ : JSON.stringify(unwrappedData);
834
+
835
+ // Write output file
836
+ await fs.promises.writeFile(outputFile, jsonOutput, "utf8");
837
+
838
+ const elapsed = Date.now() - startTime;
839
+ const recordCount = Array.isArray(unwrappedData) ? unwrappedData.length : 1;
840
+
841
+ if (!options.silent) {
842
+ console.log(
843
+ color(
844
+ `✓ Unwrapped ${recordCount.toLocaleString()} record(s) in ${elapsed}ms`,
845
+ "green",
846
+ ),
847
+ );
848
+ console.log(
849
+ color(
850
+ ` Output: ${outputFile} (${jsonOutput.length.toLocaleString()} bytes)`,
851
+ "dim",
852
+ ),
853
+ );
854
+ }
855
+
856
+ return {
857
+ records: recordCount,
858
+ bytes: jsonOutput.length,
859
+ time: elapsed,
860
+ };
861
+ } catch (error) {
862
+ console.error(color(`✗ Error: ${error.message}`, "red"));
863
+ if (options.debug) {
864
+ console.error(error.stack);
865
+ }
866
+ process.exit(1);
867
+ }
868
+ }
869
+
870
+ async function preprocessJson(inputFile, outputFile, options) {
871
+ const startTime = Date.now();
872
+
873
+ try {
874
+ // Read input file
875
+ const inputData = await fs.promises.readFile(inputFile, "utf8");
876
+ const jsonData = JSON.parse(inputData);
877
+
878
+ if (!Array.isArray(jsonData)) {
879
+ throw new Error(
880
+ "JSON data must be an array of objects for preprocessing",
881
+ );
882
+ }
883
+
884
+ if (!options.silent) {
885
+ console.log(
886
+ color(
887
+ `Preprocessing ${jsonData.length.toLocaleString()} records...`,
888
+ "dim",
889
+ ),
890
+ );
891
+ }
892
+
893
+ // Preprocess data
894
+ const processedData = jtcsv.preprocessData(jsonData);
895
+
896
+ // Apply transform if specified
897
+ let transformedData = processedData;
898
+ if (options.transform) {
899
+ if (!options.silent) {
900
+ console.log(
901
+ color(`Applying transform from: ${options.transform}`, "dim"),
902
+ );
903
+ }
904
+ try {
905
+ transformedData = transformLoader.applyTransform(
906
+ processedData,
907
+ options.transform,
908
+ );
909
+ if (!options.silent) {
910
+ console.log(
911
+ color(
912
+ `✓ Transform applied to ${transformedData.length} records`,
913
+ "green",
914
+ ),
915
+ );
916
+ }
917
+ } catch (transformError) {
918
+ console.error(
919
+ color(`✗ Transform error: ${transformError.message}`, "red"),
920
+ );
921
+ if (options.debug) {
922
+ console.error(transformError.stack);
923
+ }
924
+ process.exit(1);
925
+ }
926
+ }
927
+
928
+ // Apply deep unwrap if needed
929
+ if (options.unwrapArrays || options.stringifyObjects) {
930
+ const maxDepth = options.maxDepth || 5;
931
+ transformedData.forEach((item) => {
932
+ for (const key in item) {
933
+ if (item[key] && typeof item[key] === "object") {
934
+ item[key] = jtcsv.deepUnwrap(item[key], 0, maxDepth);
935
+ }
936
+ }
937
+ });
938
+ }
939
+
940
+ // Format JSON
941
+ const jsonOutput = options.pretty
942
+ ? JSON.stringify(transformedData, null, 2)
943
+ : JSON.stringify(transformedData);
944
+
945
+ // Write output file
946
+ await fs.promises.writeFile(outputFile, jsonOutput, "utf8");
947
+
948
+ const elapsed = Date.now() - startTime;
949
+ if (!options.silent) {
950
+ console.log(
951
+ color(
952
+ `✓ Preprocessed ${transformedData.length.toLocaleString()} records in ${elapsed}ms`,
953
+ "green",
954
+ ),
955
+ );
956
+ console.log(
957
+ color(
958
+ ` Output: ${outputFile} (${jsonOutput.length.toLocaleString()} bytes)`,
959
+ "dim",
960
+ ),
961
+ );
962
+ }
963
+
964
+ return {
965
+ records: transformedData.length,
966
+ bytes: jsonOutput.length,
967
+ time: elapsed,
968
+ };
969
+ } catch (error) {
970
+ console.error(color(`✗ Error: ${error.message}`, "red"));
971
+ if (options.debug) {
972
+ console.error(error.stack);
973
+ }
974
+ process.exit(1);
975
+ }
976
+ }
977
+
978
+ // ============================================================================
979
+ // STREAMING FUNCTIONS
980
+ // ============================================================================
981
+
982
+ async function streamJsonToCsv(inputFile, outputFile, options) {
983
+ const startTime = Date.now();
984
+ let recordCount = 0;
985
+
986
+ try {
987
+ if (!options.silent) {
988
+ console.log(color("Streaming JSON to CSV...", "dim"));
989
+ }
990
+
991
+ // Create streams
992
+ const readStream = fs.createReadStream(inputFile, "utf8");
993
+ const writeStream = fs.createWriteStream(outputFile, "utf8");
994
+
995
+ // Add UTF-8 BOM if requested
996
+ if (options.addBOM) {
997
+ writeStream.write("\uFEFF");
998
+ }
999
+
1000
+ // Parse JSON stream
1001
+ let buffer = "";
1002
+ let isFirstChunk = true;
1003
+ let headersWritten = false;
1004
+
1005
+ readStream.on("data", (chunk) => {
1006
+ buffer += chunk;
1007
+
1008
+ // Try to parse complete JSON objects
1009
+ const lines = buffer.split("\n");
1010
+ buffer = lines.pop() || "";
1011
+
1012
+ for (const line of lines) {
1013
+ if (line.trim()) {
1014
+ try {
1015
+ const obj = JSON.parse(line);
1016
+ // Apply renameMap if provided
1017
+ let finalObj = obj;
1018
+ if (options.renameMap) {
1019
+ finalObj = {};
1020
+ for (const [oldKey, newKey] of Object.entries(
1021
+ options.renameMap,
1022
+ )) {
1023
+ if (oldKey in obj) {
1024
+ finalObj[newKey] = obj[oldKey];
1025
+ }
1026
+ }
1027
+ // Copy remaining fields
1028
+ for (const [key, value] of Object.entries(obj)) {
1029
+ if (!(key in options.renameMap)) {
1030
+ finalObj[key] = value;
1031
+ }
1032
+ }
1033
+ }
1034
+ recordCount++;
1035
+
1036
+ // Write headers on first object
1037
+ if (!headersWritten && options.includeHeaders !== false) {
1038
+ const headers = Object.keys(finalObj);
1039
+ writeStream.write(headers.join(options.delimiter || ";") + "\n");
1040
+ headersWritten = true;
1041
+ }
1042
+
1043
+ // Write CSV row
1044
+ const row =
1045
+ Object.values(finalObj)
1046
+ .map((value) => {
1047
+ const str = String(value);
1048
+ if (
1049
+ str.includes(options.delimiter || ";") ||
1050
+ str.includes('"') ||
1051
+ str.includes("\n")
1052
+ ) {
1053
+ return `"${str.replace(/"/g, '""')}"`;
1054
+ }
1055
+ return str;
1056
+ })
1057
+ .join(options.delimiter || ";") + "\n";
1058
+
1059
+ writeStream.write(row);
1060
+
1061
+ // Show progress
1062
+ if (options.verbose && recordCount % 10000 === 0) {
1063
+ process.stdout.write(
1064
+ color(
1065
+ ` Processed ${recordCount.toLocaleString()} records\r`,
1066
+ "dim",
1067
+ ),
1068
+ );
1069
+ }
1070
+ } catch (error) {
1071
+ // Skip invalid JSON lines
1072
+ if (options.debug) {
1073
+ console.warn(
1074
+ color(
1075
+ ` Warning: Skipping invalid JSON line: ${error.message}`,
1076
+ "yellow",
1077
+ ),
1078
+ );
1079
+ }
1080
+ }
1081
+ }
1082
+ }
1083
+ });
1084
+
1085
+ readStream.on("end", async () => {
1086
+ // Process remaining buffer
1087
+ if (buffer.trim()) {
1088
+ try {
1089
+ const obj = JSON.parse(buffer);
1090
+
1091
+ // Apply renameMap if provided
1092
+ let finalObj = obj;
1093
+ if (options.renameMap) {
1094
+ finalObj = {};
1095
+ for (const [oldKey, newKey] of Object.entries(options.renameMap)) {
1096
+ if (oldKey in obj) {
1097
+ finalObj[newKey] = obj[oldKey];
1098
+ }
1099
+ }
1100
+ // Copy remaining fields
1101
+ for (const [key, value] of Object.entries(obj)) {
1102
+ if (!(key in options.renameMap)) {
1103
+ finalObj[key] = value;
1104
+ }
1105
+ }
1106
+ }
1107
+
1108
+ recordCount++;
1109
+
1110
+ if (!headersWritten && options.includeHeaders !== false) {
1111
+ const headers = Object.keys(finalObj);
1112
+ writeStream.write(headers.join(options.delimiter || ";") + "\n");
1113
+ }
1114
+
1115
+ const row =
1116
+ Object.values(finalObj)
1117
+ .map((value) => {
1118
+ const str = String(value);
1119
+ if (
1120
+ str.includes(options.delimiter || ";") ||
1121
+ str.includes('"') ||
1122
+ str.includes("\n")
1123
+ ) {
1124
+ return `"${str.replace(/"/g, '""')}"`;
1125
+ }
1126
+ return str;
1127
+ })
1128
+ .join(options.delimiter || ";") + "\n";
1129
+
1130
+ writeStream.write(row);
1131
+ } catch (error) {
1132
+ // Skip invalid JSON
1133
+ }
1134
+ }
1135
+
1136
+ writeStream.end();
1137
+
1138
+ // Wait for write stream to finish
1139
+ await new Promise((resolve) => writeStream.on("finish", resolve));
1140
+
1141
+ const elapsed = Date.now() - startTime;
1142
+ if (!options.silent) {
1143
+ console.log(
1144
+ color(
1145
+ `\n✓ Streamed ${recordCount.toLocaleString()} records in ${elapsed}ms`,
1146
+ "green",
1147
+ ),
1148
+ );
1149
+ console.log(color(` Output: ${outputFile}`, "dim"));
1150
+ }
1151
+ });
1152
+
1153
+ readStream.on("error", (error) => {
1154
+ console.error(color(`✗ Stream error: ${error.message}`, "red"));
1155
+ process.exit(1);
1156
+ });
1157
+
1158
+ writeStream.on("error", (error) => {
1159
+ console.error(color(`✗ Write error: ${error.message}`, "red"));
1160
+ process.exit(1);
1161
+ });
1162
+ } catch (error) {
1163
+ console.error(color(`✗ Error: ${error.message}`, "red"));
1164
+ if (options.debug) {
1165
+ console.error(error.stack);
1166
+ }
1167
+ process.exit(1);
1168
+ }
1169
+ }
1170
+
1171
+ async function streamCsvToJson(inputFile, outputFile, options) {
1172
+ const startTime = Date.now();
1173
+ let rowCount = 0;
1174
+
1175
+ try {
1176
+ if (!options.silent) {
1177
+ console.log(color("Streaming CSV to JSON...", "dim"));
1178
+ }
1179
+
1180
+ // Create streams
1181
+ const readStream = fs.createReadStream(inputFile, "utf8");
1182
+ const writeStream = fs.createWriteStream(outputFile, "utf8");
1183
+
1184
+ // Write JSON array opening bracket
1185
+ writeStream.write("[\n");
1186
+
1187
+ let buffer = "";
1188
+ let isFirstRow = true;
1189
+ let headers = [];
1190
+
1191
+ readStream.on("data", (chunk) => {
1192
+ buffer += chunk;
1193
+
1194
+ // Process complete lines
1195
+ const lines = buffer.split("\n");
1196
+ buffer = lines.pop() || "";
1197
+
1198
+ for (let i = 0; i < lines.length; i++) {
1199
+ const line = lines[i].trim();
1200
+ if (!line) continue;
1201
+
1202
+ rowCount++;
1203
+
1204
+ // Parse CSV line
1205
+ const fields = parseCsvLineSimple(line, options.delimiter || ";");
1206
+
1207
+ // First row might be headers
1208
+ if (rowCount === 1 && options.hasHeaders !== false) {
1209
+ headers = fields;
1210
+ continue;
1211
+ }
1212
+
1213
+ // Create JSON object
1214
+ const obj = {};
1215
+ const fieldCount = Math.min(fields.length, headers.length);
1216
+
1217
+ for (let j = 0; j < fieldCount; j++) {
1218
+ const header = headers[j] || `column${j + 1}`;
1219
+ // Apply renameMap if provided
1220
+ let finalHeader = header;
1221
+ if (options.renameMap && options.renameMap[header]) {
1222
+ finalHeader = options.renameMap[header];
1223
+ }
1224
+
1225
+ let value = fields[j];
1226
+
1227
+ // Parse numbers if enabled
1228
+ if (options.parseNumbers && /^-?\d+(\.\d+)?$/.test(value)) {
1229
+ const num = parseFloat(value);
1230
+ if (!isNaN(num)) {
1231
+ value = num;
1232
+ }
1233
+ }
1234
+
1235
+ // Parse booleans if enabled
1236
+ if (options.parseBooleans) {
1237
+ const lowerValue = value.toLowerCase();
1238
+ if (lowerValue === "true") value = true;
1239
+ if (lowerValue === "false") value = false;
1240
+ }
1241
+
1242
+ obj[finalHeader] = value;
1243
+ }
1244
+
1245
+ // Write JSON object
1246
+ const jsonStr = JSON.stringify(obj);
1247
+ if (!isFirstRow) {
1248
+ writeStream.write(",\n");
1249
+ }
1250
+ writeStream.write(" " + jsonStr);
1251
+ isFirstRow = false;
1252
+
1253
+ // Show progress
1254
+ if (options.verbose && rowCount % 10000 === 0) {
1255
+ process.stdout.write(
1256
+ color(` Processed ${rowCount.toLocaleString()} rows\r`, "dim"),
1257
+ );
1258
+ }
1259
+ }
1260
+ });
1261
+
1262
+ readStream.on("end", async () => {
1263
+ // Process remaining buffer
1264
+ if (buffer.trim()) {
1265
+ const fields = parseCsvLineSimple(
1266
+ buffer.trim(),
1267
+ options.delimiter || ";",
1268
+ );
1269
+
1270
+ if (fields.length > 0) {
1271
+ rowCount++;
1272
+
1273
+ // Skip if it's headers
1274
+ if (!(rowCount === 1 && options.hasHeaders !== false)) {
1275
+ const obj = {};
1276
+ const fieldCount = Math.min(fields.length, headers.length);
1277
+
1278
+ for (let j = 0; j < fieldCount; j++) {
1279
+ const header = headers[j] || `column${j + 1}`;
1280
+ // Apply renameMap if provided
1281
+ let finalHeader = header;
1282
+ if (options.renameMap && options.renameMap[header]) {
1283
+ finalHeader = options.renameMap[header];
1284
+ }
1285
+ obj[finalHeader] = fields[j];
1286
+ }
1287
+
1288
+ const jsonStr = JSON.stringify(obj);
1289
+ if (!isFirstRow) {
1290
+ writeStream.write(",\n");
1291
+ }
1292
+ writeStream.write(" " + jsonStr);
1293
+ }
1294
+ }
1295
+ }
1296
+
1297
+ // Write JSON array closing bracket
1298
+ writeStream.write("\n]");
1299
+ writeStream.end();
1300
+
1301
+ // Wait for write stream to finish
1302
+ await new Promise((resolve) => writeStream.on("finish", resolve));
1303
+
1304
+ const elapsed = Date.now() - startTime;
1305
+ if (!options.silent) {
1306
+ console.log(
1307
+ color(
1308
+ `\n✓ Streamed ${(rowCount - (options.hasHeaders !== false ? 1 : 0)).toLocaleString()} rows in ${elapsed}ms`,
1309
+ "green",
1310
+ ),
1311
+ );
1312
+ console.log(color(` Output: ${outputFile}`, "dim"));
1313
+ }
1314
+ });
1315
+
1316
+ readStream.on("error", (error) => {
1317
+ console.error(color(`✗ Stream error: ${error.message}`, "red"));
1318
+ process.exit(1);
1319
+ });
1320
+
1321
+ writeStream.on("error", (error) => {
1322
+ console.error(color(`✗ Write error: ${error.message}`, "red"));
1323
+ process.exit(1);
1324
+ });
1325
+ } catch (error) {
1326
+ console.error(color(`✗ Error: ${error.message}`, "red"));
1327
+ if (options.debug) {
1328
+ console.error(error.stack);
1329
+ }
1330
+ process.exit(1);
1331
+ }
1332
+ }
1333
+
1334
+ // Simple CSV line parser for streaming
1335
+ function parseCsvLineSimple(line, delimiter) {
1336
+ const fields = [];
1337
+ let currentField = "";
1338
+ let inQuotes = false;
1339
+
1340
+ for (let i = 0; i < line.length; i++) {
1341
+ const char = line[i];
1342
+
1343
+ if (char === '"') {
1344
+ if (inQuotes && i + 1 < line.length && line[i + 1] === '"') {
1345
+ // Escaped quote
1346
+ currentField += '"';
1347
+ i++;
1348
+ } else {
1349
+ // Toggle quotes
1350
+ inQuotes = !inQuotes;
1351
+ }
1352
+ } else if (char === delimiter && !inQuotes) {
1353
+ fields.push(currentField);
1354
+ currentField = "";
1355
+ } else {
1356
+ currentField += char;
1357
+ }
1358
+ }
1359
+
1360
+ fields.push(currentField);
1361
+ return fields;
1362
+ }
1363
+
1364
+ // ============================================================================
1365
+ // BATCH PROCESSING FUNCTIONS
1366
+ // ============================================================================
1367
+
1368
+ async function batchJsonToCsv(inputPattern, outputDir, options) {
1369
+ const startTime = Date.now();
1370
+
1371
+ try {
1372
+ let glob;
1373
+ try {
1374
+ glob = require("glob");
1375
+ } catch (error) {
1376
+ console.error(
1377
+ color(
1378
+ '✗ Error: The "glob" module is required for batch processing',
1379
+ "red",
1380
+ ),
1381
+ );
1382
+ console.error(color(" Install it with: npm install glob", "cyan"));
1383
+ console.error(color(" Or update jtcsv: npm update jtcsv", "cyan"));
1384
+ process.exit(1);
1385
+ }
1386
+ const files = glob.sync(inputPattern, {
1387
+ absolute: true,
1388
+ nodir: true,
1389
+ });
1390
+
1391
+ if (files.length === 0) {
1392
+ console.error(
1393
+ color(`✗ No files found matching pattern: ${inputPattern}`, "red"),
1394
+ );
1395
+ process.exit(1);
1396
+ }
1397
+
1398
+ if (!options.silent) {
1399
+ console.log(color(`Found ${files.length} files to process...`, "dim"));
1400
+ }
1401
+
1402
+ // Create output directory if it doesn't exist
1403
+ await fs.promises.mkdir(outputDir, { recursive: true });
1404
+
1405
+ const results = [];
1406
+ const parallelLimit = options.parallel || 4;
1407
+
1408
+ // Process files in parallel batches
1409
+ for (let i = 0; i < files.length; i += parallelLimit) {
1410
+ const batch = files.slice(i, i + parallelLimit);
1411
+ const promises = batch.map(async (file) => {
1412
+ const fileName = path.basename(file, ".json");
1413
+ const outputFile = path.join(outputDir, `${fileName}.csv`);
1414
+
1415
+ if (!options.silent && options.verbose) {
1416
+ console.log(color(` Processing: ${file}`, "dim"));
1417
+ }
1418
+
1419
+ try {
1420
+ const result = await convertJsonToCsv(file, outputFile, {
1421
+ ...options,
1422
+ silent: true, // Suppress individual file output
1423
+ });
1424
+
1425
+ if (!options.silent) {
1426
+ console.log(
1427
+ color(
1428
+ ` ✓ ${fileName}.json → ${fileName}.csv (${result.records} records)`,
1429
+ "green",
1430
+ ),
1431
+ );
1432
+ }
1433
+
1434
+ return { file, success: true, ...result };
1435
+ } catch (error) {
1436
+ if (!options.silent) {
1437
+ console.log(color(` ✗ ${fileName}.json: ${error.message}`, "red"));
1438
+ }
1439
+ return { file, success: false, error: error.message };
1440
+ }
1441
+ });
1442
+
1443
+ const batchResults = await Promise.all(promises);
1444
+ results.push(...batchResults);
1445
+
1446
+ if (!options.silent) {
1447
+ const processed = i + batch.length;
1448
+ const percent = Math.round((processed / files.length) * 100);
1449
+ console.log(
1450
+ color(
1451
+ ` Progress: ${processed}/${files.length} (${percent}%)`,
1452
+ "dim",
1453
+ ),
1454
+ );
1455
+ }
1456
+ }
1457
+
1458
+ const elapsed = Date.now() - startTime;
1459
+ const successful = results.filter((r) => r.success).length;
1460
+ const totalRecords = results
1461
+ .filter((r) => r.success)
1462
+ .reduce((sum, r) => sum + (r.records || 0), 0);
1463
+
1464
+ if (!options.silent) {
1465
+ console.log(
1466
+ color(`\n✓ Batch processing completed in ${elapsed}ms`, "green"),
1467
+ );
1468
+ console.log(
1469
+ color(` Successful: ${successful}/${files.length} files`, "dim"),
1470
+ );
1471
+ console.log(
1472
+ color(` Total records: ${totalRecords.toLocaleString()}`, "dim"),
1473
+ );
1474
+ console.log(color(` Output directory: ${outputDir}`, "dim"));
1475
+ }
1476
+
1477
+ return {
1478
+ totalFiles: files.length,
1479
+ successful,
1480
+ totalRecords,
1481
+ time: elapsed,
1482
+ results,
1483
+ };
1484
+ } catch (error) {
1485
+ console.error(color(`✗ Batch processing error: ${error.message}`, "red"));
1486
+ if (options.debug) {
1487
+ console.error(error.stack);
1488
+ }
1489
+ process.exit(1);
1490
+ }
1491
+ }
1492
+
1493
+ async function batchCsvToJson(inputPattern, outputDir, options) {
1494
+ const startTime = Date.now();
1495
+
1496
+ try {
1497
+ let glob;
1498
+ try {
1499
+ glob = require("glob");
1500
+ } catch (error) {
1501
+ console.error(
1502
+ color(
1503
+ '✗ Error: The "glob" module is required for batch processing',
1504
+ "red",
1505
+ ),
1506
+ );
1507
+ console.error(color(" Install it with: npm install glob", "cyan"));
1508
+ console.error(color(" Or update jtcsv: npm update jtcsv", "cyan"));
1509
+ process.exit(1);
1510
+ }
1511
+ const files = glob.sync(inputPattern, {
1512
+ absolute: true,
1513
+ nodir: true,
1514
+ });
1515
+
1516
+ if (files.length === 0) {
1517
+ console.error(
1518
+ color(`✗ No files found matching pattern: ${inputPattern}`, "red"),
1519
+ );
1520
+ process.exit(1);
1521
+ }
1522
+
1523
+ if (!options.silent) {
1524
+ console.log(color(`Found ${files.length} files to process...`, "dim"));
1525
+ }
1526
+
1527
+ // Create output directory if it doesn't exist
1528
+ await fs.promises.mkdir(outputDir, { recursive: true });
1529
+
1530
+ const results = [];
1531
+ const parallelLimit = options.parallel || 4;
1532
+
1533
+ // Process files in parallel batches
1534
+ for (let i = 0; i < files.length; i += parallelLimit) {
1535
+ const batch = files.slice(i, i + parallelLimit);
1536
+ const promises = batch.map(async (file) => {
1537
+ const fileName = path.basename(file, ".csv");
1538
+ const outputFile = path.join(outputDir, `${fileName}.json`);
1539
+
1540
+ if (!options.silent && options.verbose) {
1541
+ console.log(color(` Processing: ${file}`, "dim"));
1542
+ }
1543
+
1544
+ try {
1545
+ const result = await convertCsvToJson(file, outputFile, {
1546
+ ...options,
1547
+ silent: true, // Suppress individual file output
1548
+ });
1549
+
1550
+ if (!options.silent) {
1551
+ console.log(
1552
+ color(
1553
+ ` ✓ ${fileName}.csv → ${fileName}.json (${result.rows} rows)`,
1554
+ "green",
1555
+ ),
1556
+ );
1557
+ }
1558
+
1559
+ return { file, success: true, ...result };
1560
+ } catch (error) {
1561
+ if (!options.silent) {
1562
+ console.log(color(` ✗ ${fileName}.csv: ${error.message}`, "red"));
1563
+ }
1564
+ return { file, success: false, error: error.message };
1565
+ }
1566
+ });
1567
+
1568
+ const batchResults = await Promise.all(promises);
1569
+ results.push(...batchResults);
1570
+
1571
+ if (!options.silent) {
1572
+ const processed = i + batch.length;
1573
+ const percent = Math.round((processed / files.length) * 100);
1574
+ console.log(
1575
+ color(
1576
+ ` Progress: ${processed}/${files.length} (${percent}%)`,
1577
+ "dim",
1578
+ ),
1579
+ );
1580
+ }
1581
+ }
1582
+
1583
+ const elapsed = Date.now() - startTime;
1584
+ const successful = results.filter((r) => r.success).length;
1585
+ const totalRows = results
1586
+ .filter((r) => r.success)
1587
+ .reduce((sum, r) => sum + (r.rows || 0), 0);
1588
+
1589
+ if (!options.silent) {
1590
+ console.log(
1591
+ color(`\n✓ Batch processing completed in ${elapsed}ms`, "green"),
1592
+ );
1593
+ console.log(
1594
+ color(` Successful: ${successful}/${files.length} files`, "dim"),
1595
+ );
1596
+ console.log(color(` Total rows: ${totalRows.toLocaleString()}`, "dim"));
1597
+ console.log(color(` Output directory: ${outputDir}`, "dim"));
1598
+ }
1599
+
1600
+ return {
1601
+ totalFiles: files.length,
1602
+ successful,
1603
+ totalRows,
1604
+ time: elapsed,
1605
+ results,
1606
+ };
1607
+ } catch (error) {
1608
+ console.error(color(`✗ Batch processing error: ${error.message}`, "red"));
1609
+ if (options.debug) {
1610
+ console.error(error.stack);
1611
+ }
1612
+ process.exit(1);
1613
+ }
1614
+ }
1615
+
1616
+ async function batchProcessMixed(inputPattern, outputDir, options) {
1617
+ const startTime = Date.now();
1618
+
1619
+ try {
1620
+ let glob;
1621
+ try {
1622
+ glob = require("glob");
1623
+ } catch (error) {
1624
+ console.error(
1625
+ color(
1626
+ '✗ Error: The "glob" module is required for batch processing',
1627
+ "red",
1628
+ ),
1629
+ );
1630
+ console.error(color(" Install it with: npm install glob", "cyan"));
1631
+ console.error(color(" Or update jtcsv: npm update jtcsv", "cyan"));
1632
+ process.exit(1);
1633
+ }
1634
+
1635
+ // Находим все файлы
1636
+ const files = glob.sync(inputPattern, {
1637
+ absolute: true,
1638
+ nodir: true,
1639
+ });
1640
+
1641
+ if (files.length === 0) {
1642
+ console.error(
1643
+ color(`✗ No files found matching pattern: ${inputPattern}`, "red"),
1644
+ );
1645
+ process.exit(1);
1646
+ }
1647
+
1648
+ if (!options.silent) {
1649
+ console.log(color(`Found ${files.length} files to process...`, "dim"));
1650
+ }
1651
+
1652
+ // Создаем выходную директорию
1653
+ await fs.promises.mkdir(outputDir, { recursive: true });
1654
+
1655
+ const results = [];
1656
+ const parallelLimit = options.parallel || 4;
1657
+
1658
+ // Группируем файлы по типу
1659
+ const jsonFiles = files.filter((file) =>
1660
+ file.toLowerCase().endsWith(".json"),
1661
+ );
1662
+ const csvFiles = files.filter((file) =>
1663
+ file.toLowerCase().endsWith(".csv"),
1664
+ );
1665
+ const otherFiles = files.filter(
1666
+ (file) =>
1667
+ !file.toLowerCase().endsWith(".json") &&
1668
+ !file.toLowerCase().endsWith(".csv"),
1669
+ );
1670
+
1671
+ if (otherFiles.length > 0 && !options.silent) {
1672
+ console.log(
1673
+ color(
1674
+ ` Warning: Skipping ${otherFiles.length} non-JSON/CSV files`,
1675
+ "yellow",
1676
+ ),
1677
+ );
1678
+ }
1679
+
1680
+ // Обрабатываем JSON файлы
1681
+ for (let i = 0; i < jsonFiles.length; i += parallelLimit) {
1682
+ const batch = jsonFiles.slice(i, i + parallelLimit);
1683
+ const promises = batch.map(async (file) => {
1684
+ const fileName = path.basename(file, ".json");
1685
+ const outputFile = path.join(outputDir, `${fileName}.csv`);
1686
+
1687
+ if (!options.silent && options.verbose) {
1688
+ console.log(color(` Processing JSON: ${file}`, "dim"));
1689
+ }
1690
+
1691
+ try {
1692
+ const result = await convertJsonToCsv(file, outputFile, {
1693
+ ...options,
1694
+ silent: true,
1695
+ });
1696
+
1697
+ if (!options.silent) {
1698
+ console.log(
1699
+ color(
1700
+ ` ✓ ${fileName}.json → ${fileName}.csv (${result.records} records)`,
1701
+ "green",
1702
+ ),
1703
+ );
1704
+ }
1705
+
1706
+ return { file, type: "json", success: true, ...result };
1707
+ } catch (error) {
1708
+ if (!options.silent) {
1709
+ console.log(color(` ✗ ${fileName}.json: ${error.message}`, "red"));
1710
+ }
1711
+ return { file, type: "json", success: false, error: error.message };
1712
+ }
1713
+ });
1714
+
1715
+ const batchResults = await Promise.all(promises);
1716
+ results.push(...batchResults);
1717
+ }
1718
+
1719
+ // Обрабатываем CSV файлы
1720
+ for (let i = 0; i < csvFiles.length; i += parallelLimit) {
1721
+ const batch = csvFiles.slice(i, i + parallelLimit);
1722
+ const promises = batch.map(async (file) => {
1723
+ const fileName = path.basename(file, ".csv");
1724
+ const outputFile = path.join(outputDir, `${fileName}.json`);
1725
+
1726
+ if (!options.silent && options.verbose) {
1727
+ console.log(color(` Processing CSV: ${file}`, "dim"));
1728
+ }
1729
+
1730
+ try {
1731
+ const result = await convertCsvToJson(file, outputFile, {
1732
+ ...options,
1733
+ silent: true,
1734
+ });
1735
+
1736
+ if (!options.silent) {
1737
+ console.log(
1738
+ color(
1739
+ ` ✓ ${fileName}.csv → ${fileName}.json (${result.rows} rows)`,
1740
+ "green",
1741
+ ),
1742
+ );
1743
+ }
1744
+
1745
+ return { file, type: "csv", success: true, ...result };
1746
+ } catch (error) {
1747
+ if (!options.silent) {
1748
+ console.log(color(` ✗ ${fileName}.csv: ${error.message}`, "red"));
1749
+ }
1750
+ return { file, type: "csv", success: false, error: error.message };
1751
+ }
1752
+ });
1753
+
1754
+ const batchResults = await Promise.all(promises);
1755
+ results.push(...batchResults);
1756
+ }
1757
+
1758
+ const elapsed = Date.now() - startTime;
1759
+ const successful = results.filter((r) => r.success).length;
1760
+ const totalRecords = results
1761
+ .filter((r) => r.success)
1762
+ .reduce((sum, r) => sum + (r.records || r.rows || 0), 0);
1763
+
1764
+ if (!options.silent) {
1765
+ console.log(
1766
+ color(`\n✓ Mixed batch processing completed in ${elapsed}ms`, "green"),
1767
+ );
1768
+ console.log(
1769
+ color(` Successful: ${successful}/${files.length} files`, "dim"),
1770
+ );
1771
+ console.log(
1772
+ color(
1773
+ ` JSON files: ${jsonFiles.length}, CSV files: ${csvFiles.length}`,
1774
+ "dim",
1775
+ ),
1776
+ );
1777
+ console.log(
1778
+ color(` Total records: ${totalRecords.toLocaleString()}`, "dim"),
1779
+ );
1780
+ console.log(color(` Output directory: ${outputDir}`, "dim"));
1781
+ }
1782
+
1783
+ return {
1784
+ totalFiles: files.length,
1785
+ jsonFiles: jsonFiles.length,
1786
+ csvFiles: csvFiles.length,
1787
+ otherFiles: otherFiles.length,
1788
+ successful,
1789
+ totalRecords,
1790
+ time: elapsed,
1791
+ results,
1792
+ };
1793
+ } catch (error) {
1794
+ console.error(color(`✗ Batch processing error: ${error.message}`, "red"));
1795
+ if (options.debug) {
1796
+ console.error(error.stack);
1797
+ }
1798
+ process.exit(1);
1799
+ }
1800
+ }
1801
+
1802
+ // ============================================================================
1803
+ // OPTIONS PARSING
1804
+ // ============================================================================
1805
+
1806
+ function parseOptions(args) {
1807
+ const options = {
1808
+ delimiter: ";",
1809
+ autoDetect: true,
1810
+ candidates: [";", ",", "\t", "|"],
1811
+ hasHeaders: true,
1812
+ includeHeaders: true,
1813
+ renameMap: undefined,
1814
+ template: undefined,
1815
+ trim: true,
1816
+ parseNumbers: false,
1817
+ parseBooleans: false,
1818
+ useFastPath: true,
1819
+ fastPathMode: "objects",
1820
+ preventCsvInjection: true,
1821
+ rfc4180Compliant: true,
1822
+ maxRecords: undefined,
1823
+ maxRows: undefined,
1824
+ maxDepth: 5,
1825
+ pretty: false,
1826
+ silent: false,
1827
+ verbose: false,
1828
+ debug: false,
1829
+ dryRun: false,
1830
+ addBOM: false,
1831
+ unwrapArrays: false,
1832
+ stringifyObjects: false,
1833
+ recursive: false,
1834
+ pattern: "**/*",
1835
+ outputDir: "./output",
1836
+ overwrite: false,
1837
+ parallel: 4,
1838
+ chunkSize: 65536,
1839
+ bufferSize: 1000,
1840
+ schema: undefined,
1841
+ transform: undefined,
1842
+ flattenPrefix: "_",
1843
+ };
1844
+
1845
+ const files = [];
1846
+
1847
+ for (let i = 0; i < args.length; i++) {
1848
+ const arg = args[i];
1849
+
1850
+ if (arg.startsWith("--")) {
1851
+ const [key, value] = arg.slice(2).split("=");
1852
+
1853
+ switch (key) {
1854
+ case "delimiter":
1855
+ options.delimiter = value || ",";
1856
+ options.autoDetect = false;
1857
+ break;
1858
+ case "auto-detect":
1859
+ options.autoDetect = value !== "false";
1860
+ break;
1861
+ case "candidates":
1862
+ options.candidates = value ? value.split(",") : [";", ",", "\t", "|"];
1863
+ break;
1864
+ case "no-headers":
1865
+ options.includeHeaders = false;
1866
+ options.hasHeaders = false;
1867
+ break;
1868
+ case "parse-numbers":
1869
+ options.parseNumbers = true;
1870
+ break;
1871
+ case "parse-booleans":
1872
+ options.parseBooleans = true;
1873
+ break;
1874
+ case "no-trim":
1875
+ options.trim = false;
1876
+ break;
1877
+ case "no-fast-path":
1878
+ options.useFastPath = false;
1879
+ break;
1880
+ case "fast-path":
1881
+ options.useFastPath = value !== "false";
1882
+ break;
1883
+ case "fast-path-mode":
1884
+ options.fastPathMode = value || "objects";
1885
+ if (
1886
+ options.fastPathMode !== "objects" &&
1887
+ options.fastPathMode !== "compact"
1888
+ ) {
1889
+ throw new Error("Invalid --fast-path-mode value (objects|compact)");
1890
+ }
1891
+ break;
1892
+ case "rename":
1893
+ try {
1894
+ const jsonStr = value || "{}";
1895
+ const cleanStr = jsonStr
1896
+ .replace(/^'|'$/g, "")
1897
+ .replace(/^"|"$/g, "");
1898
+ options.renameMap = JSON.parse(cleanStr);
1899
+ } catch (e) {
1900
+ throw new Error(`Invalid JSON in --rename option: ${e.message}`);
1901
+ }
1902
+ break;
1903
+ case "template":
1904
+ try {
1905
+ const jsonStr = value || "{}";
1906
+ const cleanStr = jsonStr
1907
+ .replace(/^'|'$/g, "")
1908
+ .replace(/^"|"$/g, "");
1909
+ options.template = JSON.parse(cleanStr);
1910
+ } catch (e) {
1911
+ throw new Error(`Invalid JSON in --template option: ${e.message}`);
1912
+ }
1913
+ break;
1914
+ case "no-injection-protection":
1915
+ options.preventCsvInjection = false;
1916
+ break;
1917
+ case "no-rfc4180":
1918
+ options.rfc4180Compliant = false;
1919
+ break;
1920
+ case "max-records":
1921
+ options.maxRecords = parseInt(value, 10);
1922
+ break;
1923
+ case "max-rows":
1924
+ options.maxRows = parseInt(value, 10);
1925
+ break;
1926
+ case "max-depth":
1927
+ options.maxDepth = parseInt(value, 10) || 5;
1928
+ break;
1929
+ case "pretty":
1930
+ options.pretty = true;
1931
+ break;
1932
+ case "silent":
1933
+ options.silent = true;
1934
+ break;
1935
+ case "verbose":
1936
+ options.verbose = true;
1937
+ break;
1938
+ case "debug":
1939
+ options.debug = true;
1940
+ break;
1941
+ case "dry-run":
1942
+ options.dryRun = true;
1943
+ break;
1944
+ case "add-bom":
1945
+ options.addBOM = true;
1946
+ break;
1947
+ case "unwrap-arrays":
1948
+ options.unwrapArrays = true;
1949
+ break;
1950
+ case "stringify-objects":
1951
+ options.stringifyObjects = true;
1952
+ break;
1953
+ case "recursive":
1954
+ options.recursive = true;
1955
+ break;
1956
+ case "pattern":
1957
+ options.pattern = value || "**/*";
1958
+ break;
1959
+ case "output-dir":
1960
+ options.outputDir = value || "./output";
1961
+ break;
1962
+ case "overwrite":
1963
+ options.overwrite = true;
1964
+ break;
1965
+ case "parallel":
1966
+ options.parallel = parseInt(value, 10) || 4;
1967
+ break;
1968
+ case "chunk-size":
1969
+ options.chunkSize = parseInt(value, 10) || 65536;
1970
+ break;
1971
+ case "buffer-size":
1972
+ options.bufferSize = parseInt(value, 10) || 1000;
1973
+ break;
1974
+ case "schema":
1975
+ try {
1976
+ const jsonStr = value || "{}";
1977
+ const cleanStr = jsonStr
1978
+ .replace(/^'|'$/g, "")
1979
+ .replace(/^"|"$/g, "");
1980
+ options.schema = JSON.parse(cleanStr);
1981
+ } catch (e) {
1982
+ throw new Error(`Invalid JSON in --schema option: ${e.message}`);
1983
+ }
1984
+ break;
1985
+ case "transform":
1986
+ options.transform = value;
1987
+ break;
1988
+ case "port":
1989
+ options.port = parseInt(value, 10) || 3000;
1990
+ break;
1991
+ case "host":
1992
+ options.host = value || 'localhost';
1993
+ break;
1994
+ }
1995
+ } else if (!arg.startsWith("-")) {
1996
+ files.push(arg);
1997
+ }
1998
+ }
1999
+
2000
+ return { options, files };
2001
+ }
2002
+
2003
+ // ============================================================================
2004
+ // TUI LAUNCHER
2005
+ // ============================================================================
2006
+
2007
+ async function launchTUI() {
2008
+ try {
2009
+ console.log(color("Launching Terminal User Interface...", "cyan"));
2010
+ console.log(color("Press Ctrl+Q to exit", "dim"));
2011
+
2012
+ const JtcsvTUI = require("@jtcsv/tui");
2013
+ const tui = new JtcsvTUI();
2014
+ tui.start();
2015
+ } catch (error) {
2016
+ if (error.code === "MODULE_NOT_FOUND") {
2017
+ console.error(color("Error: @jtcsv/tui is not installed", "red"));
2018
+ console.log(color("Install it with:", "dim"));
2019
+ console.log(color(" npm install @jtcsv/tui", "cyan"));
2020
+ console.log(color("\nOr use the CLI interface instead:", "dim"));
2021
+ console.log(color(" jtcsv help", "cyan"));
2022
+ } else {
2023
+ console.error(color(`Error: ${error.message}`, "red"));
2024
+ }
2025
+ process.exit(1);
2026
+ }
2027
+ }
2028
+
2029
+ async function launchWebUI(options = {}) {
2030
+ try {
2031
+ const webServer = require("../src/web-server");
2032
+ webServer.startServer({
2033
+ port: options.port || 3000,
2034
+ host: options.host || 'localhost'
2035
+ });
2036
+ } catch (error) {
2037
+ console.error(color(`Error: ${error.message}`, "red"));
2038
+ if (options.debug) {
2039
+ console.error(error.stack);
2040
+ }
2041
+ process.exit(1);
2042
+ }
2043
+ }
2044
+
2045
+ async function startBasicTUI() {
2046
+ const readline = require("readline");
2047
+
2048
+ const rl = readline.createInterface({
2049
+ input: process.stdin,
2050
+ output: process.stdout,
2051
+ });
2052
+
2053
+ console.clear();
2054
+ console.log(color("╔══════════════════════════════════════╗", "cyan"));
2055
+ console.log(color("║ JTCSV Terminal Interface ║", "cyan"));
2056
+ console.log(color("╚══════════════════════════════════════╝", "cyan"));
2057
+ console.log();
2058
+ console.log(color("Select operation:", "bright"));
2059
+ console.log(" 1. JSON → CSV");
2060
+ console.log(" 2. CSV → JSON");
2061
+ console.log(" 3. Preprocess JSON");
2062
+ console.log(" 4. Batch Processing");
2063
+ console.log(" 5. Exit");
2064
+ console.log();
2065
+
2066
+ rl.question(color("Enter choice (1-5): ", "cyan"), async (choice) => {
2067
+ switch (choice) {
2068
+ case "1":
2069
+ await runJsonToCsvTUI(rl);
2070
+ break;
2071
+ case "2":
2072
+ await runCsvToJsonTUI(rl);
2073
+ break;
2074
+ case "3":
2075
+ console.log(color("Preprocess feature coming soon...", "yellow"));
2076
+ rl.close();
2077
+ break;
2078
+ case "4":
2079
+ console.log(color("Batch processing coming soon...", "yellow"));
2080
+ rl.close();
2081
+ break;
2082
+ case "5":
2083
+ console.log(color("Goodbye!", "green"));
2084
+ rl.close();
2085
+ process.exit(0);
2086
+ break;
2087
+ default:
2088
+ console.log(color("Invalid choice", "red"));
2089
+ rl.close();
2090
+ process.exit(1);
2091
+ }
2092
+ });
2093
+ }
2094
+
2095
+ async function runJsonToCsvTUI(rl) {
2096
+ console.clear();
2097
+ console.log(color("JSON → CSV Conversion", "cyan"));
2098
+ console.log();
2099
+
2100
+ rl.question("Input JSON file: ", (inputFile) => {
2101
+ rl.question("Output CSV file: ", async (outputFile) => {
2102
+ rl.question("Delimiter (default: ;): ", async (delimiter) => {
2103
+ try {
2104
+ console.log(color("\nConverting...", "dim"));
2105
+
2106
+ const result = await convertJsonToCsv(inputFile, outputFile, {
2107
+ delimiter: delimiter || ";",
2108
+ silent: false,
2109
+ });
2110
+
2111
+ console.log(color("\n✓ Conversion complete!", "green"));
2112
+ rl.question("\nPress Enter to continue...", () => {
2113
+ rl.close();
2114
+ startBasicTUI();
2115
+ });
2116
+ } catch (error) {
2117
+ console.error(color(`✗ Error: ${error.message}`, "red"));
2118
+ rl.close();
2119
+ process.exit(1);
2120
+ }
2121
+ });
2122
+ });
2123
+ });
2124
+ }
2125
+
2126
+ async function runCsvToJsonTUI(rl) {
2127
+ console.clear();
2128
+ console.log(color("CSV → JSON Conversion", "cyan"));
2129
+ console.log();
2130
+
2131
+ rl.question("Input CSV file: ", (inputFile) => {
2132
+ rl.question("Output JSON file: ", async (outputFile) => {
2133
+ rl.question("Delimiter (default: ;): ", async (delimiter) => {
2134
+ rl.question("Pretty print? (y/n): ", async (pretty) => {
2135
+ try {
2136
+ console.log(color("\nConverting...", "dim"));
2137
+
2138
+ const result = await convertCsvToJson(inputFile, outputFile, {
2139
+ delimiter: delimiter || ";",
2140
+ pretty: pretty.toLowerCase() === "y",
2141
+ silent: false,
2142
+ });
2143
+
2144
+ console.log(color("\n✓ Conversion complete!", "green"));
2145
+ rl.question("\nPress Enter to continue...", () => {
2146
+ rl.close();
2147
+ startBasicTUI();
2148
+ });
2149
+ } catch (error) {
2150
+ console.error(color(`✗ Error: ${error.message}`, "red"));
2151
+ rl.close();
2152
+ process.exit(1);
2153
+ }
2154
+ });
2155
+ });
2156
+ });
2157
+ });
2158
+ }
2159
+
2160
+ // ============================================================================
2161
+ // MAIN FUNCTION
2162
+ // ============================================================================
2163
+
2164
+ async function main() {
2165
+ const args = process.argv.slice(2);
2166
+
2167
+ if (args.length === 0) {
2168
+ showHelp();
2169
+ return;
2170
+ }
2171
+
2172
+ const command = args[0].toLowerCase();
2173
+ const { options, files } = parseOptions(args.slice(1));
2174
+
2175
+ // Handle dry run
2176
+ if (options.dryRun) {
2177
+ console.log(color("DRY RUN - No files will be modified", "yellow"));
2178
+ console.log(`Command: ${command}`);
2179
+ console.log(`Files: ${files.join(", ")}`);
2180
+ console.log(`Options:`, options);
2181
+ return;
2182
+ }
2183
+
2184
+ // Suppress output if silent mode
2185
+ if (options.silent) {
2186
+ console.log = () => {};
2187
+ console.info = () => {};
2188
+ }
2189
+
2190
+ switch (command) {
2191
+ // Main conversion commands
2192
+ case "json-to-csv":
2193
+ case "json2csv":
2194
+ if (files.length < 2) {
2195
+ console.error(color("Error: Input and output files required", "red"));
2196
+ console.log(
2197
+ color("Usage: jtcsv json-to-csv input.json output.csv", "cyan"),
2198
+ );
2199
+ process.exit(1);
2200
+ }
2201
+ await convertJsonToCsv(files[0], files[1], options);
2202
+ break;
2203
+
2204
+ case "csv-to-json":
2205
+ case "csv2json":
2206
+ if (files.length < 2) {
2207
+ console.error(color("Error: Input and output files required", "red"));
2208
+ console.log(
2209
+ color("Usage: jtcsv csv-to-json input.csv output.json", "cyan"),
2210
+ );
2211
+ process.exit(1);
2212
+ }
2213
+ await convertCsvToJson(files[0], files[1], options);
2214
+ break;
2215
+
2216
+ case "save-json":
2217
+ if (files.length < 2) {
2218
+ console.error(color("Error: Input and output files required", "red"));
2219
+ console.log(
2220
+ color("Usage: jtcsv save-json input.json output.json", "cyan"),
2221
+ );
2222
+ process.exit(1);
2223
+ }
2224
+ await saveAsJson(files[0], files[1], options);
2225
+ break;
2226
+
2227
+ case "save-csv":
2228
+ if (files.length < 2) {
2229
+ console.error(color("Error: Input and output files required", "red"));
2230
+ console.log(
2231
+ color("Usage: jtcsv save-csv input.csv output.csv", "cyan"),
2232
+ );
2233
+ process.exit(1);
2234
+ }
2235
+ await saveAsCsv(files[0], files[1], options);
2236
+ break;
2237
+
2238
+ case "preprocess":
2239
+ if (files.length < 2) {
2240
+ console.error(color("Error: Input and output files required", "red"));
2241
+ console.log(
2242
+ color("Usage: jtcsv preprocess input.json output.json", "cyan"),
2243
+ );
2244
+ process.exit(1);
2245
+ }
2246
+ await preprocessJson(files[0], files[1], options);
2247
+ break;
2248
+
2249
+ // NDJSON commands
2250
+ case "ndjson-to-csv":
2251
+ if (files.length < 2) {
2252
+ console.error(color("Error: Input and output files required", "red"));
2253
+ console.log(
2254
+ color("Usage: jtcsv ndjson-to-csv input.ndjson output.csv", "cyan"),
2255
+ );
2256
+ process.exit(1);
2257
+ }
2258
+ await convertNdjsonToCsv(files[0], files[1], options);
2259
+ break;
2260
+
2261
+ case "csv-to-ndjson":
2262
+ if (files.length < 2) {
2263
+ console.error(color("Error: Input and output files required", "red"));
2264
+ console.log(
2265
+ color("Usage: jtcsv csv-to-ndjson input.csv output.ndjson", "cyan"),
2266
+ );
2267
+ process.exit(1);
2268
+ }
2269
+ await convertCsvToNdjson(files[0], files[1], options);
2270
+ break;
2271
+
2272
+ case "ndjson-to-json":
2273
+ if (files.length < 2) {
2274
+ console.error(color("Error: Input and output files required", "red"));
2275
+ console.log(
2276
+ color("Usage: jtcsv ndjson-to-json input.ndjson output.json", "cyan"),
2277
+ );
2278
+ process.exit(1);
2279
+ }
2280
+ await convertNdjsonToJson(files[0], files[1], options);
2281
+ break;
2282
+
2283
+ case "json-to-ndjson":
2284
+ if (files.length < 2) {
2285
+ console.error(color("Error: Input and output files required", "red"));
2286
+ console.log(
2287
+ color("Usage: jtcsv json-to-ndjson input.json output.ndjson", "cyan"),
2288
+ );
2289
+ process.exit(1);
2290
+ }
2291
+ await convertJsonToNdjson(files[0], files[1], options);
2292
+ break;
2293
+
2294
+ // Unwrap/Flatten command
2295
+ case "unwrap":
2296
+ case "flatten":
2297
+ if (files.length < 2) {
2298
+ console.error(color("Error: Input and output files required", "red"));
2299
+ console.log(
2300
+ color("Usage: jtcsv unwrap input.json output.json", "cyan"),
2301
+ );
2302
+ process.exit(1);
2303
+ }
2304
+ await unwrapJson(files[0], files[1], options);
2305
+ break;
2306
+
2307
+ // Streaming commands
2308
+ case "stream":
2309
+ if (args.length < 2) {
2310
+ console.error(
2311
+ color("Error: Streaming mode requires subcommand", "red"),
2312
+ );
2313
+ console.log(
2314
+ color(
2315
+ "Usage: jtcsv stream [json-to-csv|csv-to-json|file-to-csv|file-to-json]",
2316
+ "cyan",
2317
+ ),
2318
+ );
2319
+ process.exit(1);
2320
+ }
2321
+
2322
+ const streamCommand = args[1].toLowerCase();
2323
+ // Для stream команд нужно парсить опции начиная с 3-го аргумента (после stream и подкоманды)
2324
+ const streamArgs = args.slice(2);
2325
+ const { options: streamOptions, files: streamFiles } =
2326
+ parseOptions(streamArgs);
2327
+
2328
+ if (streamCommand === "json-to-csv" && streamFiles.length >= 2) {
2329
+ await streamJsonToCsv(streamFiles[0], streamFiles[1], streamOptions);
2330
+ } else if (streamCommand === "csv-to-json" && streamFiles.length >= 2) {
2331
+ await streamCsvToJson(streamFiles[0], streamFiles[1], streamOptions);
2332
+ } else if (streamCommand === "file-to-csv" && streamFiles.length >= 2) {
2333
+ // Use jtcsv streaming API if available
2334
+ try {
2335
+ const readStream = fs.createReadStream(streamFiles[0], "utf8");
2336
+ const writeStream = fs.createWriteStream(streamFiles[1], "utf8");
2337
+
2338
+ if (streamOptions.addBOM) {
2339
+ writeStream.write("\uFEFF");
2340
+ }
2341
+
2342
+ const transformStream = jtcsv.createJsonToCsvStream(streamOptions);
2343
+ await pipeline(readStream, transformStream, writeStream);
2344
+
2345
+ console.log(color("✓ File streamed successfully", "green"));
2346
+ } catch (error) {
2347
+ console.error(color(`✗ Streaming error: ${error.message}`, "red"));
2348
+ process.exit(1);
2349
+ }
2350
+ } else if (streamCommand === "file-to-json" && streamFiles.length >= 2) {
2351
+ // Use jtcsv streaming API if available
2352
+ try {
2353
+ const readStream = fs.createReadStream(streamFiles[0], "utf8");
2354
+ const writeStream = fs.createWriteStream(streamFiles[1], "utf8");
2355
+
2356
+ const transformStream = jtcsv.createCsvToJsonStream(streamOptions);
2357
+ await pipeline(readStream, transformStream, writeStream);
2358
+
2359
+ console.log(color("✓ File streamed successfully", "green"));
2360
+ } catch (error) {
2361
+ console.error(color(`✗ Streaming error: ${error.message}`, "red"));
2362
+ process.exit(1);
2363
+ }
2364
+ } else {
2365
+ console.error(
2366
+ color("Error: Invalid streaming command or missing files", "red"),
2367
+ );
2368
+ process.exit(1);
2369
+ }
2370
+ break;
2371
+
2372
+ // Batch processing commands
2373
+ case "batch":
2374
+ if (args.length < 2) {
2375
+ console.error(color("Error: Batch mode requires subcommand", "red"));
2376
+ console.log(
2377
+ color("Usage: jtcsv batch [json-to-csv|csv-to-json|process]", "cyan"),
2378
+ );
2379
+ process.exit(1);
2380
+ }
2381
+
2382
+ const batchCommand = args[1].toLowerCase();
2383
+ // Для batch команд нужно парсить опции начиная с 3-го аргумента (после batch и подкоманды)
2384
+ const batchArgs = args.slice(2);
2385
+ const { options: batchOptions, files: batchFiles } =
2386
+ parseOptions(batchArgs);
2387
+
2388
+ if (batchCommand === "json-to-csv" && batchFiles.length >= 2) {
2389
+ await batchJsonToCsv(batchFiles[0], batchFiles[1], batchOptions);
2390
+ } else if (batchCommand === "csv-to-json" && batchFiles.length >= 2) {
2391
+ await batchCsvToJson(batchFiles[0], batchFiles[1], batchOptions);
2392
+ } else if (batchCommand === "process" && files.length >= 2) {
2393
+ await batchProcessMixed(files[0], files[1], options);
2394
+ } else {
2395
+ console.error(
2396
+ color("Error: Invalid batch command or missing files", "red"),
2397
+ );
2398
+ process.exit(1);
2399
+ }
2400
+ break;
2401
+
2402
+ // TUI command
2403
+ case "tui":
2404
+ await launchTUI();
2405
+ break;
2406
+
2407
+ // Web UI command
2408
+ case "web":
2409
+ await launchWebUI(options);
2410
+ break;
2411
+
2412
+ // Help and version
2413
+ case "help":
2414
+ case "--help":
2415
+ case "-h":
2416
+ showHelp();
2417
+ break;
2418
+
2419
+ case "version":
2420
+ case "-v":
2421
+ case "--version":
2422
+ showVersion();
2423
+ break;
2424
+
2425
+ default:
2426
+ console.error(color(`Error: Unknown command '${command}'`, "red"));
2427
+ console.log(color("Use jtcsv help for available commands", "cyan"));
2428
+ process.exit(1);
2429
+ }
2430
+ }
2431
+
2432
+ // ============================================================================
2433
+ // ERROR HANDLING
2434
+ // ============================================================================
2435
+
2436
+ process.on("uncaughtException", (error) => {
2437
+ console.error(color(`\n✗ Uncaught error: ${error.message}`, "red"));
2438
+ if (process.env.DEBUG) {
2439
+ console.error(error.stack);
2440
+ }
2441
+ process.exit(1);
2442
+ });
2443
+
2444
+ process.on("unhandledRejection", (error) => {
2445
+ console.error(
2446
+ color(`\n✗ Unhandled promise rejection: ${error.message}`, "red"),
2447
+ );
2448
+ if (process.env.DEBUG) {
2449
+ console.error(error.stack);
2450
+ }
2451
+ process.exit(1);
2452
+ });
2453
+
2454
+ // Run main function
2455
+ if (require.main === module) {
2456
+ main().catch((error) => {
2457
+ console.error(color(`\n✗ Fatal error: ${error.message}`, "red"));
2458
+ process.exit(1);
2459
+ });
2460
+ }
2461
+
2462
+ module.exports = { main };