@gefyra/diffyr6-cli 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,7 +68,8 @@ This creates a `migration-config.json` file with default settings.
68
68
  "rulesConfigPath": null,
69
69
  "validatorJarPath": null,
70
70
  "workdir": null,
71
- "compareMode": "incremental"
71
+ "compareMode": "incremental",
72
+ "exportZip": true
72
73
  }
73
74
  ```
74
75
 
@@ -159,6 +160,7 @@ console.log('Findings:', result.findingsCount);
159
160
  | `validatorJarPath` | string | `null` | Path to validator_cli.jar (auto-downloads latest from GitHub if null) |
160
161
  | `workdir` | string | `null` | Working directory (uses current dir if null) |
161
162
  | `compareMode` | string | `"incremental"` | Comparison mode: `"incremental"` or `"full"` |
163
+ | `exportZip` | boolean | `true` | Create a ZIP export containing compare HTML, markdown report, and run config |
162
164
 
163
165
  **Auto-download feature:** When `validatorJarPath` is `null`, the HL7 FHIR Validator will be automatically downloaded from a [stable GitHub release](https://github.com/hapifhir/org.hl7.fhir.core/releases/download/6.7.10/validator_cli.jar) to `<workdir>/validator_cli.jar`. This download only happens once - subsequent runs will reuse the existing JAR file.
164
166
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gefyra/diffyr6-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "FHIR R4 to R6 migration pipeline runner with automated profile comparison and rule-based analysis",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/cli.js CHANGED
@@ -4,9 +4,11 @@ import { runMigration } from './index.js';
4
4
  import { loadConfig, createExampleConfig } from './config.js';
5
5
  import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
+ import { createRequire } from 'module';
7
8
 
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = path.dirname(__filename);
11
+ const require = createRequire(import.meta.url);
10
12
 
11
13
  async function main() {
12
14
  const args = process.argv.slice(2);
@@ -19,8 +21,8 @@ async function main() {
19
21
 
20
22
  // Handle --version
21
23
  if (args.includes('--version') || args.includes('-v')) {
22
- const pkg = await import('../package.json', { assert: { type: 'json' } });
23
- console.log(pkg.default.version);
24
+ const pkg = require('../package.json');
25
+ console.log(pkg.version);
24
26
  return;
25
27
  }
26
28
 
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
@@ -8,6 +8,7 @@ import { generateFshFromPackage } from './generate-fsh.js';
8
8
  import { upgradeSushiToR6 } from './upgrade-sushi.js';
9
9
  import { compareProfiles } from './compare-profiles.js';
10
10
  import { findRemovedResources } from './utils/removed-resources.js';
11
+ import { createZip } from './utils/zip.js';
11
12
 
12
13
  /**
13
14
  * Main entry point - runs the FHIR R4 to R6 migration pipeline
@@ -67,16 +68,27 @@ export async function runMigration(config) {
67
68
  const removedResources = await findRemovedResources(resourcesDir);
68
69
  const report = await generateReport(context, compareResults, removedResources);
69
70
  context.steps.push('report');
71
+
72
+ let exportZipPath = null;
73
+ if (config.exportZip) {
74
+ console.log('\nGenerating export ZIP...');
75
+ exportZipPath = await exportComparisonZip(context, report);
76
+ context.steps.push('exportZip');
77
+ }
70
78
 
71
79
  console.log(`\n✓ Migration complete!`);
72
80
  console.log(` Report: ${report.path}`);
73
81
  console.log(` Total Score: ${report.score}`);
74
82
  console.log(` Findings: ${report.findingsCount}`);
83
+ if (exportZipPath) {
84
+ console.log(` Export ZIP: ${exportZipPath}`);
85
+ }
75
86
 
76
87
  return {
77
88
  success: true,
78
89
  steps: context.steps,
79
90
  report: report.path,
91
+ exportZip: exportZipPath,
80
92
  score: report.score,
81
93
  findingsCount: report.findingsCount,
82
94
  };
@@ -233,11 +245,67 @@ async function generateReport(context, compareResults, removedResources = []) {
233
245
 
234
246
  return {
235
247
  path: reportPath,
248
+ filename: reportFilename,
249
+ timestamp,
236
250
  score: totalScore,
237
251
  findingsCount: findings.length,
238
252
  };
239
253
  }
240
254
 
255
+ /**
256
+ * Create a ZIP export with compare HTML files, report, and run config
257
+ */
258
+ async function exportComparisonZip(context, report) {
259
+ const { compareDir, outputDir, config } = context;
260
+ const exportFilename = 'diffyr6-publish.zip';
261
+ const exportPath = path.join(outputDir, exportFilename);
262
+
263
+ const entries = [];
264
+
265
+ // Add HTML comparison files sent to the API
266
+ const htmlFiles = await listExportHtmlFiles(compareDir);
267
+ for (const file of htmlFiles) {
268
+ const filePath = path.join(compareDir, file);
269
+ const content = await fsp.readFile(filePath);
270
+ entries.push({
271
+ name: file,
272
+ data: content,
273
+ mtime: (await fsp.stat(filePath)).mtime,
274
+ });
275
+ }
276
+
277
+ // Add markdown report
278
+ const reportContent = await fsp.readFile(report.path);
279
+ entries.push({
280
+ name: report.filename,
281
+ data: reportContent,
282
+ mtime: (await fsp.stat(report.path)).mtime,
283
+ });
284
+
285
+ // Add config used for the run
286
+ entries.push({
287
+ name: 'run-config.json',
288
+ data: JSON.stringify(config, null, 2),
289
+ mtime: new Date(),
290
+ });
291
+
292
+ await createZip(exportPath, entries);
293
+ return exportPath;
294
+ }
295
+
296
+ async function listExportHtmlFiles(compareDir) {
297
+ const exists = await directoryExists(compareDir);
298
+ if (!exists) {
299
+ return [];
300
+ }
301
+ const files = await fsp.readdir(compareDir);
302
+ const allowed = /^(sd|xx)-.+-.+\.html$/i;
303
+ const excluded = /(intersection|union)\.html$/i;
304
+ return files
305
+ .filter(file => allowed.test(file) && !excluded.test(file))
306
+ .sort();
307
+ }
308
+
241
309
  /**
242
310
  * Read all HTML comparison files
243
311
  */
@@ -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
+ }