@gefyra/diffyr6-cli 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.js CHANGED
@@ -6,7 +6,7 @@ import { pathExists } from './utils/fs.js';
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
8
 
9
- export const CONFIG_VERSION = '1.0.0';
9
+ export const CONFIG_VERSION = '1.0.1';
10
10
 
11
11
  /**
12
12
  * Default configuration values
@@ -24,6 +24,7 @@ export const DEFAULT_CONFIG = {
24
24
  validatorJarPath: null,
25
25
  workdir: null,
26
26
  compareMode: 'incremental',
27
+ exportZip: true,
27
28
  };
28
29
 
29
30
  /**
@@ -94,6 +95,10 @@ function validateConfig(config) {
94
95
  if (config.compareMode && !['incremental', 'full'].includes(config.compareMode)) {
95
96
  errors.push('compareMode must be either "incremental" or "full"');
96
97
  }
98
+
99
+ if (typeof config.exportZip !== 'boolean') {
100
+ errors.push('exportZip must be a boolean');
101
+ }
97
102
 
98
103
  if (errors.length > 0) {
99
104
  throw new Error(`Invalid configuration:\n${errors.map(e => ` - ${e}`).join('\n')}`);
@@ -106,7 +111,7 @@ function validateConfig(config) {
106
111
  export async function createExampleConfig(outputPath) {
107
112
  const example = {
108
113
  configVersion: CONFIG_VERSION,
109
- packageId: 'de.basisprofil.r4#1.5.0',
114
+ packageId: 'de.basisprofil.r4',
110
115
  packageVersion: '1.5.0',
111
116
  enableGoFSH: true,
112
117
  resourcesDir: 'Resources',
@@ -116,7 +121,8 @@ export async function createExampleConfig(outputPath) {
116
121
  rulesConfigPath: null,
117
122
  validatorJarPath: null,
118
123
  workdir: null,
119
- compareMode: 'incremental'
124
+ compareMode: 'incremental',
125
+ exportZip: true
120
126
  };
121
127
 
122
128
  await fsp.writeFile(
package/src/index.js CHANGED
@@ -7,7 +7,9 @@ import { spawnProcess } from './utils/process.js';
7
7
  import { generateFshFromPackage } from './generate-fsh.js';
8
8
  import { upgradeSushiToR6 } from './upgrade-sushi.js';
9
9
  import { compareProfiles } from './compare-profiles.js';
10
+ import { compareTerminology } from './compare-terminology.js';
10
11
  import { findRemovedResources } from './utils/removed-resources.js';
12
+ import { createZip } from './utils/zip.js';
11
13
 
12
14
  /**
13
15
  * Main entry point - runs the FHIR R4 to R6 migration pipeline
@@ -37,46 +39,69 @@ export async function runMigration(config) {
37
39
  if (config.enableGoFSH) {
38
40
  const shouldRunGoFSH = await checkShouldRunGoFSH(resourcesDir);
39
41
  if (shouldRunGoFSH) {
40
- console.log('\n[1/4] Downloading package and generating FSH...');
42
+ console.log('\n[1/5] Downloading package and generating FSH...');
41
43
  await runGoFSH(context);
42
44
  context.steps.push('gofsh');
43
45
  } else {
44
- console.log('\n[1/4] GoFSH - SKIPPED (Resources directory with sushi-config.yaml already exists)');
46
+ console.log('\n[1/5] GoFSH - SKIPPED (Resources directory with sushi-config.yaml already exists)');
45
47
  }
46
48
  } else {
47
- console.log('\n[1/4] GoFSH - DISABLED in config');
49
+ console.log('\n[1/5] GoFSH - DISABLED in config');
48
50
  }
49
51
 
50
52
  // Step 2: Upgrade to R6
51
53
  const shouldRunUpgrade = await checkShouldRunUpgrade(resourcesR6Dir);
52
54
  if (shouldRunUpgrade) {
53
- console.log('\n[2/4] Upgrading to R6...');
55
+ console.log('\n[2/5] Upgrading to R6...');
54
56
  await runUpgradeToR6(context);
55
57
  context.steps.push('upgrade');
56
58
  } else {
57
- console.log('\n[2/4] Upgrade - SKIPPED (ResourcesR6 directory with sushi-config.yaml already exists)');
59
+ console.log('\n[2/5] Upgrade - SKIPPED (ResourcesR6 directory with sushi-config.yaml already exists)');
58
60
  }
59
61
 
60
62
  // Step 3: Compare profiles
61
- console.log('\n[3/4] Comparing R4 vs R6 profiles...');
63
+ console.log('\n[3/5] Comparing R4 vs R6 profiles...');
62
64
  const compareResults = await runProfileComparison(context);
63
65
  context.steps.push('compare');
64
66
 
65
- // Step 4: Generate report with rules
66
- console.log('\n[4/4] Generating migration report...');
67
+ // Step 4: Generat5] Generating migration report...');
67
68
  const removedResources = await findRemovedResources(resourcesDir);
68
69
  const report = await generateReport(context, compareResults, removedResources);
69
70
  context.steps.push('report');
71
+
72
+ // Step 5: Compare terminology bindings
73
+ console.log('\n[5/5] Comparing terminology bindings...');
74
+ let terminologyReport = null;
75
+ try {
76
+ terminologyReport = await runTerminologyComparison(context);
77
+ if (terminologyReport) {
78
+ context.steps.push('terminology');
79
+ }
80
+ } catch (error) {
81
+ console.warn(` Terminology comparison failed: ${error.message}`);
82
+ console.warn(' Continuing without terminology report...');
83
+ }
84
+
85
+ let exportZipPath = null;
86
+ if (config.exportZip) {
87
+ console.log('\nGenerating export ZIP...');
88
+ exportZipPath = await exportComparisonZip(context, report, terminologyReport);
89
+ context.steps.push('exportZip');
90
+ }
70
91
 
71
92
  console.log(`\n✓ Migration complete!`);
72
93
  console.log(` Report: ${report.path}`);
73
94
  console.log(` Total Score: ${report.score}`);
74
95
  console.log(` Findings: ${report.findingsCount}`);
96
+ if (exportZipPath) {
97
+ console.log(` Export ZIP: ${exportZipPath}`);
98
+ }
75
99
 
76
100
  return {
77
101
  success: true,
78
102
  steps: context.steps,
79
103
  report: report.path,
104
+ exportZip: exportZipPath,
80
105
  score: report.score,
81
106
  findingsCount: report.findingsCount,
82
107
  };
@@ -141,6 +166,28 @@ async function runProfileComparison(context) {
141
166
  return [];
142
167
  }
143
168
 
169
+ /**
170
+ * Run terminology comparison
171
+ */
172
+ async function runTerminologyComparison(context) {
173
+ const { resourcesDir, resourcesR6Dir, outputDir, config } = context;
174
+
175
+ const options = {
176
+ debug: config.debug || false,
177
+ };
178
+
179
+ const result = await compareTerminology(resourcesDir, resourcesR6Dir, outputDir, options);
180
+
181
+ if (result) {
182
+ console.log(` ${result.profilesWithDifferences} profile(s) with binding differences`);
183
+ console.log(` Total findings: ${result.totalFindings}`);
184
+ console.log(` Markdown report: ${result.path}`);
185
+ console.log(` JSON report: ${result.jsonPath}`);
186
+ }
187
+
188
+ return result;
189
+ }
190
+
144
191
  /**
145
192
  * Get list of profiles that need to be compared
146
193
  */
