@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 +3 -1
- package/package.json +1 -1
- package/src/cli.js +4 -2
- package/src/config.js +9 -3
- package/src/index.js +68 -0
- package/src/utils/zip.js +112 -0
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
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 =
|
|
23
|
-
console.log(pkg.
|
|
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.
|
|
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
|
@@ -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
|
*/
|
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
|
+
}
|