@gefyra/diffyr6-cli 1.0.2 → 1.1.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.
- package/README.md +15 -1
- package/config/default-rules.json +14 -0
- package/package.json +1 -1
- package/src/compare-terminology.js +976 -0
- package/src/config.js +47 -3
- package/src/index.js +117 -11
- package/src/upgrade-sushi.js +69 -2
- package/src/utils/update-check.js +128 -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.2';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Default configuration values
|
|
@@ -25,6 +25,7 @@ export const DEFAULT_CONFIG = {
|
|
|
25
25
|
workdir: null,
|
|
26
26
|
compareMode: 'incremental',
|
|
27
27
|
exportZip: true,
|
|
28
|
+
skipTerminologyReport: false,
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -32,7 +33,17 @@ export const DEFAULT_CONFIG = {
|
|
|
32
33
|
*/
|
|
33
34
|
export async function loadConfig(configPath) {
|
|
34
35
|
const raw = await fsp.readFile(configPath, 'utf8');
|
|
35
|
-
|
|
36
|
+
let config = JSON.parse(raw);
|
|
37
|
+
const originalVersion = config.configVersion;
|
|
38
|
+
|
|
39
|
+
// Migrate config if needed
|
|
40
|
+
config = migrateConfig(config);
|
|
41
|
+
|
|
42
|
+
// Write back to file if migration occurred
|
|
43
|
+
if (config.configVersion !== originalVersion) {
|
|
44
|
+
await fsp.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
45
|
+
console.log(` Config file updated to version ${config.configVersion}`);
|
|
46
|
+
}
|
|
36
47
|
|
|
37
48
|
// Validate config version
|
|
38
49
|
validateConfigVersion(config);
|
|
@@ -46,6 +57,34 @@ export async function loadConfig(configPath) {
|
|
|
46
57
|
return merged;
|
|
47
58
|
}
|
|
48
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Migrates configuration from older versions to the current version
|
|
62
|
+
*/
|
|
63
|
+
function migrateConfig(config) {
|
|
64
|
+
if (!config.configVersion) {
|
|
65
|
+
// Very old config without version - add all new fields
|
|
66
|
+
console.log(' Migrating config from pre-1.0.0 to current version...');
|
|
67
|
+
config.configVersion = CONFIG_VERSION;
|
|
68
|
+
if (config.skipTerminologyReport === undefined) {
|
|
69
|
+
config.skipTerminologyReport = false;
|
|
70
|
+
}
|
|
71
|
+
return config;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const [major, minor, patch] = config.configVersion.split('.').map(Number);
|
|
75
|
+
|
|
76
|
+
// Migrate from 1.0.0 or 1.0.1 to 1.0.2
|
|
77
|
+
if (major === 1 && minor === 0 && (patch === 0 || patch === 1)) {
|
|
78
|
+
console.log(` Migrating config from ${config.configVersion} to ${CONFIG_VERSION}...`);
|
|
79
|
+
if (config.skipTerminologyReport === undefined) {
|
|
80
|
+
config.skipTerminologyReport = false;
|
|
81
|
+
}
|
|
82
|
+
config.configVersion = CONFIG_VERSION;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return config;
|
|
86
|
+
}
|
|
87
|
+
|
|
49
88
|
/**
|
|
50
89
|
* Validates the configuration version
|
|
51
90
|
*/
|
|
@@ -99,6 +138,10 @@ function validateConfig(config) {
|
|
|
99
138
|
if (typeof config.exportZip !== 'boolean') {
|
|
100
139
|
errors.push('exportZip must be a boolean');
|
|
101
140
|
}
|
|
141
|
+
|
|
142
|
+
if (typeof config.skipTerminologyReport !== 'boolean') {
|
|
143
|
+
errors.push('skipTerminologyReport must be a boolean');
|
|
144
|
+
}
|
|
102
145
|
|
|
103
146
|
if (errors.length > 0) {
|
|
104
147
|
throw new Error(`Invalid configuration:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
|
@@ -122,7 +165,8 @@ export async function createExampleConfig(outputPath) {
|
|
|
122
165
|
validatorJarPath: null,
|
|
123
166
|
workdir: null,
|
|
124
167
|
compareMode: 'incremental',
|
|
125
|
-
exportZip: true
|
|
168
|
+
exportZip: true,
|
|
169
|
+
skipTerminologyReport: false
|
|
126
170
|
};
|
|
127
171
|
|
|
128
172
|
await fsp.writeFile(
|
package/src/index.js
CHANGED
|
@@ -7,13 +7,18 @@ 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, hasSnapshots, runSushiWithSnapshots } from './compare-terminology.js';
|
|
10
11
|
import { findRemovedResources } from './utils/removed-resources.js';
|
|
11
12
|
import { createZip } from './utils/zip.js';
|
|
13
|
+
import { checkForUpdates } from './utils/update-check.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Main entry point - runs the FHIR R4 to R6 migration pipeline
|
|
15
17
|
*/
|
|
16
18
|
export async function runMigration(config) {
|
|
19
|
+
// Check for updates
|
|
20
|
+
await checkForUpdates();
|
|
21
|
+
|
|
17
22
|
// Resolve paths
|
|
18
23
|
const workdir = config.workdir ? path.resolve(config.workdir) : process.cwd();
|
|
19
24
|
const resourcesDir = path.resolve(workdir, config.resourcesDir);
|
|
@@ -38,41 +43,62 @@ export async function runMigration(config) {
|
|
|
38
43
|
if (config.enableGoFSH) {
|
|
39
44
|
const shouldRunGoFSH = await checkShouldRunGoFSH(resourcesDir);
|
|
40
45
|
if (shouldRunGoFSH) {
|
|
41
|
-
console.log('\n[1/
|
|
46
|
+
console.log('\n[1/6] Downloading package and generating FSH...');
|
|
42
47
|
await runGoFSH(context);
|
|
43
48
|
context.steps.push('gofsh');
|
|
44
49
|
} else {
|
|
45
|
-
console.log('\n[1/
|
|
50
|
+
console.log('\n[1/6] GoFSH - SKIPPED (Resources directory with sushi-config.yaml already exists)');
|
|
46
51
|
}
|
|
47
52
|
} else {
|
|
48
|
-
console.log('\n[1/
|
|
53
|
+
console.log('\n[1/6] GoFSH - DISABLED in config');
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
// Step 2: Upgrade to R6
|
|
52
57
|
const shouldRunUpgrade = await checkShouldRunUpgrade(resourcesR6Dir);
|
|
53
58
|
if (shouldRunUpgrade) {
|
|
54
|
-
console.log('\n[2/
|
|
59
|
+
console.log('\n[2/6] Upgrading to R6...');
|
|
55
60
|
await runUpgradeToR6(context);
|
|
56
61
|
context.steps.push('upgrade');
|
|
57
62
|
} else {
|
|
58
|
-
console.log('\n[2/
|
|
63
|
+
console.log('\n[2/6] Upgrade - SKIPPED (ResourcesR6 directory with sushi-config.yaml already exists)');
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
// Step 3:
|
|
62
|
-
console.log('\n[3/
|
|
66
|
+
// Step 3: Build snapshots for both R4 and R6
|
|
67
|
+
console.log('\n[3/6] Building snapshots with SUSHI...');
|
|
68
|
+
await runSnapshotBuild(context);
|
|
69
|
+
context.steps.push('snapshots');
|
|
70
|
+
|
|
71
|
+
// Step 4: Compare profiles
|
|
72
|
+
console.log('\n[4/6] Comparing R4 vs R6 profiles...');
|
|
63
73
|
const compareResults = await runProfileComparison(context);
|
|
64
74
|
context.steps.push('compare');
|
|
65
75
|
|
|
66
|
-
// Step
|
|
67
|
-
console.log('\n[4/4] Generating migration report...');
|
|
76
|
+
// Step 5: Generat6] Generating migration report...');
|
|
68
77
|
const removedResources = await findRemovedResources(resourcesDir);
|
|
69
78
|
const report = await generateReport(context, compareResults, removedResources);
|
|
70
79
|
context.steps.push('report');
|
|
71
80
|
|
|
81
|
+
// Step 6: Compare terminology bindings
|
|
82
|
+
let terminologyReport = null;
|
|
83
|
+
if (config.skipTerminologyReport) {
|
|
84
|
+
console.log('\n[6/6] Terminology comparison - SKIPPED (skipTerminologyReport is enabled)');
|
|
85
|
+
} else {
|
|
86
|
+
console.log('\n[6/6] Comparing terminology bindings...');
|
|
87
|
+
try {
|
|
88
|
+
terminologyReport = await runTerminologyComparison(context);
|
|
89
|
+
if (terminologyReport) {
|
|
90
|
+
context.steps.push('terminology');
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.warn(` Terminology comparison failed: ${error.message}`);
|
|
94
|
+
console.warn(' Continuing without terminology report...');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
72
98
|
let exportZipPath = null;
|
|
73
99
|
if (config.exportZip) {
|
|
74
100
|
console.log('\nGenerating export ZIP...');
|
|
75
|
-
exportZipPath = await exportComparisonZip(context, report);
|
|
101
|
+
exportZipPath = await exportComparisonZip(context, report, terminologyReport);
|
|
76
102
|
context.steps.push('exportZip');
|
|
77
103
|
}
|
|
78
104
|
|
|
@@ -131,6 +157,44 @@ async function runUpgradeToR6(context) {
|
|
|
131
157
|
await upgradeSushiToR6(resourcesDir, sushiExecutable);
|
|
132
158
|
}
|
|
133
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Build snapshots for both R4 and R6 projects with sushi -s
|
|
162
|
+
*/
|
|
163
|
+
async function runSnapshotBuild(context) {
|
|
164
|
+
const { resourcesDir, resourcesR6Dir, config } = context;
|
|
165
|
+
const debug = config.debug || false;
|
|
166
|
+
|
|
167
|
+
console.log(' Checking for existing snapshots...');
|
|
168
|
+
|
|
169
|
+
const resourcesDirHasSnapshots = await hasSnapshots(resourcesDir);
|
|
170
|
+
const resourcesR6DirHasSnapshots = await hasSnapshots(resourcesR6Dir);
|
|
171
|
+
|
|
172
|
+
if (resourcesDirHasSnapshots && resourcesR6DirHasSnapshots) {
|
|
173
|
+
console.log(' Snapshots already exist in both directories, skipping SUSHI build');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(' Building snapshots with SUSHI...');
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
if (!resourcesDirHasSnapshots) {
|
|
181
|
+
await runSushiWithSnapshots(resourcesDir, debug);
|
|
182
|
+
} else {
|
|
183
|
+
console.log(` Skipping ${path.basename(resourcesDir)} (snapshots already exist)`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!resourcesR6DirHasSnapshots) {
|
|
187
|
+
await runSushiWithSnapshots(resourcesR6Dir, debug);
|
|
188
|
+
} else {
|
|
189
|
+
console.log(` Skipping ${path.basename(resourcesR6Dir)} (snapshots already exist)`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log(' Snapshots built successfully');
|
|
193
|
+
} catch (error) {
|
|
194
|
+
throw new Error(`Failed to build snapshots: ${error.message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
134
198
|
/**
|
|
135
199
|
* Run profile comparison
|
|
136
200
|
*/
|
|
@@ -153,6 +217,28 @@ async function runProfileComparison(context) {
|
|
|
153
217
|
return [];
|
|
154
218
|
}
|
|
155
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Run terminology comparison
|
|
222
|
+
*/
|
|
223
|
+
async function runTerminologyComparison(context) {
|
|
224
|
+
const { resourcesDir, resourcesR6Dir, outputDir, config } = context;
|
|
225
|
+
|
|
226
|
+
const options = {
|
|
227
|
+
debug: config.debug || false,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const result = await compareTerminology(resourcesDir, resourcesR6Dir, outputDir, options);
|
|
231
|
+
|
|
232
|
+
if (result) {
|
|
233
|
+
console.log(` ${result.profilesWithDifferences} profile(s) with binding differences`);
|
|
234
|
+
console.log(` Total findings: ${result.totalFindings}`);
|
|
235
|
+
console.log(` Markdown report: ${result.path}`);
|
|
236
|
+
console.log(` JSON report: ${result.jsonPath}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
156
242
|
/**
|
|
157
243
|
* Get list of profiles that need to be compared
|
|
158
244
|
*/
|
|
@@ -255,7 +341,7 @@ async function generateReport(context, compareResults, removedResources = []) {
|
|
|
255
341
|
/**
|
|
256
342
|
* Create a ZIP export with compare HTML files, report, and run config
|
|
257
343
|
*/
|
|
258
|
-
async function exportComparisonZip(context, report) {
|
|
344
|
+
async function exportComparisonZip(context, report, terminologyReport = null) {
|
|
259
345
|
const { compareDir, outputDir, config } = context;
|
|
260
346
|
const exportFilename = 'diffyr6-publish.zip';
|
|
261
347
|
const exportPath = path.join(outputDir, exportFilename);
|
|
@@ -282,6 +368,26 @@ async function exportComparisonZip(context, report) {
|
|
|
282
368
|
mtime: (await fsp.stat(report.path)).mtime,
|
|
283
369
|
});
|
|
284
370
|
|
|
371
|
+
// Add terminology report if available
|
|
372
|
+
if (terminologyReport && terminologyReport.path) {
|
|
373
|
+
const termContent = await fsp.readFile(terminologyReport.path);
|
|
374
|
+
entries.push({
|
|
375
|
+
name: terminologyReport.filename,
|
|
376
|
+
data: termContent,
|
|
377
|
+
mtime: (await fsp.stat(terminologyReport.path)).mtime,
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// Add terminology JSON if available
|
|
381
|
+
if (terminologyReport.jsonPath) {
|
|
382
|
+
const termJsonContent = await fsp.readFile(terminologyReport.jsonPath);
|
|
383
|
+
entries.push({
|
|
384
|
+
name: terminologyReport.jsonFilename,
|
|
385
|
+
data: termJsonContent,
|
|
386
|
+
mtime: (await fsp.stat(terminologyReport.jsonPath)).mtime,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
285
391
|
// Add config used for the run
|
|
286
392
|
entries.push({
|
|
287
393
|
name: 'run-config.json',
|
package/src/upgrade-sushi.js
CHANGED
|
@@ -7,7 +7,7 @@ import { parseSushiLog } from './utils/sushi-log.js';
|
|
|
7
7
|
|
|
8
8
|
const SOURCE_VERSION = '4.0.1';
|
|
9
9
|
const TARGET_VERSION = '6.0.0-ballot3';
|
|
10
|
-
const MAX_ITERATIONS =
|
|
10
|
+
const MAX_ITERATIONS = 10;
|
|
11
11
|
const SNOMED_CT_ERROR_TEXT = 'Resolved value "SNOMED_CT" is not a valid URI';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -248,7 +248,11 @@ async function runSushiUntilSuccess(targetDir, sushiExecutable) {
|
|
|
248
248
|
}
|
|
249
249
|
console.log(` Commented out lines in ${modifications} file(s)`);
|
|
250
250
|
}
|
|
251
|
-
throw new Error(
|
|
251
|
+
throw new Error(
|
|
252
|
+
`SUSHI failed after ${MAX_ITERATIONS} iterations. ` +
|
|
253
|
+
`Please fix the remaining SUSHI errors manually in the ResourcesR6 directory ` +
|
|
254
|
+
`and then run 'sushi -s' to build the snapshots.`
|
|
255
|
+
);
|
|
252
256
|
}
|
|
253
257
|
|
|
254
258
|
async function runSushi(executable, targetDir) {
|
|
@@ -371,6 +375,69 @@ function expandContainsBlockLines(lines, lineNumbers) {
|
|
|
371
375
|
|
|
372
376
|
const currentLine = lines[idx].trim();
|
|
373
377
|
|
|
378
|
+
// Handle multi-line string literals (e.g., ^comment, ^description, etc.)
|
|
379
|
+
// Check if line contains = followed by a quote that's not closed
|
|
380
|
+
const hasStringAssignment = currentLine.match(/=\s*(""")?(")?/);
|
|
381
|
+
if (hasStringAssignment) {
|
|
382
|
+
const tripleQuote = hasStringAssignment[1]; // """
|
|
383
|
+
const singleQuote = hasStringAssignment[2]; // "
|
|
384
|
+
|
|
385
|
+
if (tripleQuote) {
|
|
386
|
+
// Triple-quoted string - find closing """
|
|
387
|
+
const firstTriplePos = currentLine.indexOf('"""');
|
|
388
|
+
const secondTriplePos = currentLine.indexOf('"""', firstTriplePos + 3);
|
|
389
|
+
|
|
390
|
+
if (secondTriplePos === -1) {
|
|
391
|
+
// No closing """ on this line - include following lines
|
|
392
|
+
let nextIdx = idx + 1;
|
|
393
|
+
while (nextIdx < lines.length) {
|
|
394
|
+
expanded.add(nextIdx + 1);
|
|
395
|
+
if (lines[nextIdx].includes('"""')) {
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
nextIdx++;
|
|
399
|
+
// Safety limit to prevent infinite loops
|
|
400
|
+
if (nextIdx - idx > 100) break;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} else if (singleQuote) {
|
|
404
|
+
// Single-quoted string - check if it's closed on the same line
|
|
405
|
+
const firstQuotePos = currentLine.indexOf('"');
|
|
406
|
+
let secondQuotePos = -1;
|
|
407
|
+
|
|
408
|
+
// Find closing quote that's not escaped
|
|
409
|
+
for (let i = firstQuotePos + 1; i < currentLine.length; i++) {
|
|
410
|
+
if (currentLine[i] === '"' && currentLine[i - 1] !== '\\') {
|
|
411
|
+
secondQuotePos = i;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (secondQuotePos === -1) {
|
|
417
|
+
// No closing " on this line - include following lines
|
|
418
|
+
let nextIdx = idx + 1;
|
|
419
|
+
while (nextIdx < lines.length) {
|
|
420
|
+
expanded.add(nextIdx + 1);
|
|
421
|
+
// Check for unescaped closing quote
|
|
422
|
+
const nextLine = lines[nextIdx];
|
|
423
|
+
let foundClosing = false;
|
|
424
|
+
for (let i = 0; i < nextLine.length; i++) {
|
|
425
|
+
if (nextLine[i] === '"' && (i === 0 || nextLine[i - 1] !== '\\')) {
|
|
426
|
+
foundClosing = true;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (foundClosing) {
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
nextIdx++;
|
|
434
|
+
// Safety limit to prevent infinite loops
|
|
435
|
+
if (nextIdx - idx > 100) break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
374
441
|
if (currentLine.includes(' contains') || currentLine.endsWith(' and')) {
|
|
375
442
|
let nextIdx = idx + 1;
|
|
376
443
|
while (nextIdx < lines.length) {
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Gets the current package version from package.json
|
|
11
|
+
*/
|
|
12
|
+
function getCurrentVersion() {
|
|
13
|
+
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
|
|
14
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
15
|
+
return packageJson.version;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fetches the latest version from NPM registry
|
|
20
|
+
*/
|
|
21
|
+
function getLatestVersion(packageName) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const options = {
|
|
24
|
+
hostname: 'registry.npmjs.org',
|
|
25
|
+
port: 443,
|
|
26
|
+
path: `/${packageName}`,
|
|
27
|
+
method: 'GET',
|
|
28
|
+
headers: {
|
|
29
|
+
'Accept': 'application/json',
|
|
30
|
+
},
|
|
31
|
+
timeout: 3000,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const req = https.request(options, (res) => {
|
|
35
|
+
let data = '';
|
|
36
|
+
|
|
37
|
+
res.on('data', (chunk) => {
|
|
38
|
+
data += chunk;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
res.on('end', () => {
|
|
42
|
+
try {
|
|
43
|
+
if (res.statusCode === 200) {
|
|
44
|
+
const json = JSON.parse(data);
|
|
45
|
+
resolve(json['dist-tags']?.latest || null);
|
|
46
|
+
} else {
|
|
47
|
+
resolve(null);
|
|
48
|
+
}
|
|
49
|
+
} catch (error) {
|
|
50
|
+
resolve(null);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
req.on('error', () => {
|
|
56
|
+
resolve(null);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
req.on('timeout', () => {
|
|
60
|
+
req.destroy();
|
|
61
|
+
resolve(null);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
req.end();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Compares two semantic versions
|
|
70
|
+
* Returns true if newVersion is greater than currentVersion
|
|
71
|
+
*/
|
|
72
|
+
function isNewerVersion(currentVersion, newVersion) {
|
|
73
|
+
if (!currentVersion || !newVersion) return false;
|
|
74
|
+
|
|
75
|
+
const current = currentVersion.split('.').map(Number);
|
|
76
|
+
const latest = newVersion.split('.').map(Number);
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < 3; i++) {
|
|
79
|
+
if (latest[i] > current[i]) return true;
|
|
80
|
+
if (latest[i] < current[i]) return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Creates a boxed update notification message
|
|
88
|
+
*/
|
|
89
|
+
function createUpdateNotification(currentVersion, latestVersion, packageName) {
|
|
90
|
+
const lines = [
|
|
91
|
+
'',
|
|
92
|
+
'╔════════════════════════════════════════════════════════════════╗',
|
|
93
|
+
'║ ║',
|
|
94
|
+
'║ UPDATE AVAILABLE ║',
|
|
95
|
+
'║ ║',
|
|
96
|
+
`║ Current version: ${currentVersion.padEnd(43)} ║`,
|
|
97
|
+
`║ Latest version: ${latestVersion.padEnd(43)} ║`,
|
|
98
|
+
'║ ║',
|
|
99
|
+
'║ Run one of the following commands to update: ║',
|
|
100
|
+
'║ ║',
|
|
101
|
+
`║ npm update -g ${packageName.padEnd(42)} ║`,
|
|
102
|
+
`║ npm install -g ${packageName}@latest`.padEnd(65) + '║',
|
|
103
|
+
'║ ║',
|
|
104
|
+
'╚════════════════════════════════════════════════════════════════╝',
|
|
105
|
+
'',
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Checks for updates and displays notification if available
|
|
113
|
+
* This function is non-blocking and will not throw errors
|
|
114
|
+
*/
|
|
115
|
+
export async function checkForUpdates() {
|
|
116
|
+
try {
|
|
117
|
+
const packageName = '@gefyra/diffyr6-cli';
|
|
118
|
+
const currentVersion = getCurrentVersion();
|
|
119
|
+
const latestVersion = await getLatestVersion(packageName);
|
|
120
|
+
|
|
121
|
+
if (latestVersion && isNewerVersion(currentVersion, latestVersion)) {
|
|
122
|
+
const notification = createUpdateNotification(currentVersion, latestVersion, packageName);
|
|
123
|
+
console.log(notification);
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Silently fail - update check should never block the main functionality
|
|
127
|
+
}
|
|
128
|
+
}
|