@gefyra/diffyr6-cli 1.0.3 → 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/config/default-rules.json +14 -0
- package/package.json +1 -1
- package/src/compare-terminology.js +11 -39
- package/src/config.js +47 -3
- package/src/index.js +69 -18
- package/src/upgrade-sushi.js +69 -2
- package/src/utils/update-check.js +128 -0
|
@@ -44,6 +44,20 @@
|
|
|
44
44
|
],
|
|
45
45
|
"template": "For element {{Name}}, the cardinality changed in R6 and it had an MS in R4: {{Comments}}"
|
|
46
46
|
},
|
|
47
|
+
{
|
|
48
|
+
"name": "Element type changed in R6",
|
|
49
|
+
"description": "The data type of an element has changed between R4 and R6. This may require data transformation during migration.",
|
|
50
|
+
"rank": 15,
|
|
51
|
+
"value": 10,
|
|
52
|
+
"conditions": [
|
|
53
|
+
{
|
|
54
|
+
"column": "L Type",
|
|
55
|
+
"operator": "!equals",
|
|
56
|
+
"valueColumn": "R Type"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"template": "The element type switched in R6 at element {{Name}}"
|
|
60
|
+
},
|
|
47
61
|
{
|
|
48
62
|
"name": "Element removed in R6",
|
|
49
63
|
"description": "An element from R4 no longer exists in R6. Data in this element may need to be migrated to another element or discarded.",
|
package/package.json
CHANGED
|
@@ -8,11 +8,12 @@ import { directoryExists, fileExists } from './utils/fs.js';
|
|
|
8
8
|
* Compares terminology bindings between R4 and R6 profiles
|
|
9
9
|
*
|
|
10
10
|
* Steps:
|
|
11
|
-
* 1.
|
|
12
|
-
* 2.
|
|
13
|
-
* 3.
|
|
14
|
-
* 4.
|
|
15
|
-
*
|
|
11
|
+
* 1. Find all profile pairs from the comparison run
|
|
12
|
+
* 2. Compare element[].binding.strength and valueSet between R4 and R6
|
|
13
|
+
* 3. If valueSet has a version (pipe notation), compare the actual ValueSet content from local package cache
|
|
14
|
+
* 4. Generate a markdown report with all findings
|
|
15
|
+
*
|
|
16
|
+
* Note: Snapshots must already exist (built by runSnapshotBuild in index.js before calling this function)
|
|
16
17
|
*
|
|
17
18
|
* @param {string} resourcesDir - R4 resources directory
|
|
18
19
|
* @param {string} resourcesR6Dir - R6 resources directory
|
|
@@ -23,38 +24,9 @@ import { directoryExists, fileExists } from './utils/fs.js';
|
|
|
23
24
|
export async function compareTerminology(resourcesDir, resourcesR6Dir, outputDir, options = {}) {
|
|
24
25
|
const { debug = false } = options;
|
|
25
26
|
|
|
26
|
-
console.log(' Checking for existing snapshots...');
|
|
27
|
-
|
|
28
|
-
// Step 1: Check if snapshots already exist, if not run sushi -s
|
|
29
|
-
const resourcesDirHasSnapshots = await hasSnapshots(resourcesDir);
|
|
30
|
-
const resourcesR6DirHasSnapshots = await hasSnapshots(resourcesR6Dir);
|
|
31
|
-
|
|
32
|
-
if (resourcesDirHasSnapshots && resourcesR6DirHasSnapshots) {
|
|
33
|
-
console.log(' Snapshots already exist in both directories, skipping SUSHI build');
|
|
34
|
-
} else {
|
|
35
|
-
console.log(' Building snapshots with SUSHI...');
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
if (!resourcesDirHasSnapshots) {
|
|
39
|
-
await runSushiWithSnapshots(resourcesDir, debug);
|
|
40
|
-
} else {
|
|
41
|
-
console.log(` Skipping ${path.basename(resourcesDir)} (snapshots already exist)`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (!resourcesR6DirHasSnapshots) {
|
|
45
|
-
await runSushiWithSnapshots(resourcesR6Dir, debug);
|
|
46
|
-
} else {
|
|
47
|
-
console.log(` Skipping ${path.basename(resourcesR6Dir)} (snapshots already exist)`);
|
|
48
|
-
}
|
|
49
|
-
} catch (error) {
|
|
50
|
-
throw new Error(`Failed to build snapshots: ${error.message}`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
console.log(' Snapshots built successfully');
|
|
54
|
-
}
|
|
55
27
|
console.log(' Analyzing binding differences...');
|
|
56
28
|
|
|
57
|
-
//
|
|
29
|
+
// Collect profile pairs and compare bindings
|
|
58
30
|
const r4Profiles = await collectStructureDefinitions(resourcesDir);
|
|
59
31
|
const r6Profiles = await collectStructureDefinitions(resourcesR6Dir);
|
|
60
32
|
|
|
@@ -81,13 +53,13 @@ export async function compareTerminology(resourcesDir, resourcesR6Dir, outputDir
|
|
|
81
53
|
|
|
82
54
|
console.log(` Found ${findings.length} profile(s) with binding differences`);
|
|
83
55
|
|
|
84
|
-
//
|
|
56
|
+
// Identify common bindings across all profiles
|
|
85
57
|
const commonBindings = identifyCommonBindings(findings);
|
|
86
58
|
|
|
87
59
|
// Remove common bindings from individual profiles
|
|
88
60
|
const filteredFindings = removeCommonBindingsFromProfiles(findings, commonBindings);
|
|
89
61
|
|
|
90
|
-
//
|
|
62
|
+
// Generate reports
|
|
91
63
|
const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+/, '').replace('T', '-');
|
|
92
64
|
const reportFilename = `terminology-report-${timestamp}.md`;
|
|
93
65
|
const reportPath = path.join(outputDir, reportFilename);
|
|
@@ -123,7 +95,7 @@ export async function compareTerminology(resourcesDir, resourcesR6Dir, outputDir
|
|
|
123
95
|
/**
|
|
124
96
|
* Check if snapshots already exist in the StructureDefinition files
|
|
125
97
|
*/
|
|
126
|
-
async function hasSnapshots(dir) {
|
|
98
|
+
export async function hasSnapshots(dir) {
|
|
127
99
|
const resourcesPath = path.join(dir, 'fsh-generated', 'resources');
|
|
128
100
|
const exists = await directoryExists(resourcesPath);
|
|
129
101
|
|
|
@@ -160,7 +132,7 @@ async function hasSnapshots(dir) {
|
|
|
160
132
|
/**
|
|
161
133
|
* Run sushi with snapshots flag in a directory
|
|
162
134
|
*/
|
|
163
|
-
async function runSushiWithSnapshots(dir, debug = false) {
|
|
135
|
+
export async function runSushiWithSnapshots(dir, debug = false) {
|
|
164
136
|
const sushiConfigPath = path.join(dir, 'sushi-config.yaml');
|
|
165
137
|
const exists = await fileExists(sushiConfigPath);
|
|
166
138
|
|
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,14 +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 } from './compare-terminology.js';
|
|
10
|
+
import { compareTerminology, hasSnapshots, runSushiWithSnapshots } from './compare-terminology.js';
|
|
11
11
|
import { findRemovedResources } from './utils/removed-resources.js';
|
|
12
12
|
import { createZip } from './utils/zip.js';
|
|
13
|
+
import { checkForUpdates } from './utils/update-check.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Main entry point - runs the FHIR R4 to R6 migration pipeline
|
|
16
17
|
*/
|
|
17
18
|
export async function runMigration(config) {
|
|
19
|
+
// Check for updates
|
|
20
|
+
await checkForUpdates();
|
|
21
|
+
|
|
18
22
|
// Resolve paths
|
|
19
23
|
const workdir = config.workdir ? path.resolve(config.workdir) : process.cwd();
|
|
20
24
|
const resourcesDir = path.resolve(workdir, config.resourcesDir);
|
|
@@ -39,47 +43,56 @@ export async function runMigration(config) {
|
|
|
39
43
|
if (config.enableGoFSH) {
|
|
40
44
|
const shouldRunGoFSH = await checkShouldRunGoFSH(resourcesDir);
|
|
41
45
|
if (shouldRunGoFSH) {
|
|
42
|
-
console.log('\n[1/
|
|
46
|
+
console.log('\n[1/6] Downloading package and generating FSH...');
|
|
43
47
|
await runGoFSH(context);
|
|
44
48
|
context.steps.push('gofsh');
|
|
45
49
|
} else {
|
|
46
|
-
console.log('\n[1/
|
|
50
|
+
console.log('\n[1/6] GoFSH - SKIPPED (Resources directory with sushi-config.yaml already exists)');
|
|
47
51
|
}
|
|
48
52
|
} else {
|
|
49
|
-
console.log('\n[1/
|
|
53
|
+
console.log('\n[1/6] GoFSH - DISABLED in config');
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
// Step 2: Upgrade to R6
|
|
53
57
|
const shouldRunUpgrade = await checkShouldRunUpgrade(resourcesR6Dir);
|
|
54
58
|
if (shouldRunUpgrade) {
|
|
55
|
-
console.log('\n[2/
|
|
59
|
+
console.log('\n[2/6] Upgrading to R6...');
|
|
56
60
|
await runUpgradeToR6(context);
|
|
57
61
|
context.steps.push('upgrade');
|
|
58
62
|
} else {
|
|
59
|
-
console.log('\n[2/
|
|
63
|
+
console.log('\n[2/6] Upgrade - SKIPPED (ResourcesR6 directory with sushi-config.yaml already exists)');
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
// Step 3:
|
|
63
|
-
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...');
|
|
64
73
|
const compareResults = await runProfileComparison(context);
|
|
65
74
|
context.steps.push('compare');
|
|
66
75
|
|
|
67
|
-
// Step
|
|
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
|
|
|
72
|
-
// Step
|
|
73
|
-
console.log('\n[5/5] Comparing terminology bindings...');
|
|
81
|
+
// Step 6: Compare terminology bindings
|
|
74
82
|
let terminologyReport = null;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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...');
|
|
79
95
|
}
|
|
80
|
-
} catch (error) {
|
|
81
|
-
console.warn(` Terminology comparison failed: ${error.message}`);
|
|
82
|
-
console.warn(' Continuing without terminology report...');
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
let exportZipPath = null;
|
|
@@ -144,6 +157,44 @@ async function runUpgradeToR6(context) {
|
|
|
144
157
|
await upgradeSushiToR6(resourcesDir, sushiExecutable);
|
|
145
158
|
}
|
|
146
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
|
+
|
|
147
198
|
/**
|
|
148
199
|
* Run profile comparison
|
|
149
200
|
*/
|
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
|
+
}
|