@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/README.md +18 -2
- package/package.json +1 -1
- package/src/cli.js +4 -2
- package/src/compare-terminology.js +1004 -0
- package/src/config.js +9 -3
- package/src/index.js +131 -8
- package/src/utils/zip.js +112 -0
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.
|
|
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
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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:
|
|
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
|
*/
|
package/src/utils/zip.js
ADDED
|
@@ -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
|
+
}
|