jtcsv 2.1.0 → 2.1.3

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 (40) hide show
  1. package/README.md +63 -17
  2. package/bin/jtcsv.js +1013 -117
  3. package/csv-to-json.js +385 -311
  4. package/examples/simple-usage.js +2 -3
  5. package/index.d.ts +288 -5
  6. package/index.js +23 -0
  7. package/json-to-csv.js +130 -89
  8. package/package.json +47 -19
  9. package/plugins/README.md +146 -2
  10. package/plugins/hono/README.md +25 -0
  11. package/plugins/hono/index.d.ts +12 -0
  12. package/plugins/hono/index.js +36 -0
  13. package/plugins/hono/package.json +35 -0
  14. package/plugins/nestjs/README.md +33 -0
  15. package/plugins/nestjs/index.d.ts +25 -0
  16. package/plugins/nestjs/index.js +77 -0
  17. package/plugins/nestjs/package.json +37 -0
  18. package/plugins/nuxt/README.md +25 -0
  19. package/plugins/nuxt/index.js +21 -0
  20. package/plugins/nuxt/package.json +35 -0
  21. package/plugins/nuxt/runtime/composables/useJtcsv.js +6 -0
  22. package/plugins/nuxt/runtime/plugin.js +6 -0
  23. package/plugins/remix/README.md +26 -0
  24. package/plugins/remix/index.d.ts +16 -0
  25. package/plugins/remix/index.js +62 -0
  26. package/plugins/remix/package.json +35 -0
  27. package/plugins/sveltekit/README.md +28 -0
  28. package/plugins/sveltekit/index.d.ts +17 -0
  29. package/plugins/sveltekit/index.js +54 -0
  30. package/plugins/sveltekit/package.json +33 -0
  31. package/plugins/trpc/README.md +22 -0
  32. package/plugins/trpc/index.d.ts +7 -0
  33. package/plugins/trpc/index.js +32 -0
  34. package/plugins/trpc/package.json +34 -0
  35. package/src/core/delimiter-cache.js +186 -0
  36. package/src/core/transform-hooks.js +350 -0
  37. package/src/engines/fast-path-engine.js +829 -340
  38. package/src/formats/tsv-parser.js +336 -0
  39. package/src/index-with-plugins.js +36 -14
  40. package/cli-tui.js +0 -5
package/bin/jtcsv.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * jtcsv CLI - Command Line Interface
4
+ * jtcsv CLI - Complete Command Line Interface
5
5
  *
6
- * Simple command-line interface for JSON↔CSV conversion
7
- * with streaming support and security features.
6
+ * Full-featured command-line interface for JSON↔CSV conversion
7
+ * with streaming, batch processing, and all security features.
8
8
  */
9
9
 
10
10
  const fs = require('fs');
@@ -40,14 +40,27 @@ ${color('The Complete JSON↔CSV Converter for Node.js', 'dim')}
40
40
  ${color('USAGE:', 'bright')}
41
41
  jtcsv [command] [options] [file...]
42
42
 
43
- ${color('COMMANDS:', 'bright')}
43
+ ${color('MAIN COMMANDS:', 'bright')}
44
44
  ${color('json-to-csv', 'green')} Convert JSON to CSV (alias: json2csv)
45
45
  ${color('csv-to-json', 'green')} Convert CSV to JSON (alias: csv2json)
46
- ${color('stream', 'yellow')} Streaming conversion for large files
47
- ${color('batch', 'yellow')} Batch process multiple files
48
- ${color('tui', 'magenta')} Launch Terminal User Interface (requires blessed)
49
- ${color('help', 'blue')} Show this help message
50
- ${color('version', 'blue')} Show version information
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
51
64
 
52
65
  ${color('EXAMPLES:', 'bright')}
53
66
  ${color('Convert JSON file to CSV:', 'dim')}
@@ -56,13 +69,25 @@ ${color('EXAMPLES:', 'bright')}
56
69
  ${color('Convert CSV file to JSON:', 'dim')}
57
70
  jtcsv csv-to-json input.csv output.json --parse-numbers --auto-detect
58
71
 
72
+ ${color('Save data as JSON file:', 'dim')}
73
+ jtcsv save-json data.json output.json --pretty
74
+
59
75
  ${color('Stream large JSON file to CSV:', 'dim')}
60
76
  jtcsv stream json-to-csv large.json output.csv --max-records=1000000
61
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
+
62
87
  ${color('Launch TUI interface:', 'dim')}
63
88
  jtcsv tui