@@ -233,11 +280,87 @@ async function generateReport(context, compareResults, removedResources = []) {
233
280
 
234
281
  return {
235
282
  path: reportPath,
283
+ filename: reportFilename,
284
+ timestamp,
236
285
  score: totalScore,
237
286
  findingsCount: findings.length,
238
287
  };
239
288
  }
240
289
 
290
+ /**
291
+ * Create a ZIP export with compare HTML files, report, and run config
292
+ */
293
+ async function exportComparisonZip(context, report, terminologyReport = null) {
294
+ const { compareDir, outputDir, config } = context;
295
+ const exportFilename = 'diffyr6-publish.zip';
296
+ const exportPath = path.join(outputDir, exportFilename);
297
+
298
+ const entries = [];
299
+
300
+ // Add HTML comparison files sent to the API
301
+ const htmlFiles = await listExportHtmlFiles(compareDir);
302
+ for (const file of htmlFiles) {
303
+ const filePath = path.join(compareDir, file);
304
+ const content = await fsp.readFile(filePath);
305
+ entries.push({
306
+ name: file,
307
+ data: content,
308
+ mtime: (await fsp.stat(filePath)).mtime,
309
+ });
310
+ }
311
+
312
+ // Add markdown report
313
+ const reportContent = await fsp.readFile(report.path);
314
+ entries.push({
315
+ name: report.filename,
316
+ data: reportContent,
317
+ mtime: (await fsp.stat(report.path)).mtime,
318
+ });
319
+
320
+ // Add terminology report if available
321
+ if (terminologyReport && terminologyReport.path) {
322
+ const termContent = await fsp.readFile(terminologyReport.path);
323
+ entries.push({
324
+ name: terminologyReport.filename,
325
+ data: termContent,
326
+ mtime: (await fsp.stat(terminologyReport.path)).mtime,
327
+ });
328
+
329
+ // Add terminology JSON if available
330
+ if (terminologyReport.jsonPath) {
331
+ const termJsonContent = await fsp.readFile(terminologyReport.jsonPath);
332
+ entries.push({
333
+ name: terminologyReport.jsonFilename,
334
+ data: termJsonContent,
335
+ mtime: (await fsp.stat(terminologyReport.jsonPath)).mtime,
336
+ });
337
+ }
338
+ }
339
+
340
+ // Add config used for the run
341
+ entries.push({
342
+ name: 'run-config.json',
343
+ data: JSON.stringify(config, null, 2),
344
+ mtime: new Date(),
345
+ });
346
+
347
+ await createZip(exportPath, entries);
348
+ return exportPath;
349
+ }
350
+
351
+ async function listExportHtmlFiles(compareDir) {
352
+ const exists = await directoryExists(compareDir);
353
+ if (!exists) {
354
+ return [];
355
+ }
356
+ const files = await fsp.readdir(compareDir);
357
+ const allowed = /^(sd|xx)-.+-.+\.html$/i;
358
+ const excluded = /(intersection|union)\.html$/i;
359
+ return files
360
+ .filter(file => allowed.test(file) && !excluded.test(file))
361
+ .sort();
362
+ }
363
+
241
364
  /**
242
365
  * Read all HTML comparison files
243
366
  */
