dhti-cli 0.5.0 → 0.7.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.
@@ -0,0 +1,579 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import { exec } from 'node:child_process';
4
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
5
+ import { homedir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { createInterface } from 'node:readline';
8
+ import { promisify } from 'node:util';
9
+ const execAsync = promisify(exec);
10
+ /**
11
+ * Synthea command for managing synthetic FHIR data generation
12
+ *
13
+ * This command provides subcommands to:
14
+ * - install: Download and install Synthea JAR file
15
+ * - generate: Generate synthetic FHIR data using Synthea
16
+ * - upload: Upload generated FHIR resources to a FHIR server
17
+ * - delete: Clean up generated synthetic data
18
+ * - download: Download pre-generated Synthea datasets
19
+ */
20
+ export default class Synthea extends Command {
21
+ static args = {
22
+ subcommand: Args.string({
23
+ description: 'Subcommand to execute: install, generate, upload, delete, download',
24
+ options: ['install', 'generate', 'upload', 'delete', 'download'],
25
+ required: true,
26
+ }),
27
+ };
28
+ static description = 'Manage Synthea synthetic FHIR data generation';
29
+ static examples = [
30
+ '<%= config.bin %> <%= command.id %> install',
31
+ '<%= config.bin %> <%= command.id %> generate -p 10',
32
+ '<%= config.bin %> <%= command.id %> upload -e http://fhir:8005/baseR4',
33
+ '<%= config.bin %> <%= command.id %> delete',
34
+ '<%= config.bin %> <%= command.id %> download --covid19',
35
+ ];
36
+ static flags = {
37
+ // Generate flags
38
+ age: Flags.string({
39
+ char: 'a',
40
+ description: 'Generate patients with specific age range (e.g., "0-18" for pediatric)',
41
+ }),
42
+ city: Flags.string({
43
+ char: 'c',
44
+ description: 'City for patient generation',
45
+ }),
46
+ // Download flags - various datasets from synthea.mitre.org
47
+ covid19: Flags.boolean({
48
+ description: 'Download COVID-19 dataset (1k patients)',
49
+ }),
50
+ // eslint-disable-next-line camelcase
51
+ covid19_10k: Flags.boolean({
52
+ description: 'Download COVID-19 dataset (10k patients)',
53
+ }),
54
+ // eslint-disable-next-line camelcase
55
+ covid19_csv: Flags.boolean({
56
+ description: 'Download COVID-19 CSV dataset (1k patients)',
57
+ }),
58
+ // eslint-disable-next-line camelcase
59
+ covid19_csv_10k: Flags.boolean({
60
+ description: 'Download COVID-19 CSV dataset (10k patients)',
61
+ }),
62
+ // Common flags
63
+ 'dry-run': Flags.boolean({
64
+ default: false,
65
+ description: 'Show what changes would be made without actually making them',
66
+ }),
67
+ // Upload flags
68
+ endpoint: Flags.string({
69
+ char: 'e',
70
+ default: 'http://fhir:8005/baseR4',
71
+ description: 'FHIR server endpoint URL',
72
+ }),
73
+ gender: Flags.string({
74
+ char: 'g',
75
+ description: 'Generate patients of specific gender (M or F)',
76
+ options: ['M', 'F'],
77
+ }),
78
+ population: Flags.integer({
79
+ char: 'p',
80
+ default: 1,
81
+ description: 'Number of patients to generate',
82
+ }),
83
+ seed: Flags.string({
84
+ char: 's',
85
+ description: 'Random seed for reproducible generation',
86
+ }),
87
+ state: Flags.string({
88
+ description: 'State for patient generation (default: Massachusetts)',
89
+ }),
90
+ // eslint-disable-next-line camelcase
91
+ synthea_sample_data_csv_latest: Flags.boolean({
92
+ description: 'Download latest CSV sample data',
93
+ }),
94
+ // eslint-disable-next-line camelcase
95
+ synthea_sample_data_fhir_latest: Flags.boolean({
96
+ description: 'Download latest FHIR sample data',
97
+ }),
98
+ // eslint-disable-next-line camelcase
99
+ synthea_sample_data_fhir_stu3_latest: Flags.boolean({
100
+ description: 'Download latest FHIR STU3 sample data',
101
+ }),
102
+ token: Flags.string({
103
+ char: 't',
104
+ description: 'Bearer token for FHIR server authentication',
105
+ }),
106
+ workdir: Flags.string({
107
+ char: 'w',
108
+ default: join(homedir(), 'dhti'),
109
+ description: 'Working directory for Synthea files',
110
+ }),
111
+ };
112
+ /**
113
+ * Main command execution
114
+ * Dispatches to appropriate subcommand handler
115
+ * @returns Promise that resolves when subcommand completes
116
+ */
117
+ async run() {
118
+ const { args, flags } = await this.parse(Synthea);
119
+ // Execute appropriate subcommand
120
+ switch (args.subcommand) {
121
+ case 'install': {
122
+ await this.install(flags);
123
+ break;
124
+ }
125
+ case 'generate': {
126
+ await this.generate(flags);
127
+ break;
128
+ }
129
+ case 'upload': {
130
+ await this.upload(flags);
131
+ break;
132
+ }
133
+ case 'delete': {
134
+ await this.delete(flags);
135
+ break;
136
+ }
137
+ case 'download': {
138
+ await this.download(flags);
139
+ break;
140
+ }
141
+ default: {
142
+ this.error(`Unknown subcommand: ${args.subcommand}`);
143
+ }
144
+ }
145
+ }
146
+ /**
147
+ * Delete synthetic data
148
+ * @param flags Command flags including workdir and dry-run
149
+ * @returns Promise that resolves when deletion completes
150
+ */
151
+ async delete(flags) {
152
+ const dataDir = join(flags.workdir, 'synthea_data');
153
+ if (flags['dry-run']) {
154
+ console.log(chalk.yellow('[DRY RUN] Data deletion simulation'));
155
+ console.log(chalk.cyan(` Data directory: ${dataDir}`));
156
+ console.log(chalk.green('[DRY RUN] Would delete all files in synthea_data directory'));
157
+ return;
158
+ }
159
+ // Check if directory exists
160
+ if (!existsSync(dataDir)) {
161
+ console.log(chalk.yellow(`⚠ Directory does not exist: ${dataDir}`));
162
+ return;
163
+ }
164
+ // Count files
165
+ let fileCount = 0;
166
+ const countFiles = (dir) => {
167
+ const items = readdirSync(dir);
168
+ for (const item of items) {
169
+ const fullPath = join(dir, item);
170
+ const stat = statSync(fullPath);
171
+ if (stat.isDirectory()) {
172
+ countFiles(fullPath);
173
+ }
174
+ else {
175
+ fileCount++;
176
+ }
177
+ }
178
+ };
179
+ countFiles(dataDir);
180
+ console.log(chalk.yellow(`⚠ About to delete ${fileCount} files from: ${dataDir}`));
181
+ // Confirmation prompt
182
+ const rl = createInterface({
183
+ input: process.stdin,
184
+ output: process.stdout,
185
+ });
186
+ const answer = await new Promise((resolve) => {
187
+ rl.question(chalk.red('Are you sure you want to delete all data? (yes/N): '), resolve);
188
+ });
189
+ rl.close();
190
+ if (answer.toLowerCase() !== 'yes') {
191
+ console.log(chalk.blue('Deletion cancelled.'));
192
+ return;
193
+ }
194
+ // Delete directory
195
+ try {
196
+ rmSync(dataDir, { force: true, recursive: true });
197
+ console.log(chalk.green(`✓ Deleted: ${dataDir}`));
198
+ }
199
+ catch (error) {
200
+ this.error(`Failed to delete directory: ${error instanceof Error ? error.message : String(error)}`);
201
+ }
202
+ }
203
+ /**
204
+ * Download pre-generated Synthea datasets
205
+ * @param flags Command flags including workdir, dataset selections, and dry-run
206
+ * @returns Promise that resolves when download completes
207
+ */
208
+ async download(flags) {
209
+ const tmpDir = '/tmp/synthea_downloads';
210
+ const outputDir = join(flags.workdir, 'synthea_data');
211
+ // Map of dataset flags to download URLs
212
+ // eslint-disable-next-line camelcase
213
+ const datasets = {
214
+ covid19: {
215
+ file: 'covid19.zip',
216
+ url: 'https://synthea.mitre.org/downloads/covid19_1k.zip',
217
+ },
218
+ // eslint-disable-next-line camelcase
219
+ covid19_10k: {
220
+ file: 'covid19_10k.zip',
221
+ url: 'https://synthea.mitre.org/downloads/covid19_10k.zip',
222
+ },
223
+ // eslint-disable-next-line camelcase
224
+ covid19_csv: {
225
+ file: 'covid19_csv.zip',
226
+ url: 'https://synthea.mitre.org/downloads/covid19_csv_1k.zip',
227
+ },
228
+ // eslint-disable-next-line camelcase
229
+ covid19_csv_10k: {
230
+ file: 'covid19_csv_10k.zip',
231
+ url: 'https://synthea.mitre.org/downloads/covid19_csv_10k.zip',
232
+ },
233
+ // eslint-disable-next-line camelcase
234
+ synthea_sample_data_csv_latest: {
235
+ file: 'synthea_sample_data_csv_latest.zip',
236
+ url: 'https://synthea.mitre.org/downloads/synthea_sample_data_csv_latest.zip',
237
+ },
238
+ // eslint-disable-next-line camelcase
239
+ synthea_sample_data_fhir_latest: {
240
+ file: 'synthea_sample_data_fhir_latest.zip',
241
+ url: 'https://synthea.mitre.org/downloads/synthea_sample_data_fhir_latest.zip',
242
+ },
243
+ // eslint-disable-next-line camelcase
244
+ synthea_sample_data_fhir_stu3_latest: {
245
+ file: 'synthea_sample_data_fhir_stu3_latest.zip',
246
+ url: 'https://synthea.mitre.org/downloads/synthea_sample_data_fhir_stu3_latest.zip',
247
+ },
248
+ };
249
+ // Find which dataset to download
250
+ const selectedDatasets = Object.keys(datasets).filter((key) => flags[key]);
251
+ if (selectedDatasets.length === 0) {
252
+ if (flags['dry-run']) {
253
+ console.log(chalk.yellow('[DRY RUN] Dataset download simulation'));
254
+ console.log(chalk.yellow('⚠ No dataset selected. Use one of the following flags:'));
255
+ }
256
+ else {
257
+ console.log(chalk.yellow('⚠ No dataset selected. Use one of the following flags:'));
258
+ }
259
+ for (const [key] of Object.entries(datasets)) {
260
+ console.log(chalk.cyan(` --${key}`));
261
+ }
262
+ return;
263
+ }
264
+ if (flags['dry-run']) {
265
+ console.log(chalk.yellow('[DRY RUN] Dataset download simulation'));
266
+ console.log(chalk.cyan(` Temporary directory: ${tmpDir}`));
267
+ console.log(chalk.cyan(` Output directory: ${outputDir}`));
268
+ for (const dataset of selectedDatasets) {
269
+ console.log(chalk.cyan(` Dataset: ${dataset}`));
270
+ console.log(chalk.cyan(` URL: ${datasets[dataset].url}`));
271
+ }
272
+ console.log(chalk.green('[DRY RUN] Would download and extract selected datasets'));
273
+ return;
274
+ }
275
+ // Create directories
276
+ if (!existsSync(tmpDir)) {
277
+ mkdirSync(tmpDir, { recursive: true });
278
+ }
279
+ if (!existsSync(outputDir)) {
280
+ mkdirSync(outputDir, { recursive: true });
281
+ }
282
+ // Download and extract each selected dataset
283
+ // Note: Sequential processing is intentional to avoid overwhelming the server
284
+ // eslint-disable-next-line no-await-in-loop
285
+ for (const datasetKey of selectedDatasets) {
286
+ const dataset = datasets[datasetKey];
287
+ const downloadPath = join(tmpDir, dataset.file);
288
+ console.log(chalk.blue(`\nDownloading ${datasetKey}...`));
289
+ console.log(chalk.gray(`URL: ${dataset.url}`));
290
+ try {
291
+ // Download file
292
+ // eslint-disable-next-line no-await-in-loop
293
+ const response = await fetch(dataset.url);
294
+ if (!response.ok) {
295
+ throw new Error(`Failed to download: ${response.statusText}`);
296
+ }
297
+ const fileStream = createWriteStream(downloadPath);
298
+ // @ts-expect-error - ReadableStream types from fetch
299
+ const reader = response.body.getReader();
300
+ let downloadedBytes = 0;
301
+ const contentLength = Number.parseInt(response.headers.get('content-length') || '0', 10);
302
+ // eslint-disable-next-line no-constant-condition
303
+ while (true) {
304
+ // eslint-disable-next-line no-await-in-loop
305
+ const { done, value } = await reader.read();
306
+ if (done)
307
+ break;
308
+ downloadedBytes += value.length;
309
+ fileStream.write(value);
310
+ if (contentLength > 0) {
311
+ const progress = Math.round((downloadedBytes / contentLength) * 100);
312
+ process.stdout.write(`\rDownloading: ${progress}%`);
313
+ }
314
+ }
315
+ fileStream.end();
316
+ console.log('\n' + chalk.green(`✓ Downloaded ${dataset.file}`));
317
+ // Extract ZIP file
318
+ console.log(chalk.blue('Extracting...'));
319
+ // eslint-disable-next-line no-await-in-loop
320
+ await execAsync(`unzip -o "${downloadPath}" -d "${outputDir}"`);
321
+ console.log(chalk.green(`✓ Extracted to ${outputDir}`));
322
+ }
323
+ catch (error) {
324
+ console.log(chalk.red(`✗ Failed to download ${datasetKey}: ${error instanceof Error ? error.message : String(error)}`));
325
+ }
326
+ }
327
+ console.log(chalk.green(`\n✓ Download complete. Data available at: ${outputDir}`));
328
+ }
329
+ /**
330
+ * Generate synthetic FHIR data
331
+ * @param flags Command flags including population, state, city, gender, age, seed, workdir, and dry-run
332
+ * @returns Promise that resolves when generation completes
333
+ */
334
+ async generate(flags) {
335
+ const syntheaDir = join(flags.workdir, 'synthea');
336
+ const jarPath = join(syntheaDir, 'synthea-with-dependencies.jar');
337
+ const outputDir = join(flags.workdir, 'synthea_data');
338
+ if (flags['dry-run']) {
339
+ console.log(chalk.yellow('[DRY RUN] Synthetic data generation simulation'));
340
+ console.log(chalk.cyan(` Synthea JAR: ${jarPath}`));
341
+ console.log(chalk.cyan(` Output directory: ${outputDir}`));
342
+ console.log(chalk.cyan(` Population: ${flags.population} patients`));
343
+ if (flags.state)
344
+ console.log(chalk.cyan(` State: ${flags.state}`));
345
+ if (flags.city)
346
+ console.log(chalk.cyan(` City: ${flags.city}`));
347
+ if (flags.gender)
348
+ console.log(chalk.cyan(` Gender: ${flags.gender}`));
349
+ if (flags.age)
350
+ console.log(chalk.cyan(` Age range: ${flags.age}`));
351
+ if (flags.seed)
352
+ console.log(chalk.cyan(` Random seed: ${flags.seed}`));
353
+ console.log(chalk.green('[DRY RUN] Would create output directory'));
354
+ console.log(chalk.green('[DRY RUN] Would execute Synthea JAR to generate data'));
355
+ return;
356
+ }
357
+ // Check if JAR exists
358
+ if (!existsSync(jarPath)) {
359
+ console.log(chalk.red(`✗ Synthea JAR not found at: ${jarPath}\nRun 'dhti-cli synthea install' first.`));
360
+ this.exit(1);
361
+ }
362
+ // Create output directory
363
+ if (!existsSync(outputDir)) {
364
+ mkdirSync(outputDir, { recursive: true });
365
+ console.log(chalk.green(`✓ Created output directory: ${outputDir}`));
366
+ }
367
+ // Build Synthea command
368
+ const javaArgs = ['-jar', jarPath];
369
+ // Add optional flags
370
+ if (flags.population)
371
+ javaArgs.push('-p', String(flags.population));
372
+ if (flags.state)
373
+ javaArgs.push('-s', flags.state);
374
+ if (flags.city)
375
+ javaArgs.push('-c', flags.city);
376
+ if (flags.gender)
377
+ javaArgs.push('-g', flags.gender);
378
+ if (flags.age)
379
+ javaArgs.push('-a', flags.age);
380
+ if (flags.seed)
381
+ javaArgs.push('--seed', flags.seed);
382
+ // Set output directory
383
+ javaArgs.push('--exporter.baseDirectory', outputDir);
384
+ console.log(chalk.blue('Generating synthetic data...'));
385
+ console.log(chalk.gray(`Command: java ${javaArgs.join(' ')}`));
386
+ try {
387
+ const { stderr, stdout } = await execAsync(`java ${javaArgs.join(' ')}`, {
388
+ cwd: syntheaDir,
389
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
390
+ });
391
+ if (stdout)
392
+ console.log(stdout);
393
+ if (stderr)
394
+ console.error(chalk.yellow(stderr));
395
+ console.log(chalk.green(`✓ Generated synthetic data in: ${outputDir}`));
396
+ // Show FHIR output location
397
+ const fhirDir = join(outputDir, 'fhir');
398
+ if (existsSync(fhirDir)) {
399
+ const files = readdirSync(fhirDir);
400
+ console.log(chalk.cyan(`\nGenerated ${files.length} FHIR resource files`));
401
+ console.log(chalk.white(`FHIR files location: ${fhirDir}`));
402
+ }
403
+ }
404
+ catch (error) {
405
+ this.error(`Failed to generate synthetic data: ${error instanceof Error ? error.message : String(error)}`);
406
+ }
407
+ }
408
+ /**
409
+ * Install Synthea JAR file
410
+ * @param flags Command flags including workdir and dry-run
411
+ * @returns Promise that resolves when installation completes
412
+ */
413
+ async install(flags) {
414
+ const syntheaDir = join(flags.workdir, 'synthea');
415
+ const jarPath = join(syntheaDir, 'synthea-with-dependencies.jar');
416
+ if (flags['dry-run']) {
417
+ console.log(chalk.yellow('[DRY RUN] Synthea installation simulation'));
418
+ console.log(chalk.cyan(` Working directory: ${flags.workdir}`));
419
+ console.log(chalk.cyan(` Synthea directory: ${syntheaDir}`));
420
+ console.log(chalk.cyan(` JAR path: ${jarPath}`));
421
+ console.log(chalk.green('[DRY RUN] Would create synthea directory'));
422
+ console.log(chalk.green('[DRY RUN] Would download synthea-with-dependencies.jar'));
423
+ console.log(chalk.green('[DRY RUN] Would display usage instructions'));
424
+ return;
425
+ }
426
+ // Create synthea directory
427
+ if (!existsSync(syntheaDir)) {
428
+ mkdirSync(syntheaDir, { recursive: true });
429
+ console.log(chalk.green(`✓ Created directory: ${syntheaDir}`));
430
+ }
431
+ // Check if JAR already exists
432
+ if (existsSync(jarPath)) {
433
+ console.log(chalk.yellow(`⚠ Synthea JAR already exists at: ${jarPath}`));
434
+ const rl = createInterface({
435
+ input: process.stdin,
436
+ output: process.stdout,
437
+ });
438
+ const answer = await new Promise((resolve) => {
439
+ rl.question('Overwrite existing file? (y/N): ', resolve);
440
+ });
441
+ rl.close();
442
+ if (answer.toLowerCase() !== 'y') {
443
+ console.log(chalk.blue('Installation cancelled.'));
444
+ return;
445
+ }
446
+ }
447
+ // Download synthea-with-dependencies.jar
448
+ console.log(chalk.blue('Downloading synthea-with-dependencies.jar...'));
449
+ const downloadUrl = 'https://github.com/synthetichealth/synthea/releases/download/master-branch-latest/synthea-with-dependencies.jar';
450
+ try {
451
+ const response = await fetch(downloadUrl);
452
+ if (!response.ok) {
453
+ throw new Error(`Failed to download: ${response.statusText}`);
454
+ }
455
+ const fileStream = createWriteStream(jarPath);
456
+ // @ts-expect-error - ReadableStream types from fetch
457
+ const reader = response.body.getReader();
458
+ let downloadedBytes = 0;
459
+ const contentLength = Number.parseInt(response.headers.get('content-length') || '0', 10);
460
+ // eslint-disable-next-line no-constant-condition
461
+ while (true) {
462
+ // eslint-disable-next-line no-await-in-loop
463
+ const { done, value } = await reader.read();
464
+ if (done)
465
+ break;
466
+ downloadedBytes += value.length;
467
+ fileStream.write(value);
468
+ if (contentLength > 0) {
469
+ const progress = Math.round((downloadedBytes / contentLength) * 100);
470
+ process.stdout.write(`\rDownloading: ${progress}%`);
471
+ }
472
+ }
473
+ fileStream.end();
474
+ console.log('\n' + chalk.green(`✓ Downloaded synthea-with-dependencies.jar to ${jarPath}`));
475
+ }
476
+ catch (error) {
477
+ this.error(`Failed to download Synthea JAR: ${error instanceof Error ? error.message : String(error)}`);
478
+ }
479
+ // Display usage instructions
480
+ console.log(chalk.cyan('\n' + '='.repeat(60)));
481
+ console.log(chalk.bold.green('Synthea Installation Complete!'));
482
+ console.log(chalk.cyan('='.repeat(60)));
483
+ console.log(chalk.white('\nUsage Instructions:'));
484
+ console.log(chalk.white('-------------------'));
485
+ console.log(chalk.white('To generate synthetic data:'));
486
+ console.log(chalk.yellow(` ${this.config.bin} synthea generate -p 10`));
487
+ console.log(chalk.white('\nTo upload data to FHIR server:'));
488
+ console.log(chalk.yellow(` ${this.config.bin} synthea upload -e http://fhir:8005/baseR4`));
489
+ console.log(chalk.white('\nManual usage:'));
490
+ console.log(chalk.yellow(` cd ${syntheaDir}`));
491
+ console.log(chalk.yellow(' java -jar synthea-with-dependencies.jar -p 10'));
492
+ console.log(chalk.white('\nFor more options, see:'));
493
+ console.log(chalk.blue(' https://github.com/synthetichealth/synthea/wiki/Basic-Setup-and-Running'));
494
+ console.log(chalk.cyan('='.repeat(60) + '\n'));
495
+ }
496
+ /**
497
+ * Upload FHIR resources to server
498
+ * @param flags Command flags including endpoint, token, workdir, and dry-run
499
+ * @returns Promise that resolves when upload completes
500
+ */
501
+ async upload(flags) {
502
+ const fhirDir = join(flags.workdir, 'synthea_data', 'fhir');
503
+ if (flags['dry-run']) {
504
+ console.log(chalk.yellow('[DRY RUN] FHIR upload simulation'));
505
+ console.log(chalk.cyan(` FHIR directory: ${fhirDir}`));
506
+ console.log(chalk.cyan(` Endpoint: ${flags.endpoint}`));
507
+ if (flags.token)
508
+ console.log(chalk.cyan(' Authentication: Bearer token'));
509
+ console.log(chalk.green('[DRY RUN] Would read FHIR resources from directory'));
510
+ console.log(chalk.green('[DRY RUN] Would upload each resource to FHIR server'));
511
+ return;
512
+ }
513
+ // Check if FHIR directory exists
514
+ if (!existsSync(fhirDir)) {
515
+ console.log(chalk.red(`✗ FHIR data directory not found: ${fhirDir}\nRun 'dhti-cli synthea generate' first.`));
516
+ this.exit(1);
517
+ }
518
+ // Read all JSON files from FHIR directory
519
+ const files = readdirSync(fhirDir).filter((f) => f.endsWith('.json'));
520
+ if (files.length === 0) {
521
+ console.log(chalk.yellow('⚠ No FHIR JSON files found in directory'));
522
+ return;
523
+ }
524
+ console.log(chalk.blue(`Found ${files.length} FHIR resource files`));
525
+ // Prepare headers
526
+ const headers = {
527
+ 'Content-Type': 'application/fhir+json',
528
+ };
529
+ if (flags.token) {
530
+ headers.Authorization = `Bearer ${flags.token}`;
531
+ }
532
+ let successCount = 0;
533
+ let failCount = 0;
534
+ // Upload each file
535
+ // Note: Sequential processing is intentional to maintain order and avoid overwhelming server
536
+ // eslint-disable-next-line no-await-in-loop
537
+ for (const [index, file] of files.entries()) {
538
+ const filePath = join(fhirDir, file);
539
+ console.log(chalk.gray(`[${index + 1}/${files.length}] Uploading ${file}...`));
540
+ try {
541
+ const content = readFileSync(filePath, 'utf8');
542
+ const resource = JSON.parse(content);
543
+ // Determine resource type and construct URL
544
+ const { resourceType } = resource;
545
+ if (!resourceType) {
546
+ console.log(chalk.yellow(` ⚠ Skipping ${file} - no resourceType`));
547
+ continue;
548
+ }
549
+ const url = `${flags.endpoint}/${resourceType}`;
550
+ // eslint-disable-next-line no-await-in-loop
551
+ const response = await fetch(url, {
552
+ body: content,
553
+ headers,
554
+ method: 'POST',
555
+ });
556
+ if (response.ok) {
557
+ successCount++;
558
+ console.log(chalk.green(` ✓ Uploaded ${file}`));
559
+ }
560
+ else {
561
+ failCount++;
562
+ console.log(chalk.red(` ✗ Failed to upload ${file}: ${response.status} ${response.statusText}`));
563
+ }
564
+ }
565
+ catch (error) {
566
+ failCount++;
567
+ console.log(chalk.red(` ✗ Error uploading ${file}: ${error instanceof Error ? error.message : String(error)}`));
568
+ }
569
+ }
570
+ // Summary
571
+ console.log(chalk.cyan('\n' + '='.repeat(60)));
572
+ console.log(chalk.bold.white('Upload Summary'));
573
+ console.log(chalk.cyan('='.repeat(60)));
574
+ console.log(chalk.green(` ✓ Successful: ${successCount}`));
575
+ console.log(chalk.red(` ✗ Failed: ${failCount}`));
576
+ console.log(chalk.white(` Total: ${files.length}`));
577
+ console.log(chalk.cyan('='.repeat(60) + '\n'));
578
+ }
579
+ }