64
89
 
65
- ${color('OPTIONS:', 'bright')}
90
+ ${color('CONVERSION OPTIONS:', 'bright')}
66
91
  ${color('--delimiter=', 'cyan')}CHAR CSV delimiter (default: ;)
67
92
  ${color('--auto-detect', 'cyan')} Auto-detect delimiter (default: true)
68
93
  ${color('--candidates=', 'cyan')}LIST Delimiter candidates (default: ;,\t|)
@@ -70,37 +95,71 @@ ${color('OPTIONS:', 'bright')}
70
95
  ${color('--parse-numbers', 'cyan')} Parse numeric values in CSV
71
96
  ${color('--parse-booleans', 'cyan')} Parse boolean values in CSV
72
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)
73
100
  ${color('--rename=', 'cyan')}JSON Rename columns (JSON map)
74
101
  ${color('--template=', 'cyan')}JSON Column order template (JSON object)
75
102
  ${color('--no-injection-protection', 'cyan')} Disable CSV injection protection
76
103
  ${color('--no-rfc4180', 'cyan')} Disable RFC 4180 compliance
77
- ${color('--max-records=', 'cyan')}N Maximum records to process (optional, no limit by default)
78
- ${color('--max-rows=', 'cyan')}N Maximum rows to process (optional, no limit by default)
104
+ ${color('--max-records=', 'cyan')}N Maximum records to process
105
+ ${color('--max-rows=', 'cyan')}N Maximum rows to process
79
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')}
80
128
  ${color('--silent', 'cyan')} Suppress all output except errors
81
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
82
132
 
83
133
  ${color('SECURITY FEATURES:', 'bright')}
84
134
  • CSV injection protection (enabled by default)
85
135
  • Path traversal protection
86
136
  • Input validation and sanitization
87
137
  • Size limits to prevent DoS attacks
138
+ • Schema validation support
88
139
 
89
- ${color('STREAMING SUPPORT:', 'bright')}
90
- Process files >100MB without loading into memory
91
- Real-time transformation with backpressure handling
92
- Schema validation during streaming
140
+ ${color('PERFORMANCE FEATURES:', 'bright')}
141
+ Streaming for files >100MB
142
+ Batch processing with parallel execution
143
+ Memory-efficient preprocessing
144
+ • Configurable buffer sizes
93
145
 
94
146
  ${color('LEARN MORE:', 'dim')}
95
147
  GitHub: https://github.com/Linol-Hamelton/jtcsv
96
148
  Issues: https://github.com/Linol-Hamelton/jtcsv/issues
