wyvrnpm 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.
package/README.md ADDED
@@ -0,0 +1,139 @@
1
+ # wyvrnpm
2
+
3
+ A simple, private C++ package manager that works with any static file hosting provider — Amazon S3, Azure Blob Storage, a plain nginx server, or anything that can serve files over HTTPS.
4
+
5
+ There is no central registry. You control where packages are hosted.
6
+
7
+ ---
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g wyvrnpm
13
+ ```
14
+
15
+ Requires Node.js >= 18.
16
+
17
+ ---
18
+
19
+ ## Commands
20
+
21
+ ### `wyvrnpm init`
22
+
23
+ Creates a `wyvrn.json` manifest in the current directory (or the directory given by `--root`).
24
+
25
+ ```bash
26
+ wyvrnpm init
27
+ wyvrnpm init --root ./my-project
28
+ ```
29
+
30
+ ### `wyvrnpm install`
31
+
32
+ Resolves the full dependency graph and downloads all packages.
33
+
34
+ ```bash
35
+ wyvrnpm install --source https://my-bucket.s3.amazonaws.com/cpp --platform win_x64
36
+
37
+ # Multiple sources — tried in order, first to respond wins
38
+ wyvrnpm install \
39
+ --source https://primary.example.com/cpp \
40
+ --source https://mirror.example.com/cpp \
41
+ --platform linux_x64
42
+
43
+ # Aliases
44
+ wyvrnpm install -s https://pkg.example.com/cpp -s https://mirror.example.com/cpp
45
+ ```
46
+
47
+ **Options**
48
+
49
+ | Option | Default | Description |
50
+ |---|---|---|
51
+ | `--source` / `-s` | — | Base URL of a package source. Repeat for multiple sources. |
52
+ | `--platform` | `win_x64` | Target platform. One of: `win_x64`, `win_x86`, `linux_x64`, `linux_x86`, `osx_x64`, `osx_arm64` |
53
+ | `--timeout` | `300` | HTTP request timeout in seconds |
54
+ | `--manifest` | `./wyvrn.json` | Path to the manifest file |
55
+ | `--root` | `./` | Project root (packages extracted to `{root}/wyvrn_internal/`) |
56
+
57
+ Packages are extracted to `wyvrn_internal/{name}/` and a `wyvrn.lock` file is written alongside the manifest recording the exact resolved versions. On subsequent installs the lock pins all previously resolved versions — only newly added dependencies are resolved fresh.
58
+
59
+ ### `wyvrnpm clean`
60
+
61
+ Removes the `wyvrn_internal/` package cache and `wyvrn.lock`.
62
+
63
+ ```bash
64
+ wyvrnpm clean
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Registry Layout
70
+
71
+ Packages must be hosted at the following URL structure:
72
+
73
+ ```
74
+ {source}/{platform}/{name}/{version}/wyvrn.json <- package manifest (preferred)
75
+ {source}/{platform}/{name}/{version}/wyvrn.zip <- package archive (preferred)
76
+ ```
77
+
78
+ The tool tries `wyvrn.json` / `wyvrn.zip` first and falls back to `razer.json` / `razer.zip` if not found, for backwards compatibility.
79
+
80
+ **Example:**
81
+
82
+ ```
83
+ https://my-bucket.s3.amazonaws.com/cpp/win_x64/OpenSSL/3.0.0.0/wyvrn.json
84
+ https://my-bucket.s3.amazonaws.com/cpp/win_x64/OpenSSL/3.0.0.0/wyvrn.zip
85
+ ```
86
+
87
+ ### Package manifest format (wyvrn.json / razer.json)
88
+
89
+ ```json
90
+ {
91
+ "Name": "OpenSSL",
92
+ "Version": "3.0.0.0",
93
+ "Description": "TLS/SSL toolkit",
94
+ "Dependencies": [
95
+ { "Name": "zlib", "Version": "1.3.0.0" }
96
+ ]
97
+ }
98
+ ```
99
+
100
+ Fields use PascalCase. `Dependencies` is an array — transitive dependencies are resolved automatically.
101
+
102
+ ---
103
+
104
+ ## Project Manifest (wyvrn.json)
105
+
106
+ ```json
107
+ {
108
+ "name": "my-app",
109
+ "version": "1.0.0.0",
110
+ "description": "My C++ application",
111
+ "kind": "ConsoleApp",
112
+ "dependencies": {
113
+ "OpenSSL": "3.0.0.0",
114
+ "zlib": "1.3.0.0"
115
+ }
116
+ }
117
+ ```
118
+
119
+ **`kind` values:** `ConsoleApp`, `StaticLib`, `DynamicLib`, `HeaderOnlyLib`, `WebApp`, `Utility`, `Service`, `TestProject`
120
+
121
+ **`dependencies`** maps package names to version strings (`"major.minor.patch.build"`).
122
+
123
+ ---
124
+
125
+ ## Version Conflict Resolution
126
+
127
+ When the dependency graph requires the same package at multiple versions, the highest version wins. If a `wyvrn.lock` exists, locked versions always take priority over any conflicting transitive request. A warning is printed for every conflict detected.
128
+
129
+ ---
130
+
131
+ ## Ignoring generated files
132
+
133
+ Add the following to your `.gitignore`:
134
+
135
+ ```
136
+ wyvrn_internal/
137
+ wyvrn.lock
138
+ *.zip
139
+ ```
package/bin/wyvrn.js ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const yargs = require('yargs');
6
+ const init = require('../src/commands/init');
7
+ const install = require('../src/commands/install');
8
+ const clean = require('../src/commands/clean');
9
+
10
+ yargs
11
+ .option('manifest', {
12
+ type: 'string',
13
+ description: 'Path to manifest file',
14
+ default: './wyvrn.json',
15
+ })
16
+ .option('root', {
17
+ type: 'string',
18
+ description: 'Root directory',
19
+ default: './',
20
+ })
21
+ .command(
22
+ 'init',
23
+ 'Initialise a new wyvrn.json manifest in the current project',
24
+ () => {},
25
+ (argv) => init(argv),
26
+ )
27
+ .command(
28
+ 'install',
29
+ 'Resolve and download all dependencies listed in the manifest',
30
+ (yargs) => {
31
+ yargs
32
+ .option('source', {
33
+ alias: 's',
34
+ type: 'string',
35
+ description: 'Base URL of a package source (can be repeated)',
36
+ array: true,
37
+ })
38
+ .option('platform', {
39
+ type: 'string',
40
+ description: 'Target platform',
41
+ choices: ['win_x64', 'win_x86', 'linux_x64', 'linux_x86', 'osx_x64', 'osx_arm64'],
42
+ default: 'win_x64',
43
+ })
44
+ .option('timeout', {
45
+ type: 'number',
46
+ description: 'HTTP timeout in seconds',
47
+ default: 300,
48
+ });
49
+ },
50
+ (argv) => install(argv),
51
+ )
52
+ .command(
53
+ 'clean',
54
+ 'Remove downloaded packages and lock file',
55
+ () => {},
56
+ (argv) => clean(argv),
57
+ )
58
+ .demandCommand(1)
59
+ .strict()
60
+ .help()
61
+ .parse();
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "wyvrnpm",
3
+ "version": "1.0.0",
4
+ "description": "A simple, static-hosting-compatible C++ package manager",
5
+ "keywords": [
6
+ "c++",
7
+ "package-manager",
8
+ "cpp",
9
+ "dependency-management",
10
+ "s3",
11
+ "azure"
12
+ ],
13
+ "license": "MIT",
14
+ "bin": {
15
+ "wyvrnpm": "bin/wyvrn.js"
16
+ },
17
+ "main": "src/index.js",
18
+ "type": "commonjs",
19
+ "files": [
20
+ "bin/",
21
+ "src/",
22
+ "README.md"
23
+ ],
24
+ "scripts": {
25
+ "test": "node --test tests/*.test.js"
26
+ },
27
+ "dependencies": {
28
+ "adm-zip": "^0.5.16",
29
+ "yargs": "^17.7.2"
30
+ },
31
+ "engines": {
32
+ "node": ">=18.0.0"
33
+ }
34
+ }
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Removes the wyvrn_internal/ package cache directory and the wyvrn.lock file.
8
+ *
9
+ * @param {object} argv - Parsed yargs arguments.
10
+ * @param {string} argv.root - Project root directory.
11
+ * @param {string} argv.manifest - Path to the manifest file (used to locate wyvrn.lock).
12
+ * @returns {Promise<void>}
13
+ */
14
+ async function clean(argv) {
15
+ const rootDir = path.resolve(argv.root);
16
+ const razerDir = path.join(rootDir, 'wyvrn_internal');
17
+ const lockPath = path.join(path.dirname(path.resolve(argv.manifest)), 'wyvrn.lock');
18
+
19
+ let removedAnything = false;
20
+
21
+ if (fs.existsSync(razerDir)) {
22
+ fs.rmSync(razerDir, { recursive: true, force: true });
23
+ console.log(`[wyvrn] Removed ${razerDir}`);
24
+ removedAnything = true;
25
+ } else {
26
+ console.log(`[wyvrn] Nothing to clean at ${razerDir}`);
27
+ }
28
+
29
+ if (fs.existsSync(lockPath)) {
30
+ fs.unlinkSync(lockPath);
31
+ console.log(`[wyvrn] Removed ${lockPath}`);
32
+ removedAnything = true;
33
+ }
34
+
35
+ if (!removedAnything) {
36
+ console.log('[wyvrn] Nothing to clean.');
37
+ }
38
+ }
39
+
40
+ module.exports = clean;
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { defaultManifest, writeManifest } = require('../manifest');
6
+
7
+ /**
8
+ * Initialises a new wyvrn.json manifest in the project root.
9
+ * If a manifest already exists the command exits early without overwriting it.
10
+ *
11
+ * @param {object} argv - Parsed yargs arguments.
12
+ * @param {string} argv.manifest - Path to the manifest file.
13
+ * @param {string} argv.root - Project root directory.
14
+ * @returns {Promise<void>}
15
+ */
16
+ async function init(argv) {
17
+ const manifestPath = path.resolve(argv.manifest);
18
+ const rootDir = path.resolve(argv.root);
19
+
20
+ if (fs.existsSync(manifestPath)) {
21
+ console.log(`[wyvrn] Manifest already exists at ${manifestPath}`);
22
+ return;
23
+ }
24
+
25
+ const projectName = path.basename(rootDir);
26
+ const manifest = defaultManifest(projectName);
27
+
28
+ writeManifest(manifestPath, manifest);
29
+
30
+ console.log(`[wyvrn] Created manifest at ${manifestPath}`);
31
+ }
32
+
33
+ module.exports = init;
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { readManifest } = require('../manifest');
6
+ const { resolveDependencies } = require('../resolve');
7
+ const { downloadDependencies } = require('../download');
8
+
9
+ /**
10
+ * Resolves and downloads all dependencies declared in the manifest, then writes
11
+ * a wyvrn.lock file recording exactly what was installed.
12
+ *
13
+ * @param {object} argv - Parsed yargs arguments.
14
+ * @param {string} argv.manifest - Path to wyvrn.json.
15
+ * @param {string} argv.root - Project root directory.
16
+ * @param {string[]} [argv.source] - One or more base URLs for package sources.
17
+ * @param {string} argv.platform - Target platform string.
18
+ * @param {number} argv.timeout - HTTP timeout in seconds.
19
+ * @returns {Promise<void>}
20
+ */
21
+ async function install(argv) {
22
+ const manifestPath = path.resolve(argv.manifest);
23
+ const rootDir = path.resolve(argv.root);
24
+
25
+ // Validate that at least one source is provided.
26
+ const packageSources = argv.source && argv.source.length > 0 ? argv.source : [];
27
+ if (packageSources.length === 0) {
28
+ console.warn(
29
+ '[wyvrn] Warning: no --source URLs provided. ' +
30
+ 'Pass at least one with --source <url> or -s <url>.',
31
+ );
32
+ }
33
+
34
+ const lockPath = path.join(path.dirname(manifestPath), 'wyvrn.lock');
35
+ const lockedVersions = new Map();
36
+ if (fs.existsSync(lockPath)) {
37
+ try {
38
+ const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
39
+ for (const [name, version] of Object.entries(lock.packages || {})) {
40
+ lockedVersions.set(name, version);
41
+ }
42
+ console.log(`[wyvrn] Lock file found — ${lockedVersions.size} pinned package(s)`);
43
+ } catch {
44
+ console.warn('[wyvrn] Warning: could not read wyvrn.lock, resolving from scratch');
45
+ }
46
+ }
47
+
48
+ const manifest = readManifest(manifestPath);
49
+
50
+ // Normalize dependencies: support both array [{ Name, Version }] and object { name: version }.
51
+ const rawDeps = manifest.dependencies || manifest.Dependencies || {};
52
+ const rootDeps = Array.isArray(rawDeps)
53
+ ? Object.fromEntries(rawDeps.map((d) => [d.Name ?? d.name, d.Version ?? d.version]))
54
+ : rawDeps;
55
+
56
+ console.log(
57
+ `[wyvrn] Installing dependencies for "${manifest.name}" (platform: ${argv.platform})`,
58
+ );
59
+
60
+ const resolvedDeps = await resolveDependencies(
61
+ rootDeps,
62
+ packageSources,
63
+ argv.platform,
64
+ fetch,
65
+ lockedVersions,
66
+ );
67
+
68
+ const razerDir = path.join(rootDir, 'wyvrn_internal');
69
+
70
+ await downloadDependencies(
71
+ resolvedDeps,
72
+ packageSources,
73
+ argv.platform,
74
+ razerDir,
75
+ fetch,
76
+ argv.timeout,
77
+ );
78
+
79
+ // Build lock file — packages sorted alphabetically for deterministic diffs.
80
+ const sortedPackages = {};
81
+ for (const key of [...resolvedDeps.keys()].sort()) {
82
+ sortedPackages[key] = resolvedDeps.get(key);
83
+ }
84
+
85
+ const lockData = {
86
+ generatedAt: new Date().toISOString(),
87
+ platform: argv.platform,
88
+ packages: sortedPackages,
89
+ };
90
+
91
+ fs.writeFileSync(lockPath, JSON.stringify(lockData, null, 2) + '\n', 'utf8');
92
+ console.log(`[wyvrn] Lock file written to ${lockPath}`);
93
+
94
+ const count = resolvedDeps.size;
95
+ console.log(`[wyvrn] Done — ${count} package${count !== 1 ? 's' : ''} installed.`);
96
+ }
97
+
98
+ module.exports = install;
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const AdmZip = require('adm-zip');
6
+
7
+ /**
8
+ * Downloads a single URL to a local file path, respecting the given timeout.
9
+ *
10
+ * @param {string} url - Remote URL to download.
11
+ * @param {string} destPath - Local path to write the response body to.
12
+ * @param {Function} httpClient - fetch-compatible function.
13
+ * @param {number} timeoutMs - Abort timeout in milliseconds.
14
+ * @returns {Promise<void>}
15
+ * @throws {Error} On non-OK HTTP status or network / timeout error.
16
+ */
17
+ async function downloadFile(url, destPath, httpClient, timeoutMs) {
18
+ const controller = new AbortController();
19
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
20
+
21
+ let response;
22
+ try {
23
+ response = await httpClient(url, { signal: controller.signal });
24
+ } finally {
25
+ clearTimeout(timer);
26
+ }
27
+
28
+ if (!response.ok) {
29
+ throw new Error(`HTTP ${response.status} for ${url}`);
30
+ }
31
+
32
+ const arrayBuffer = await response.arrayBuffer();
33
+ fs.writeFileSync(destPath, Buffer.from(arrayBuffer));
34
+ }
35
+
36
+ /**
37
+ * Resolves which source URL contains a given package by probing each one in order.
38
+ *
39
+ * @param {string} name
40
+ * @param {string} version
41
+ * @param {string[]} packageSources
42
+ * @param {string} platform
43
+ * @param {Function} httpClient
44
+ * @param {number} timeoutMs
45
+ * @param {string} destZipPath - Where to save the downloaded zip.
46
+ * @returns {Promise<boolean>} True if successfully downloaded, false otherwise.
47
+ */
48
+ async function tryDownloadFromSources(
49
+ name,
50
+ version,
51
+ packageSources,
52
+ platform,
53
+ httpClient,
54
+ timeoutMs,
55
+ destZipPath,
56
+ ) {
57
+ for (const source of packageSources) {
58
+ const base = source.replace(/\/$/, '');
59
+ const candidates = [
60
+ `${base}/${platform}/${name}/${version}/wyvrn.zip`,
61
+ `${base}/${platform}/${name}/${version}/razer.zip`,
62
+ ];
63
+ for (const url of candidates) {
64
+ try {
65
+ await downloadFile(url, destZipPath, httpClient, timeoutMs);
66
+ return true;
67
+ } catch (err) {
68
+ console.warn(`[wyvrn] Could not download ${name}@${version} from ${url}: ${err.message}`);
69
+ }
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+
75
+ /**
76
+ * Downloads and extracts all resolved dependencies that are not already present on disk.
77
+ *
78
+ * @param {Map<string, string>} resolvedDeps - Map of package name -> version string.
79
+ * @param {string[]} packageSources - Ordered list of base URLs to try.
80
+ * @param {string} platform - Target platform string (e.g. "win_x64").
81
+ * @param {string} razerDir - Local directory used for extraction (e.g. "wyvrn_internal").
82
+ * @param {Function} httpClient - fetch-compatible function.
83
+ * @param {number} timeout - HTTP timeout in seconds.
84
+ * @returns {Promise<void>}
85
+ */
86
+ async function downloadDependencies(
87
+ resolvedDeps,
88
+ packageSources,
89
+ platform,
90
+ razerDir,
91
+ httpClient,
92
+ timeout,
93
+ ) {
94
+ const timeoutMs = timeout * 1000;
95
+
96
+ if (!fs.existsSync(razerDir)) {
97
+ fs.mkdirSync(razerDir, { recursive: true });
98
+ }
99
+
100
+ for (const [name, version] of resolvedDeps) {
101
+ const extractDir = path.join(razerDir, name);
102
+ const versionFile = path.join(extractDir, '.wyvrn-version');
103
+
104
+ if (fs.existsSync(extractDir)) {
105
+ const installedVersion = fs.existsSync(versionFile)
106
+ ? fs.readFileSync(versionFile, 'utf8').trim()
107
+ : null;
108
+ if (installedVersion === version) {
109
+ console.log(`[wyvrn] Already present: ${name}@${version} — skipping`);
110
+ continue;
111
+ }
112
+ console.log(`[wyvrn] Version changed: ${name} ${installedVersion} → ${version}, re-downloading`);
113
+ fs.rmSync(extractDir, { recursive: true, force: true });
114
+ }
115
+
116
+ console.log(`[wyvrn] Downloading: ${name}@${version}`);
117
+
118
+ const destZipPath = path.join(razerDir, `${name}.zip`);
119
+
120
+ const ok = await tryDownloadFromSources(
121
+ name,
122
+ version,
123
+ packageSources,
124
+ platform,
125
+ httpClient,
126
+ timeoutMs,
127
+ destZipPath,
128
+ );
129
+
130
+ if (!ok) {
131
+ console.warn(
132
+ `[wyvrn] Warning: package ${name}@${version} was not found in any configured source`,
133
+ );
134
+ continue;
135
+ }
136
+
137
+ try {
138
+ const zip = new AdmZip(destZipPath);
139
+ zip.extractAllTo(extractDir, /* overwrite */ true);
140
+ fs.writeFileSync(versionFile, version, 'utf8');
141
+ console.log(`[wyvrn] Extracted: ${name}@${version} → ${extractDir}`);
142
+ } catch (err) {
143
+ console.warn(`[wyvrn] Failed to extract ${name}@${version}: ${err.message}`);
144
+ } finally {
145
+ // Always remove the zip, even if extraction failed.
146
+ try {
147
+ fs.unlinkSync(destZipPath);
148
+ } catch {
149
+ // Ignore cleanup errors.
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ module.exports = {
156
+ downloadDependencies,
157
+ };
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ init: require('./commands/init'),
5
+ install: require('./commands/install'),
6
+ clean: require('./commands/clean'),
7
+ };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Valid project kind values, matching the C# serialisation enum.
8
+ * @type {string[]}
9
+ */
10
+ const VALID_KINDS = [
11
+ 'ConsoleApp',
12
+ 'StaticLib',
13
+ 'DynamicLib',
14
+ 'HeaderOnlyLib',
15
+ 'WebApp',
16
+ 'Utility',
17
+ 'Service',
18
+ 'TestProject',
19
+ ];
20
+
21
+ /**
22
+ * Returns a blank manifest object populated with sensible defaults.
23
+ *
24
+ * @param {string} name - Project name (typically the directory basename).
25
+ * @returns {object}
26
+ */
27
+ function defaultManifest(name) {
28
+ return {
29
+ name: name,
30
+ version: '1.0.0.0',
31
+ description: '',
32
+ kind: 'ConsoleApp',
33
+ dependencies: {},
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Reads and parses a wyvrn.json manifest from disk.
39
+ *
40
+ * @param {string} manifestPath - Absolute or relative path to wyvrn.json.
41
+ * @returns {object} Parsed manifest data.
42
+ * @throws {Error} If the file does not exist or contains invalid JSON.
43
+ */
44
+ function readManifest(manifestPath) {
45
+ const resolved = path.resolve(manifestPath);
46
+ if (!fs.existsSync(resolved)) {
47
+ throw new Error(`Manifest not found: ${resolved}`);
48
+ }
49
+ const raw = fs.readFileSync(resolved, 'utf8');
50
+ try {
51
+ return JSON.parse(raw);
52
+ } catch (err) {
53
+ throw new Error(`Failed to parse manifest at ${resolved}: ${err.message}`);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Serialises and writes manifest data to disk with 2-space indentation.
59
+ *
60
+ * @param {string} manifestPath - Absolute or relative path to wyvrn.json.
61
+ * @param {object} data - Manifest object to serialise.
62
+ */
63
+ function writeManifest(manifestPath, data) {
64
+ const resolved = path.resolve(manifestPath);
65
+ const dir = path.dirname(resolved);
66
+ if (!fs.existsSync(dir)) {
67
+ fs.mkdirSync(dir, { recursive: true });
68
+ }
69
+ fs.writeFileSync(resolved, JSON.stringify(data, null, 2) + '\n', 'utf8');
70
+ }
71
+
72
+ module.exports = {
73
+ VALID_KINDS,
74
+ defaultManifest,
75
+ readManifest,
76
+ writeManifest,
77
+ };
package/src/resolve.js ADDED
@@ -0,0 +1,148 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Compares two version strings of the form "major.minor.patch.build".
5
+ * Each missing component is treated as 0.
6
+ *
7
+ * @param {string} a - First version string.
8
+ * @param {string} b - Second version string.
9
+ * @returns {-1|0|1} -1 if a < b, 0 if equal, 1 if a > b.
10
+ */
11
+ function compareVersions(a, b) {
12
+ const parsePart = (s) => s.split('.').map((n) => parseInt(n, 10) || 0);
13
+ const pa = parsePart(a);
14
+ const pb = parsePart(b);
15
+ const len = Math.max(pa.length, pb.length);
16
+ for (let i = 0; i < len; i++) {
17
+ const na = pa[i] !== undefined ? pa[i] : 0;
18
+ const nb = pb[i] !== undefined ? pb[i] : 0;
19
+ if (na < nb) return -1;
20
+ if (na > nb) return 1;
21
+ }
22
+ return 0;
23
+ }
24
+
25
+ /**
26
+ * Fetches razer.json for a single package from the first source that responds.
27
+ *
28
+ * @param {string} name
29
+ * @param {string} version
30
+ * @param {string[]} packageSources
31
+ * @param {string} platform
32
+ * @param {Function} httpClient
33
+ * @param {Map<string, object>} cache - In-flight / completed fetch cache.
34
+ * @returns {Promise<object|null>} Parsed razer.json or null if not found anywhere.
35
+ */
36
+ async function fetchPackageManifest(name, version, packageSources, platform, httpClient, cache) {
37
+ const cacheKey = `${name}@${version}`;
38
+ if (cache.has(cacheKey)) {
39
+ return cache.get(cacheKey);
40
+ }
41
+
42
+ for (const source of packageSources) {
43
+ const base = source.replace(/\/$/, '');
44
+ const candidates = [
45
+ `${base}/${platform}/${name}/${version}/wyvrn.json`,
46
+ `${base}/${platform}/${name}/${version}/razer.json`,
47
+ ];
48
+ for (const url of candidates) {
49
+ try {
50
+ const response = await httpClient(url);
51
+ if (!response.ok) continue;
52
+ const data = await response.json();
53
+ cache.set(cacheKey, data);
54
+ return data;
55
+ } catch {
56
+ // Try next candidate.
57
+ }
58
+ }
59
+ }
60
+
61
+ console.warn(`[wyvrn] Warning: could not fetch manifest for ${name}@${version} from any source`);
62
+ cache.set(cacheKey, null);
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Resolves the full, flattened dependency graph starting from rootDeps.
68
+ * When the same package is required at multiple versions the highest version wins.
69
+ *
70
+ * @param {Record<string, string>} rootDeps - Direct dependencies { name: version }.
71
+ * @param {string[]} packageSources - Ordered list of base URLs to try.
72
+ * @param {string} platform - Target platform string (e.g. "win_x64").
73
+ * @param {Function} httpClient - fetch-compatible function.
74
+ * @returns {Promise<Map<string, string>>} Resolved map of name -> version.
75
+ */
76
+ async function resolveDependencies(rootDeps, packageSources, platform, httpClient, lockedVersions = new Map()) {
77
+ /** @type {Map<string, string>} Final resolved versions. */
78
+ const resolved = new Map();
79
+
80
+ /** @type {Map<string, object|null>} Manifest cache to avoid duplicate fetches. */
81
+ const manifestCache = new Map();
82
+
83
+ /**
84
+ * Pending work queue: each entry is { name, version }.
85
+ * We process iteratively to avoid deep recursion on large dependency trees.
86
+ */
87
+ const queue = Object.entries(rootDeps).map(([name, version]) => ({ name, version }));
88
+
89
+ while (queue.length > 0) {
90
+ let { name, version } = queue.shift();
91
+
92
+ // Lock file always wins — use pinned version regardless of what was requested.
93
+ if (lockedVersions.has(name)) {
94
+ version = lockedVersions.get(name);
95
+ }
96
+
97
+ if (resolved.has(name)) {
98
+ const existing = resolved.get(name);
99
+ if (existing === version) {
100
+ continue;
101
+ }
102
+ // Locked packages never lose to a conflict.
103
+ if (lockedVersions.has(name)) {
104
+ continue;
105
+ }
106
+ // Conflict: keep the latest version.
107
+ const winner = compareVersions(version, existing) > 0 ? version : existing;
108
+ if (winner !== existing) {
109
+ console.warn(
110
+ `[wyvrn] Version conflict for "${name}": ${existing} vs ${version} — using ${winner}`,
111
+ );
112
+ resolved.set(name, winner);
113
+ // Re-fetch the winning version's manifest to pull in its transitive deps.
114
+ queue.push({ name, version: winner });
115
+ }
116
+ continue;
117
+ }
118
+
119
+ resolved.set(name, version);
120
+
121
+ const manifest = await fetchPackageManifest(
122
+ name,
123
+ version,
124
+ packageSources,
125
+ platform,
126
+ httpClient,
127
+ manifestCache,
128
+ );
129
+ if (!manifest) {
130
+ continue;
131
+ }
132
+
133
+ // razer.json uses PascalCase: { Dependencies: [ { Name, Version }, ... ] }
134
+ const deps = Array.isArray(manifest.Dependencies) ? manifest.Dependencies : [];
135
+ for (const dep of deps) {
136
+ if (dep.Name && dep.Version) {
137
+ queue.push({ name: dep.Name, version: dep.Version });
138
+ }
139
+ }
140
+ }
141
+
142
+ return resolved;
143
+ }
144
+
145
+ module.exports = {
146
+ compareVersions,
147
+ resolveDependencies,
148
+ };