@gefyra/diffyr6-cli 1.0.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.
@@ -0,0 +1,101 @@
1
+ import { spawn } from 'child_process';
2
+
3
+ /**
4
+ * Spawns a process and collects or streams output
5
+ */
6
+ export function spawnProcess(command, args, cwd, options = {}) {
7
+ const { rejectOnNonZero = false, stream = false, stdio } = options;
8
+ return new Promise((resolve, reject) => {
9
+ const spawnOptions = {
10
+ cwd,
11
+ shell: process.platform === 'win32',
12
+ env: process.env,
13
+ };
14
+ if (stdio) {
15
+ spawnOptions.stdio = stdio;
16
+ } else if (stream) {
17
+ spawnOptions.stdio = 'inherit';
18
+ }
19
+
20
+ const child = spawn(command, args, spawnOptions);
21
+
22
+ let stdout = '';
23
+ let stderr = '';
24
+
25
+ const collectOutput = !spawnOptions.stdio;
26
+ if (collectOutput && child.stdout) {
27
+ child.stdout.on('data', (data) => {
28
+ stdout += data.toString();
29
+ });
30
+ }
31
+ if (collectOutput && child.stderr) {
32
+ child.stderr.on('data', (data) => {
33
+ stderr += data.toString();
34
+ });
35
+ }
36
+ child.on('error', (error) => {
37
+ reject(
38
+ new Error(`${command} could not be started: ${error.message}`)
39
+ );
40
+ });
41
+ child.on('close', (code) => {
42
+ const exitCode = code ?? 0;
43
+ if (rejectOnNonZero && exitCode !== 0) {
44
+ const msg = collectOutput
45
+ ? `${command} failed (exit code ${exitCode}). Details:\n${stdout}\n${stderr}`
46
+ : `${command} failed (exit code ${exitCode}).`;
47
+ reject(new Error(msg));
48
+ return;
49
+ }
50
+ resolve({ stdout, stderr, exitCode });
51
+ });
52
+ });
53
+ }
54
+
55
+ /**
56
+ * Creates a simple terminal spinner with configurable frames
57
+ */
58
+ export function createAnimator(label, options = {}) {
59
+ const frames =
60
+ options.frames ||
61
+ [
62
+ '🐟 ~~~~~',
63
+ ' 🐟 ~~~~~',
64
+ ' 🐟 ~~~~~',
65
+ ' 🐟 ~~~~~',
66
+ ' 🐟 ~~~~~',
67
+ ' 🐟 ~~~~~',
68
+ ' 🐟 ~~~~~',
69
+ ' 🐟 ~~~~~',
70
+ ];
71
+ const interval = options.interval || 150;
72
+ let index = 0;
73
+ let timer = null;
74
+
75
+ function render() {
76
+ const frame = frames[index];
77
+ index = (index + 1) % frames.length;
78
+ const text = `${frame} ${label}`;
79
+ process.stdout.write(`\r${text}`);
80
+ }
81
+
82
+ return {
83
+ start() {
84
+ if (timer) {
85
+ return;
86
+ }
87
+ render();
88
+ timer = setInterval(render, interval);
89
+ },
90
+ stop() {
91
+ if (!timer) {
92
+ return;
93
+ }
94
+ clearInterval(timer);
95
+ timer = null;
96
+ process.stdout.write('\r');
97
+ const blank = ' '.repeat(process.stdout.columns || 40);
98
+ process.stdout.write(`${blank}\r`);
99
+ },
100
+ };
101
+ }
@@ -0,0 +1,135 @@
1
+ import fsp from 'fs/promises';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ /**
9
+ * Identifies R4 profiles that are based on resource types removed in R6
10
+ * @param {string} r4Dir - R4 resources directory
11
+ * @returns {Promise<Array<{profile: string, resource: string}>>}
12
+ */
13
+ export async function findRemovedResources(r4Dir) {
14
+ // Load list of resources removed in R6
15
+ const removedResourceTypes = await loadRemovedResourceTypes();
16
+ const removedSet = new Set(removedResourceTypes);
17
+
18
+ // Read R4 profiles
19
+ const r4Profiles = await readProfileResources(r4Dir);
20
+
21
+ // Filter profiles based on removed resource types
22
+ const removed = [];
23
+ for (const { profile, resource } of r4Profiles) {
24
+ if (removedSet.has(resource)) {
25
+ removed.push({ profile, resource });
26
+ }
27
+ }
28
+
29
+ return removed;
30
+ }
31
+
32
+ /**
33
+ * Loads the list of resource types that were removed in R6
34
+ */
35
+ async function loadRemovedResourceTypes() {
36
+ const configPath = path.resolve(__dirname, '..', '..', 'config', 'resources-r4-not-in-r6.json');
37
+ const content = await fsp.readFile(configPath, 'utf8');
38
+ const data = JSON.parse(content);
39
+ return data.resources || [];
40
+ }
41
+
42
+ /**
43
+ * Reads all StructureDefinition profiles from a directory
44
+ */
45
+ async function readProfileResources(baseDir) {
46
+ const resourcesDir = path.join(baseDir, 'fsh-generated', 'resources');
47
+ const stat = await fsp.stat(resourcesDir).catch(() => null);
48
+ if (!stat || !stat.isDirectory()) {
49
+ return [];
50
+ }
51
+
52
+ const files = await collectJsonFiles(resourcesDir);
53
+ const results = [];
54
+ const seen = new Set();
55
+
56
+ for (const filePath of files) {
57
+ const raw = await fsp.readFile(filePath, 'utf8').catch(() => '');
58
+ if (!raw) {
59
+ continue;
60
+ }
61
+
62
+ let data;
63
+ try {
64
+ data = JSON.parse(raw);
65
+ } catch {
66
+ continue;
67
+ }
68
+
69
+ if (!data || data.resourceType !== 'StructureDefinition') {
70
+ continue;
71
+ }
72
+
73
+ const resource = resolveProfileResourceType(data);
74
+ const profile =
75
+ data.title ||
76
+ data.name ||
77
+ data.id ||
78
+ extractLastSegment(data.url) ||
79
+ path.basename(filePath, '.json');
80
+
81
+ if (!resource || !profile) {
82
+ continue;
83
+ }
84
+
85
+ const key = `${profile}::${resource}`.toLowerCase();
86
+ if (seen.has(key)) {
87
+ continue;
88
+ }
89
+
90
+ seen.add(key);
91
+ results.push({ profile, resource });
92
+ }
93
+
94
+ return results;
95
+ }
96
+
97
+ async function collectJsonFiles(dir) {
98
+ const results = [];
99
+ const entries = await fsp.readdir(dir, { withFileTypes: true }).catch(() => []);
100
+ for (const entry of entries) {
101
+ const entryPath = path.join(dir, entry.name);
102
+ if (entry.isDirectory()) {
103
+ const nested = await collectJsonFiles(entryPath);
104
+ results.push(...nested);
105
+ continue;
106
+ }
107
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
108
+ results.push(entryPath);
109
+ }
110
+ }
111
+ return results;
112
+ }
113
+
114
+ function resolveProfileResourceType(data) {
115
+ if (!data || typeof data !== 'object') {
116
+ return '';
117
+ }
118
+ if (typeof data.type === 'string' && data.type) {
119
+ return data.type;
120
+ }
121
+ if (typeof data.baseDefinition === 'string' && data.baseDefinition) {
122
+ return extractLastSegment(data.baseDefinition);
123
+ }
124
+ return '';
125
+ }
126
+
127
+ function extractLastSegment(url) {
128
+ if (!url) {
129
+ return '';
130
+ }
131
+ const hashIndex = url.lastIndexOf('#');
132
+ const slashIndex = url.lastIndexOf('/');
133
+ const index = Math.max(hashIndex, slashIndex);
134
+ return index >= 0 ? url.slice(index + 1) : url;
135
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Parses SUSHI log files and returns a list of error entries with file/line information
3
+ */
4
+ export function parseSushiLog(logContent) {
5
+ const entries = [];
6
+ const lines = logContent.split(/\r?\n/);
7
+ let index = 0;
8
+
9
+ while (index < lines.length) {
10
+ const line = lines[index];
11
+ const errorMatch = line.match(/^\s*error\s+(.*)$/i);
12
+ if (!errorMatch) {
13
+ index += 1;
14
+ continue;
15
+ }
16
+
17
+ const entry = { message: errorMatch[1], file: null, line: null, endLine: null };
18
+ index += 1;
19
+
20
+ while (index < lines.length) {
21
+ const detailMatch = lines[index].match(/^\s{2,}(.*)$/);
22
+ if (!detailMatch) {
23
+ break;
24
+ }
25
+ const detail = detailMatch[1].trim();
26
+ const fileMatch = detail.match(/^File:\s*(.+)$/i);
27
+ if (fileMatch) {
28
+ entry.file = fileMatch[1];
29
+ }
30
+ // Parse single line (e.g., "Line: 122") or line range (e.g., "Line: 122 - 124")
31
+ const lineRangeMatch = detail.match(/^Line:\s*(\d+)\s*-\s*(\d+)$/i);
32
+ const lineMatch = detail.match(/^Line:\s*(\d+)$/i);
33
+ if (lineRangeMatch) {
34
+ entry.line = Number.parseInt(lineRangeMatch[1], 10);
35
+ entry.endLine = Number.parseInt(lineRangeMatch[2], 10);
36
+ } else if (lineMatch) {
37
+ entry.line = Number.parseInt(lineMatch[1], 10);
38
+ }
39
+ index += 1;
40
+ }
41
+
42
+ entries.push(entry);
43
+ }
44
+
45
+ return entries;
46
+ }
@@ -0,0 +1,103 @@
1
+ import fsp from 'fs/promises';
2
+ import fs from 'fs';
3
+ import https from 'https';
4
+ import path from 'path';
5
+ import { fileExists } from './fs.js';
6
+ import { createAnimator } from './process.js';
7
+
8
+ const VALIDATOR_DOWNLOAD_URL = 'https://github.com/hapifhir/org.hl7.fhir.core/releases/download/6.7.10/validator_cli.jar';
9
+ const DEFAULT_VALIDATOR_FILENAME = 'validator_cli.jar';
10
+
11
+ /**
12
+ * Ensures the validator JAR exists, downloading it if necessary
13
+ * @param {string|null} jarPath - Path to validator JAR or null for auto-download
14
+ * @param {string} workdir - Working directory where to download the JAR
15
+ * @returns {Promise<string>} Path to the validator JAR
16
+ */
17
+ export async function ensureValidator(jarPath, workdir) {
18
+ // If jarPath is explicitly provided, verify it exists
19
+ if (jarPath) {
20
+ const resolvedPath = path.isAbsolute(jarPath) ? jarPath : path.resolve(workdir, jarPath);
21
+ if (await fileExists(resolvedPath)) {
22
+ return resolvedPath;
23
+ }
24
+ throw new Error(`Validator JAR not found at specified path: ${resolvedPath}`);
25
+ }
26
+
27
+ // Auto-download: check default location in workdir
28
+ const defaultPath = path.resolve(workdir, DEFAULT_VALIDATOR_FILENAME);
29
+ if (await fileExists(defaultPath)) {
30
+ console.log(` Using existing validator: ${defaultPath}`);
31
+ return defaultPath;
32
+ }
33
+
34
+ // Download validator
35
+ console.log(' Validator not found, downloading latest version...');
36
+ await downloadValidator(defaultPath);
37
+ console.log(` Downloaded validator to: ${defaultPath}`);
38
+ return defaultPath;
39
+ }
40
+
41
+ /**
42
+ * Downloads the validator JAR from GitHub releases
43
+ */
44
+ async function downloadValidator(targetPath) {
45
+ await fsp.mkdir(path.dirname(targetPath), { recursive: true });
46
+
47
+ const animator = createAnimator('Downloading HL7 FHIR Validator...');
48
+ animator.start();
49
+
50
+ try {
51
+ await downloadFile(VALIDATOR_DOWNLOAD_URL, targetPath);
52
+ } finally {
53
+ animator.stop();
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Downloads a file from a URL with redirect following
59
+ */
60
+ async function downloadFile(url, targetPath, maxRedirects = 5) {
61
+ if (maxRedirects <= 0) {
62
+ throw new Error('Too many redirects while downloading validator');
63
+ }
64
+
65
+ return new Promise((resolve, reject) => {
66
+ https.get(url, (response) => {
67
+ // Handle redirects
68
+ if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308) {
69
+ const redirectUrl = response.headers.location;
70
+ if (!redirectUrl) {
71
+ reject(new Error('Redirect without location header'));
72
+ return;
73
+ }
74
+ downloadFile(redirectUrl, targetPath, maxRedirects - 1)
75
+ .then(resolve)
76
+ .catch(reject);
77
+ return;
78
+ }
79
+
80
+ // Handle errors
81
+ if (response.statusCode !== 200) {
82
+ reject(new Error(`Failed to download validator: HTTP ${response.statusCode}`));
83
+ return;
84
+ }
85
+
86
+ // Write to file
87
+ const fileStream = fs.createWriteStream(targetPath);
88
+ response.pipe(fileStream);
89
+
90
+ fileStream.on('finish', () => {
91
+ fileStream.close();
92
+ resolve();
93
+ });
94
+
95
+ fileStream.on('error', (err) => {
96
+ fsp.unlink(targetPath).catch(() => {});
97
+ reject(err);
98
+ });
99
+ }).on('error', (err) => {
100
+ reject(new Error(`Network error while downloading validator: ${err.message}`));
101
+ });
102
+ });
103
+ }