149
+ Documentation: https://github.com/Linol-Hamelton/jtcsv#readme
97
150
  `);
98
151
  }
99
152
 
100
153
  function showVersion() {
101
154
  console.log(`jtcsv v${VERSION}`);
155
+ console.log(`Node.js ${process.version}`);
156
+ console.log(`Platform: ${process.platform} ${process.arch}`);
102
157
  }
103
158
 
159
+ // ============================================================================
160
+ // CONVERSION FUNCTIONS
161
+ // ============================================================================
162
+
104
163
  async function convertJsonToCsv(inputFile, outputFile, options) {
105
164
  const startTime = Date.now();
106
165
 
@@ -113,7 +172,9 @@ async function convertJsonToCsv(inputFile, outputFile, options) {
113
172
  throw new Error('JSON data must be an array of objects');
114
173
  }
115
174
 
116
- console.log(color(`Converting ${jsonData.length} records...`, 'dim'));
175
+ if (!options.silent) {
176
+ console.log(color(`Converting ${jsonData.length.toLocaleString()} records...`, 'dim'));
177
+ }
117
178
 
118
179
  // Prepare options for jtcsv
119
180
  const jtcsvOptions = {
@@ -133,11 +194,18 @@ async function convertJsonToCsv(inputFile, outputFile, options) {
133
194
  await fs.promises.writeFile(outputFile, csvData, 'utf8');
134
195
 
135
196
  const elapsed = Date.now() - startTime;
136
- console.log(color(`✓ Converted ${jsonData.length} records in ${elapsed}ms`, 'green'));
137
- console.log(color(` Output: ${outputFile} (${csvData.length} bytes)`, 'dim'));
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 };
138
203
 
139
204
  } catch (error) {
140
205
  console.error(color(`✗ Error: ${error.message}`, 'red'));
206
+ if (options.debug) {
207
+ console.error(error.stack);
208
+ }
141
209
  process.exit(1);
142
210
  }
143
211
  }
@@ -146,7 +214,9 @@ async function convertCsvToJson(inputFile, outputFile, options) {
146
214
  const startTime = Date.now();
147
215
 
148
216
  try {
149
- console.log(color('Reading CSV file...', 'dim'));
217
+ if (!options.silent) {
218
+ console.log(color('Reading CSV file...', 'dim'));
219
+ }
150
220
 
151
221
  // Prepare options for jtcsv
152
222
  const jtcsvOptions = {
@@ -158,7 +228,9 @@ async function convertCsvToJson(inputFile, outputFile, options) {
158
228
  trim: options.trim,
159
229
  parseNumbers: options.parseNumbers,
160
230
  parseBooleans: options.parseBooleans,
161
- maxRows: options.maxRows
231
+ maxRows: options.maxRows,
232
+ useFastPath: options.useFastPath,
233
+ fastPathMode: options.fastPathMode
162
234
  };
163
235
 
164
236
  // Read and convert CSV
@@ -173,59 +245,224 @@ async function convertCsvToJson(inputFile, outputFile, options) {
173
245
  await fs.promises.writeFile(outputFile, jsonOutput, 'utf8');
174
246
 
175
247
  const elapsed = Date.now() - startTime;
176
- console.log(color(`✓ Converted ${jsonData.length} rows in ${elapsed}ms`, 'green'));
177
- console.log(color(` Output: ${outputFile} (${jsonOutput.length} bytes)`, 'dim'));
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 };
178
347
 
179
348
  } catch (error) {
180
349
  console.error(color(`✗ Error: ${error.message}`, 'red'));
350
+ if (options.debug) {
351
+ console.error(error.stack);
352
+ }
181
353
  process.exit(1);
182
354
  }
183
355
  }
184
356
 
357
+ // ============================================================================
358
+ // STREAMING FUNCTIONS
359
+ // ============================================================================
360
+
185
361
  async function streamJsonToCsv(inputFile, outputFile, options) {
186
362
  const startTime = Date.now();
363
+ let recordCount = 0;
187
364
 
188
365
  try {
189
- console.log(color('Streaming conversion started...', 'dim'));
366
+ if (!options.silent) {
367
+ console.log(color('Streaming JSON to CSV...', 'dim'));
368
+ }
190
369
 
191
370
  // Create streams
192
371
  const readStream = fs.createReadStream(inputFile, 'utf8');
193
372
  const writeStream = fs.createWriteStream(outputFile, 'utf8');
194
373
 
195
- // For simplicity, we'll read line by line
196
- // In a real implementation, you would use a proper JSON stream parser
197
- let recordCount = 0;
374
+ // Add UTF-8 BOM if requested
375
+ if (options.addBOM) {
376
+ writeStream.write('\uFEFF');
377
+ }
378
+
379
+ // Parse JSON stream
198
380
  let buffer = '';
381
+ let isFirstChunk = true;
382
+ let headersWritten = false;
199
383
 
200
384
  readStream.on('data', (chunk) => {
201
385
  buffer += chunk;
202
386
 
203
- // Simple line-by-line processing for demonstration
387
+ // Try to parse complete JSON objects
204
388
  const lines = buffer.split('\n');
205
389
  buffer = lines.pop() || '';
206
390
 
207
- recordCount += lines.length;
208
- if (options.verbose && recordCount % 10000 === 0) {
209
- process.stdout.write(color(` Processed ${recordCount} records\r`, 'dim'));
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
+ }
210
427
  }
211
428
  });
212
429
 
213
430
  readStream.on('end', async () => {
214
431
  // Process remaining buffer
215
432
  if (buffer.trim()) {
216
- recordCount++;
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
+ }
217
454
  }
218
455
 
219
- // For this demo, we'll fall back to regular conversion
220
- const inputData = await fs.promises.readFile(inputFile, 'utf8');
221
- const jsonData = JSON.parse(inputData);
222
- const csvData = jtcsv.jsonToCsv(jsonData, options);
456
+ writeStream.end();
223
457
 
224
- await fs.promises.writeFile(outputFile, csvData, 'utf8');
458
+ // Wait for write stream to finish
459
+ await new Promise(resolve => writeStream.on('finish', resolve));
225
460
 
226
461
  const elapsed = Date.now() - startTime;
227
- console.log(color(`\n✓ Streamed ${recordCount} records in ${elapsed}ms`, 'green'));
228
- console.log(color(` Output: ${outputFile} (${csvData.length} bytes)`, 'dim'));
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
+ }
229
466
  });
230
467
 
231
468
  readStream.on('error', (error) => {
@@ -233,35 +470,387 @@ async function streamJsonToCsv(inputFile, outputFile, options) {
233
470
  process.exit(1);
234
471
  });
235
472
 
473
+ writeStream.on('error', (error) => {
474
+ console.error(color(`✗ Write error: ${error.message}`, 'red'));
475
+ process.exit(1);
476
+ });
477
+
236
478
  } catch (error) {
237
479
  console.error(color(`✗ Error: ${error.message}`, 'red'));
480
+ if (options.debug) {
481
+ console.error(error.stack);
482
+ }
238
483
  process.exit(1);
239
484
  }
240
485
  }
241
486
 
242
- async function launchTUI() {
487
+ async function streamCsvToJson(inputFile, outputFile, options) {
488
+ const startTime = Date.now();
489
+ let rowCount = 0;
490
+
243
491
  try {
244
- // Check if blessed is installed
245
- require.resolve('blessed');
492
+ if (!options.silent) {
493
+ console.log(color('Streaming CSV to JSON...', 'dim'));
494
+ }
246
495
 
247
- console.log(color('Launching Terminal User Interface...', 'cyan'));
248
- console.log(color('Press Ctrl+Q to exit', 'dim'));
496
+ // Create streams
497
+ const readStream = fs.createReadStream(inputFile, 'utf8');
498
+ const writeStream = fs.createWriteStream(outputFile, 'utf8');
249
499
 
250
- // Import and launch TUI
251
- const JtcsvTUI = require('../cli-tui.js');
252
- const tui = new JtcsvTUI();
253
- tui.start();
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
+ };
254
840
 
255
841
  } catch (error) {
256
- console.error(color('Error: blessed is required for TUI interface', 'red'));
257
- console.log(color('Install it with:', 'dim'));
258
- console.log(color(' npm install blessed blessed-contrib', 'cyan'));
259
- console.log(color('\nOr use the CLI interface instead:', 'dim'));
260
- console.log(color(' jtcsv help', 'cyan'));
842
+ console.error(color(`✗ Batch processing error: ${error.message}`, 'red'));
843
+ if (options.debug) {
844
+ console.error(error.stack);
845
+ }
261
846
  process.exit(1);
262
847
  }
263
848
  }
264
849
 
850
+ // ============================================================================
851
+ // OPTIONS PARSING
852
+ // ============================================================================
853
+
265
854
  function parseOptions(args) {
266
855
  const options = {
267
856
  delimiter: ';',
@@ -274,13 +863,30 @@ function parseOptions(args) {
274
863
  trim: true,
275
864
  parseNumbers: false,
276
865
  parseBooleans: false,
866
+ useFastPath: true,
867
+ fastPathMode: 'objects',
277
868
  preventCsvInjection: true,
278
869
  rfc4180Compliant: true,
279
870
  maxRecords: undefined,
280
871
  maxRows: undefined,
872
+ maxDepth: 5,
281
873
  pretty: false,
282
874
  silent: false,
283
- verbose: 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
284
890
  };
285
891
 
286
892
  const files = [];
@@ -294,7 +900,7 @@ function parseOptions(args) {
294
900
  switch (key) {
295
901
  case 'delimiter':
296
902
  options.delimiter = value || ',';
297
- options.autoDetect = false; // Disable auto-detect if delimiter is specified
903
+ options.autoDetect = false;
298
904
  break;
299
905
  case 'auto-detect':
300
906
  options.autoDetect = value !== 'false';
@@ -315,26 +921,34 @@ function parseOptions(args) {
315
921
  case 'no-trim':
316
922
  options.trim = false;
317
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;
318
936
  case 'rename':
319
937
  try {
320
- // Handle both quoted and unquoted JSON
321
938
  const jsonStr = value || '{}';
322
- // Remove surrounding single quotes if present
323
939
  const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
324
940
  options.renameMap = JSON.parse(cleanStr);
325
941
  } catch (e) {
326
- throw new Error(`Invalid JSON in --rename option: ${e.message}. Value: ${value}`);
942
+ throw new Error(`Invalid JSON in --rename option: ${e.message}`);
327
943
  }
328
944
  break;
329
945
  case 'template':
330
946
  try {
331
- // Handle both quoted and unquoted JSON
332
947
  const jsonStr = value || '{}';
333
- // Remove surrounding single quotes if present
334
948
  const cleanStr = jsonStr.replace(/^'|'$/g, '').replace(/^"|"$/g, '');
335
949
  options.template = JSON.parse(cleanStr);
336
950
  } catch (e) {
337
- throw new Error(`Invalid JSON in --template option: ${e.message}. Value: ${value}`);
951
+ throw new Error(`Invalid JSON in --template option: ${e.message}`);
338
952
  }
339
953
  break;
340
954
  case 'no-injection-protection':
@@ -349,6 +963,9 @@ function parseOptions(args) {
349
963
  case 'max-rows':
350
964
  options.maxRows = parseInt(value, 10);
351
965
  break;
966
+ case 'max-depth':
967
+ options.maxDepth = parseInt(value, 10) || 5;
968
+ break;
352
969
  case 'pretty':
353
970
  options.pretty = true;
354
971
  break;
@@ -358,6 +975,54 @@ function parseOptions(args) {
358
975
  case 'verbose':
359
976
  options.verbose = true;
360
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;
361
1026
  }
362
1027
  } else if (!arg.startsWith('-')) {
363
1028
  files.push(arg);
@@ -367,6 +1032,151 @@ function parseOptions(args) {
367
1032
  return { options, files };
368
1033
  }
369
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
+
370
1180
  async function main() {
371
1181
  const args = process.argv.slice(2);
372
1182
 
@@ -378,6 +1188,15 @@ async function main() {
378
1188
  const command = args[0].toLowerCase();
379
1189
  const { options, files } = parseOptions(args.slice(1));
380
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
+
381
1200
  // Suppress output if silent mode
382
1201
  if (options.silent) {
383
1202
  console.log = () => {};
@@ -385,63 +1204,147 @@ async function main() {
385
1204
  }
386
1205
 
387
1206
  switch (command) {
388
- case 'json-to-csv':
389
- case 'json2csv': // Backward compatibility
390
- if (files.length < 2) {
391
- console.error(color('Error: Input and output files required', 'red'));
392
- console.log(color('Usage: jtcsv json-to-csv input.json output.csv', 'cyan'));
393
- process.exit(1);
394
- }
395
- await convertJsonToCsv(files[0], files[1], options);
396
- break;
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;
397
1217
 
398
- case 'csv-to-json':
399
- case 'csv2json': // Backward compatibility
400
- if (files.length < 2) {
401
- console.error(color('Error: Input and output files required', 'red'));
402
- console.log(color('Usage: jtcsv csv-to-json input.csv output.json', 'cyan'));
403
- process.exit(1);
404
- }
405
- await convertCsvToJson(files[0], files[1], options);
406
- break;
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;
407
1227
 
408
- case 'stream':
409
- if (args.length < 2) {
410
- console.error(color('Error: Streaming mode requires subcommand', 'red'));
411
- console.log(color('Usage: jtcsv stream [json2csv|csv2json] input output', 'cyan'));
412
- process.exit(1);
413
- }
414
- const streamCommand = args[1].toLowerCase();
415
- if (streamCommand === 'json2csv' && files.length >= 2) {
416
- await streamJsonToCsv(files[0], files[1], options);
417
- } else {
418
- console.error(color('Error: Invalid streaming command', 'red'));
419
- process.exit(1);
420
- }
421
- break;
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;
422
1236
 
423
- case 'tui':
424
- await launchTUI();
425
- break;
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;
426
1245
 
427
- case 'help':
428
- showHelp();
429
- break;
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
+ }
430
1253
 
431
- case 'version':
432
- case '-v':
433
- case '--version':
434
- showVersion();
435
- break;
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;
436
1296
 
437
- default:
438
- console.error(color(`Error: Unknown command '${command}'`, 'red'));
439
- console.log(color('Use jtcsv help for available commands', 'cyan'));
440
- process.exit(1);
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);
441
1341
  }
442
1342
  }
443
1343
 
444
- // Handle uncaught errors
1344
+ // ============================================================================
1345
+ // ERROR HANDLING
1346
+ // ============================================================================
1347
+
445
1348
  process.on('uncaughtException', (error) => {
446
1349
  console.error(color(`\n✗ Uncaught error: ${error.message}`, 'red'));
447
1350
  if (process.env.DEBUG) {
@@ -466,11 +1369,4 @@ if (require.main === module) {
466
1369
  });
467
1370
  }
468
1371
 
469
- module.exports = { main };
470
-
471
-
472
-
473
-
474
-
475
-
476
-
1372
+ module.exports = { main };