@@ -0,0 +1,112 @@
1
+ import fsp from 'fs/promises';
2
+
3
+ const CRC32_TABLE = (() => {
4
+ const table = new Uint32Array(256);
5
+ for (let i = 0; i < 256; i += 1) {
6
+ let c = i;
7
+ for (let k = 0; k < 8; k += 1) {
8
+ c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
9
+ }
10
+ table[i] = c >>> 0;
11
+ }
12
+ return table;
13
+ })();
14
+
15
+ function crc32(buffer) {
16
+ let crc = 0xFFFFFFFF;
17
+ for (let i = 0; i < buffer.length; i += 1) {
18
+ const byte = buffer[i];
19
+ crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ byte) & 0xFF];
20
+ }
21
+ return (crc ^ 0xFFFFFFFF) >>> 0;
22
+ }
23
+
24
+ function toDosDateTime(date) {
25
+ const d = date instanceof Date ? date : new Date(date);
26
+ let year = d.getFullYear();
27
+ if (year < 1980) {
28
+ year = 1980;
29
+ }
30
+ const month = d.getMonth() + 1;
31
+ const day = d.getDate();
32
+ const hours = d.getHours();
33
+ const minutes = d.getMinutes();
34
+ const seconds = Math.floor(d.getSeconds() / 2);
35
+ const dosTime = (hours << 11) | (minutes << 5) | seconds;
36
+ const dosDate = ((year - 1980) << 9) | (month << 5) | day;
37
+ return { dosTime, dosDate };
38
+ }
39
+
40
+ export async function createZip(outputPath, entries) {
41
+ const fileParts = [];
42
+ const centralParts = [];
43
+ let offset = 0;
44
+
45
+ for (const entry of entries) {
46
+ const name = entry.name.replace(/\\/g, '/');
47
+ const nameBuffer = Buffer.from(name, 'utf8');
48
+ const dataBuffer = Buffer.isBuffer(entry.data)
49
+ ? entry.data
50
+ : Buffer.from(entry.data, 'utf8');
51
+ const { dosTime, dosDate } = toDosDateTime(entry.mtime || new Date());
52
+ const crc = crc32(dataBuffer);
53
+ const size = dataBuffer.length;
54
+
55
+ const localHeader = Buffer.alloc(30 + nameBuffer.length);
56
+ let p = 0;
57
+ localHeader.writeUInt32LE(0x04034b50, p); p += 4; // Local file header signature
58
+ localHeader.writeUInt16LE(20, p); p += 2; // Version needed
59
+ localHeader.writeUInt16LE(0, p); p += 2; // Flags
60
+ localHeader.writeUInt16LE(0, p); p += 2; // Compression (store)
61
+ localHeader.writeUInt16LE(dosTime, p); p += 2;
62
+ localHeader.writeUInt16LE(dosDate, p); p += 2;
63
+ localHeader.writeUInt32LE(crc, p); p += 4;
64
+ localHeader.writeUInt32LE(size, p); p += 4;
65
+ localHeader.writeUInt32LE(size, p); p += 4;
66
+ localHeader.writeUInt16LE(nameBuffer.length, p); p += 2;
67
+ localHeader.writeUInt16LE(0, p); p += 2; // Extra length
68
+ nameBuffer.copy(localHeader, p);
69
+
70
+ fileParts.push(localHeader, dataBuffer);
71
+
72
+ const centralHeader = Buffer.alloc(46 + nameBuffer.length);
73
+ p = 0;
74
+ centralHeader.writeUInt32LE(0x02014b50, p); p += 4; // Central dir signature
75
+ centralHeader.writeUInt16LE(20, p); p += 2; // Version made by
76
+ centralHeader.writeUInt16LE(20, p); p += 2; // Version needed
77
+ centralHeader.writeUInt16LE(0, p); p += 2; // Flags
78
+ centralHeader.writeUInt16LE(0, p); p += 2; // Compression
79
+ centralHeader.writeUInt16LE(dosTime, p); p += 2;
80
+ centralHeader.writeUInt16LE(dosDate, p); p += 2;
81
+ centralHeader.writeUInt32LE(crc, p); p += 4;
82
+ centralHeader.writeUInt32LE(size, p); p += 4;
83
+ centralHeader.writeUInt32LE(size, p); p += 4;
84
+ centralHeader.writeUInt16LE(nameBuffer.length, p); p += 2;
85
+ centralHeader.writeUInt16LE(0, p); p += 2; // Extra length
86
+ centralHeader.writeUInt16LE(0, p); p += 2; // Comment length
87
+ centralHeader.writeUInt16LE(0, p); p += 2; // Disk number
88
+ centralHeader.writeUInt16LE(0, p); p += 2; // Internal attributes
89
+ centralHeader.writeUInt32LE(0, p); p += 4; // External attributes
90
+ centralHeader.writeUInt32LE(offset, p); p += 4; // Local header offset
91
+ nameBuffer.copy(centralHeader, p);
92
+
93
+ centralParts.push(centralHeader);
94
+ offset += localHeader.length + dataBuffer.length;
95
+ }
96
+
97
+ const centralDirSize = centralParts.reduce((sum, buf) => sum + buf.length, 0);
98
+ const centralDirOffset = offset;
99
+ const endRecord = Buffer.alloc(22);
100
+ let e = 0;
101
+ endRecord.writeUInt32LE(0x06054b50, e); e += 4; // End of central dir signature
102
+ endRecord.writeUInt16LE(0, e); e += 2; // Disk number
103
+ endRecord.writeUInt16LE(0, e); e += 2; // Central dir start disk
104
+ endRecord.writeUInt16LE(entries.length, e); e += 2; // Entries on disk
105
+ endRecord.writeUInt16LE(entries.length, e); e += 2; // Total entries
106
+ endRecord.writeUInt32LE(centralDirSize, e); e += 4;
107
+ endRecord.writeUInt32LE(centralDirOffset, e); e += 4;
108
+ endRecord.writeUInt16LE(0, e); e += 2; // Comment length
109
+
110
+ const buffer = Buffer.concat([...fileParts, ...centralParts, endRecord]);
111
+ await fsp.writeFile(outputPath, buffer);
112
+ }