jtcsv 2.2.8 → 3.1.0

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