wyvrnpm 2.10.2 → 2.12.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.
Files changed (49) hide show
  1. package/README.md +1914 -1860
  2. package/bin/{wyvrn.js → wyvrnpm.js} +66 -0
  3. package/cmake/cpp.cmake +9 -9
  4. package/cmake/functions.cmake +224 -224
  5. package/cmake/macros.cmake +284 -284
  6. package/package.json +3 -2
  7. package/src/auth.js +66 -66
  8. package/src/binary-dir.js +95 -0
  9. package/src/bootstrap/cookbook.js +196 -196
  10. package/src/bootstrap/detect.js +150 -150
  11. package/src/bootstrap/index.js +220 -220
  12. package/src/bootstrap/version.js +72 -72
  13. package/src/build/cache.js +344 -344
  14. package/src/build/clone.js +170 -170
  15. package/src/build/cmake.js +342 -342
  16. package/src/build/index.js +299 -297
  17. package/src/build/msvc-env.js +260 -260
  18. package/src/build/recipe.js +188 -188
  19. package/src/commands/add.js +141 -141
  20. package/src/commands/bootstrap.js +96 -96
  21. package/src/commands/build.js +482 -452
  22. package/src/commands/cache.js +189 -189
  23. package/src/commands/clean.js +80 -80
  24. package/src/commands/configure.js +92 -92
  25. package/src/commands/init.js +70 -70
  26. package/src/commands/install-skill.js +115 -115
  27. package/src/commands/install.js +730 -674
  28. package/src/commands/link.js +320 -320
  29. package/src/commands/profile.js +237 -237
  30. package/src/commands/publish.js +584 -555
  31. package/src/commands/show.js +252 -252
  32. package/src/commands/version.js +187 -0
  33. package/src/compat.js +273 -273
  34. package/src/conf/index.js +415 -391
  35. package/src/conf/namespaces.js +94 -94
  36. package/src/context.js +230 -230
  37. package/src/http-fetch.js +53 -53
  38. package/src/ignore.js +118 -118
  39. package/src/logger.js +122 -122
  40. package/src/options.js +303 -303
  41. package/src/settings-overrides.js +152 -152
  42. package/src/toolchain/deps.js +164 -164
  43. package/src/toolchain/index.js +212 -212
  44. package/src/toolchain/presets.js +356 -340
  45. package/src/toolchain/template.cmake +77 -77
  46. package/src/upload-built.js +265 -265
  47. package/src/version-range.js +301 -301
  48. package/src/zip-safe.js +52 -52
  49. package/src/zip-stream.js +126 -0
@@ -1,301 +1,301 @@
1
- 'use strict';
2
-
3
- /**
4
- * Version range parsing and matching for 4-part `major.minor.patch.build`
5
- * versions.
6
- *
7
- * Accepts three range syntaxes in addition to the existing exact and
8
- * `"latest"` strings:
9
- *
10
- * "^1.2.3.4" major-pinned : >= 1.2.3.4 < 2.0.0.0
11
- * "~1.2.3.4" minor-pinned : >= 1.2.3.4 < 1.3.0.0
12
- * ">=1.2.3.4 <2.0" explicit : one or more space-separated comparators
13
- * among >=, >, <=, <, =
14
- *
15
- * `parseRange(s)` classifies the input; `matchesRange(v, parsed)`
16
- * evaluates a candidate version; `pickHighestInRange(versions, parsed)`
17
- * selects the best match; `intersectRanges(a, b)` combines two ranges
18
- * (returns null when the intersection is empty).
19
- *
20
- * See claude/PLAN-VERSION-RANGES.md for the full design.
21
- */
22
-
23
- const VERSION_RE = /^\d+\.\d+\.\d+\.\d+$/;
24
-
25
- /**
26
- * Compare two 4-part versions numerically. Missing trailing components
27
- * default to 0 so `compareVersions("1.2", "1.2.0.0") === 0`, though the
28
- * parser elsewhere always emits fully-qualified 4-part strings.
29
- *
30
- * @param {string} a
31
- * @param {string} b
32
- * @returns {-1|0|1}
33
- */
34
- function compareVersions(a, b) {
35
- const partsOf = (s) => s.split('.').map((n) => parseInt(n, 10) || 0);
36
- const pa = partsOf(a);
37
- const pb = partsOf(b);
38
- const len = Math.max(pa.length, pb.length);
39
- for (let i = 0; i < len; i++) {
40
- const x = pa[i] ?? 0;
41
- const y = pb[i] ?? 0;
42
- if (x < y) return -1;
43
- if (x > y) return 1;
44
- }
45
- return 0;
46
- }
47
-
48
- function bumpMajor(v) {
49
- const [maj] = v.split('.').map((n) => parseInt(n, 10) || 0);
50
- return `${maj + 1}.0.0.0`;
51
- }
52
-
53
- function bumpMinor(v) {
54
- const [maj, min] = v.split('.').map((n) => parseInt(n, 10) || 0);
55
- return `${maj}.${min + 1}.0.0`;
56
- }
57
-
58
- /**
59
- * Parse a dependency version string into a structured spec. Exact versions
60
- * and `"latest"` pass through unchanged; `^` / `~` / comparator strings
61
- * become `{ kind: 'range', min, max }` specs.
62
- *
63
- * `min` and `max` are each `{ op: '>=' | '>' | null, v: string } | null`
64
- * (null means unbounded on that side — an open upper bound, etc.).
65
- *
66
- * Throws on malformed input with a user-friendly multi-line error that
67
- * lists the supported syntaxes.
68
- *
69
- * @param {string} spec
70
- * @returns {{ kind: 'exact', version: string }
71
- * | { kind: 'tag', tag: 'latest' }
72
- * | { kind: 'range', raw: string,
73
- * min: { op: '>='|'>', v: string } | null,
74
- * max: { op: '<='|'<', v: string } | null,
75
- * exact?: string }}
76
- */
77
- function parseRange(spec) {
78
- if (typeof spec !== 'string' || spec.length === 0) {
79
- throw new Error(malformedRangeMessage(spec, 'version spec must be a non-empty string'));
80
- }
81
- const s = spec.trim();
82
-
83
- if (s === 'latest') return { kind: 'tag', tag: 'latest' };
84
-
85
- // `linked:` is handled by resolve.js directly — not our concern.
86
- if (s.startsWith('linked:')) return { kind: 'linked', raw: s };
87
-
88
- if (VERSION_RE.test(s)) return { kind: 'exact', version: s };
89
-
90
- // Caret / tilde — require a full 4-part version as the operand.
91
- if (s[0] === '^' || s[0] === '~') {
92
- const op = s[0];
93
- const v = s.slice(1).trim();
94
- if (!VERSION_RE.test(v)) {
95
- throw new Error(malformedRangeMessage(spec, `${op} prefix requires a 4-part version`));
96
- }
97
- return {
98
- kind: 'range',
99
- raw: s,
100
- min: { op: '>=', v },
101
- max: { op: '<', v: op === '^' ? bumpMajor(v) : bumpMinor(v) },
102
- };
103
- }
104
-
105
- // Explicit comparator form: one or more of {>=, >, <=, <, =} each followed
106
- // by a 4-part version, space-separated.
107
- if (/^[<>=]/.test(s)) {
108
- const tokens = s.split(/\s+/);
109
- let min = null;
110
- let max = null;
111
- let exact = null;
112
- for (const tok of tokens) {
113
- const m = tok.match(/^(>=|<=|>|<|=)(\d+\.\d+\.\d+\.\d+)$/);
114
- if (!m) {
115
- throw new Error(malformedRangeMessage(spec, `unrecognised comparator "${tok}"`));
116
- }
117
- const [, op, v] = m;
118
- if (op === '=') {
119
- if (exact && exact !== v) {
120
- throw new Error(malformedRangeMessage(spec, `conflicting = comparators (${exact} vs ${v})`));
121
- }
122
- exact = v;
123
- } else if (op === '>=' || op === '>') {
124
- if (!min || compareVersions(v, min.v) > 0 || (compareVersions(v, min.v) === 0 && op === '>')) {
125
- min = { op, v };
126
- }
127
- } else {
128
- if (!max || compareVersions(v, max.v) < 0 || (compareVersions(v, max.v) === 0 && op === '<')) {
129
- max = { op, v };
130
- }
131
- }
132
- }
133
- if (exact) return { kind: 'exact', version: exact };
134
- if (!min && !max) {
135
- throw new Error(malformedRangeMessage(spec, 'no comparators parsed'));
136
- }
137
- return { kind: 'range', raw: s, min, max };
138
- }
139
-
140
- throw new Error(malformedRangeMessage(spec, 'unknown version syntax'));
141
- }
142
-
143
- function malformedRangeMessage(spec, why) {
144
- return (
145
- `cannot parse dependency version ${JSON.stringify(spec)}: ${why}\n` +
146
- ` supported syntaxes:\n` +
147
- ` "1.2.3.4" exact version\n` +
148
- ` "latest" latest-published tag\n` +
149
- ` "^1.2.3.4" major-pinned range (>= 1.2.3.4 < 2.0.0.0)\n` +
150
- ` "~1.2.3.4" minor-pinned range (>= 1.2.3.4 < 1.3.0.0)\n` +
151
- ` ">=1.2.3.4 <2.0.0.0" explicit range (>=, >, <=, <, = separated by spaces)`
152
- );
153
- }
154
-
155
- /**
156
- * Returns true if `version` satisfies every bound in `parsed`. Exact-kind
157
- * specs require byte-equal; tag/linked specs always return false here
158
- * because they are handled upstream.
159
- *
160
- * @param {string} version
161
- * @param {ReturnType<typeof parseRange>} parsed
162
- * @returns {boolean}
163
- */
164
- function matchesRange(version, parsed) {
165
- if (!parsed) return false;
166
- if (parsed.kind === 'exact') return version === parsed.version;
167
- if (parsed.kind !== 'range') return false;
168
- if (parsed.min) {
169
- const cmp = compareVersions(version, parsed.min.v);
170
- if (parsed.min.op === '>=' && cmp < 0) return false;
171
- if (parsed.min.op === '>' && cmp <= 0) return false;
172
- }
173
- if (parsed.max) {
174
- const cmp = compareVersions(version, parsed.max.v);
175
- if (parsed.max.op === '<=' && cmp > 0) return false;
176
- if (parsed.max.op === '<' && cmp >= 0) return false;
177
- }
178
- return true;
179
- }
180
-
181
- /**
182
- * Pick the highest version from `publishedVersions` that satisfies
183
- * `parsed`. Returns null when no version matches. Input is not mutated.
184
- *
185
- * @param {string[]} publishedVersions
186
- * @param {ReturnType<typeof parseRange>} parsed
187
- * @returns {string|null}
188
- */
189
- function pickHighestInRange(publishedVersions, parsed) {
190
- if (!Array.isArray(publishedVersions) || publishedVersions.length === 0) return null;
191
- const matches = publishedVersions.filter((v) => matchesRange(v, parsed));
192
- if (matches.length === 0) return null;
193
- matches.sort(compareVersions);
194
- return matches[matches.length - 1];
195
- }
196
-
197
- /**
198
- * Intersect two parsed range specs. Exact specs are treated as the degenerate
199
- * range `[v, v]`. Returns a new range spec or null when the intersection is
200
- * empty.
201
- *
202
- * `tag` and `linked` kinds cannot participate in intersection — the caller
203
- * must resolve them to exact first. We throw rather than silently ignore.
204
- *
205
- * @param {ReturnType<typeof parseRange>} a
206
- * @param {ReturnType<typeof parseRange>} b
207
- * @returns {{ kind: 'range'|'exact', ... }|null}
208
- */
209
- function intersectRanges(a, b) {
210
- const liftExact = (spec) => spec.kind === 'exact'
211
- ? { kind: 'range', raw: spec.version, min: { op: '>=', v: spec.version }, max: { op: '<=', v: spec.version } }
212
- : spec;
213
-
214
- if (!a || !b) throw new Error('intersectRanges: null spec');
215
- if (a.kind === 'tag' || a.kind === 'linked' || b.kind === 'tag' || b.kind === 'linked') {
216
- throw new Error('intersectRanges: tag/linked must be resolved before intersection');
217
- }
218
-
219
- const ra = liftExact(a);
220
- const rb = liftExact(b);
221
-
222
- // Tighter min (higher value; strictly-greater beats equal under gte).
223
- const tighterMin = pickTighter(ra.min, rb.min, 'min');
224
- // Tighter max (lower value; strictly-less beats equal under lte).
225
- const tighterMax = pickTighter(ra.max, rb.max, 'max');
226
-
227
- if (tighterMin && tighterMax) {
228
- const cmp = compareVersions(tighterMin.v, tighterMax.v);
229
- if (cmp > 0) return null;
230
- if (cmp === 0) {
231
- // Both bounds reference the same version. Feasible only if both are
232
- // inclusive; otherwise the point is excluded.
233
- if (tighterMin.op !== '>=' || tighterMax.op !== '<=') return null;
234
- return { kind: 'exact', version: tighterMin.v };
235
- }
236
- }
237
-
238
- return {
239
- kind: 'range',
240
- raw: `${ra.raw ?? 'range'} ∩ ${rb.raw ?? 'range'}`,
241
- min: tighterMin,
242
- max: tighterMax,
243
- };
244
- }
245
-
246
- function pickTighter(a, b, kind) {
247
- if (!a) return b;
248
- if (!b) return a;
249
- const cmp = compareVersions(a.v, b.v);
250
- if (kind === 'min') {
251
- if (cmp > 0) return a;
252
- if (cmp < 0) return b;
253
- // equal: strictly-greater is tighter than gte
254
- return a.op === '>' ? a : b;
255
- } else {
256
- if (cmp < 0) return a;
257
- if (cmp > 0) return b;
258
- return a.op === '<' ? a : b;
259
- }
260
- }
261
-
262
- /**
263
- * Human-readable rendering for error messages. Exact and tag kinds pass
264
- * through unchanged; ranges render as the original raw string when
265
- * available, else as a comparator reconstruction.
266
- *
267
- * @param {ReturnType<typeof parseRange>} parsed
268
- * @returns {string}
269
- */
270
- function rangeToString(parsed) {
271
- if (!parsed) return '(unparsed)';
272
- if (parsed.kind === 'exact') return parsed.version;
273
- if (parsed.kind === 'tag') return parsed.tag;
274
- if (parsed.kind === 'linked') return parsed.raw;
275
- if (parsed.raw && !parsed.raw.includes('∩')) return parsed.raw;
276
- const parts = [];
277
- if (parsed.min) parts.push(`${parsed.min.op}${parsed.min.v}`);
278
- if (parsed.max) parts.push(`${parsed.max.op}${parsed.max.v}`);
279
- return parts.join(' ');
280
- }
281
-
282
- /**
283
- * Convenience predicate used by the resolver: does this spec require
284
- * range-based resolution (as opposed to exact / latest / linked)?
285
- */
286
- function isRangeSpec(spec) {
287
- try {
288
- const parsed = parseRange(spec);
289
- return parsed.kind === 'range';
290
- } catch { return false; }
291
- }
292
-
293
- module.exports = {
294
- parseRange,
295
- matchesRange,
296
- pickHighestInRange,
297
- intersectRanges,
298
- rangeToString,
299
- isRangeSpec,
300
- compareVersions,
301
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Version range parsing and matching for 4-part `major.minor.patch.build`
5
+ * versions.
6
+ *
7
+ * Accepts three range syntaxes in addition to the existing exact and
8
+ * `"latest"` strings:
9
+ *
10
+ * "^1.2.3.4" major-pinned : >= 1.2.3.4 < 2.0.0.0
11
+ * "~1.2.3.4" minor-pinned : >= 1.2.3.4 < 1.3.0.0
12
+ * ">=1.2.3.4 <2.0" explicit : one or more space-separated comparators
13
+ * among >=, >, <=, <, =
14
+ *
15
+ * `parseRange(s)` classifies the input; `matchesRange(v, parsed)`
16
+ * evaluates a candidate version; `pickHighestInRange(versions, parsed)`
17
+ * selects the best match; `intersectRanges(a, b)` combines two ranges
18
+ * (returns null when the intersection is empty).
19
+ *
20
+ * See claude/PLAN-VERSION-RANGES.md for the full design.
21
+ */
22
+
23
+ const VERSION_RE = /^\d+\.\d+\.\d+\.\d+$/;
24
+
25
+ /**
26
+ * Compare two 4-part versions numerically. Missing trailing components
27
+ * default to 0 so `compareVersions("1.2", "1.2.0.0") === 0`, though the
28
+ * parser elsewhere always emits fully-qualified 4-part strings.
29
+ *
30
+ * @param {string} a
31
+ * @param {string} b
32
+ * @returns {-1|0|1}
33
+ */
34
+ function compareVersions(a, b) {
35
+ const partsOf = (s) => s.split('.').map((n) => parseInt(n, 10) || 0);
36
+ const pa = partsOf(a);
37
+ const pb = partsOf(b);
38
+ const len = Math.max(pa.length, pb.length);
39
+ for (let i = 0; i < len; i++) {
40
+ const x = pa[i] ?? 0;
41
+ const y = pb[i] ?? 0;
42
+ if (x < y) return -1;
43
+ if (x > y) return 1;
44
+ }
45
+ return 0;
46
+ }
47
+
48
+ function bumpMajor(v) {
49
+ const [maj] = v.split('.').map((n) => parseInt(n, 10) || 0);
50
+ return `${maj + 1}.0.0.0`;
51
+ }
52
+
53
+ function bumpMinor(v) {
54
+ const [maj, min] = v.split('.').map((n) => parseInt(n, 10) || 0);
55
+ return `${maj}.${min + 1}.0.0`;
56
+ }
57
+
58
+ /**
59
+ * Parse a dependency version string into a structured spec. Exact versions
60
+ * and `"latest"` pass through unchanged; `^` / `~` / comparator strings
61
+ * become `{ kind: 'range', min, max }` specs.
62
+ *
63
+ * `min` and `max` are each `{ op: '>=' | '>' | null, v: string } | null`
64
+ * (null means unbounded on that side — an open upper bound, etc.).
65
+ *
66
+ * Throws on malformed input with a user-friendly multi-line error that
67
+ * lists the supported syntaxes.
68
+ *
69
+ * @param {string} spec
70
+ * @returns {{ kind: 'exact', version: string }
71
+ * | { kind: 'tag', tag: 'latest' }
72
+ * | { kind: 'range', raw: string,
73
+ * min: { op: '>='|'>', v: string } | null,
74
+ * max: { op: '<='|'<', v: string } | null,
75
+ * exact?: string }}
76
+ */
77
+ function parseRange(spec) {
78
+ if (typeof spec !== 'string' || spec.length === 0) {
79
+ throw new Error(malformedRangeMessage(spec, 'version spec must be a non-empty string'));
80
+ }
81
+ const s = spec.trim();
82
+
83
+ if (s === 'latest') return { kind: 'tag', tag: 'latest' };
84
+
85
+ // `linked:` is handled by resolve.js directly — not our concern.
86
+ if (s.startsWith('linked:')) return { kind: 'linked', raw: s };
87
+
88
+ if (VERSION_RE.test(s)) return { kind: 'exact', version: s };
89
+
90
+ // Caret / tilde — require a full 4-part version as the operand.
91
+ if (s[0] === '^' || s[0] === '~') {
92
+ const op = s[0];
93
+ const v = s.slice(1).trim();
94
+ if (!VERSION_RE.test(v)) {
95
+ throw new Error(malformedRangeMessage(spec, `${op} prefix requires a 4-part version`));
96
+ }
97
+ return {
98
+ kind: 'range',
99
+ raw: s,
100
+ min: { op: '>=', v },
101
+ max: { op: '<', v: op === '^' ? bumpMajor(v) : bumpMinor(v) },
102
+ };
103
+ }
104
+
105
+ // Explicit comparator form: one or more of {>=, >, <=, <, =} each followed
106
+ // by a 4-part version, space-separated.
107
+ if (/^[<>=]/.test(s)) {
108
+ const tokens = s.split(/\s+/);
109
+ let min = null;
110
+ let max = null;
111
+ let exact = null;
112
+ for (const tok of tokens) {
113
+ const m = tok.match(/^(>=|<=|>|<|=)(\d+\.\d+\.\d+\.\d+)$/);
114
+ if (!m) {
115
+ throw new Error(malformedRangeMessage(spec, `unrecognised comparator "${tok}"`));
116
+ }
117
+ const [, op, v] = m;
118
+ if (op === '=') {
119
+ if (exact && exact !== v) {
120
+ throw new Error(malformedRangeMessage(spec, `conflicting = comparators (${exact} vs ${v})`));
121
+ }
122
+ exact = v;
123
+ } else if (op === '>=' || op === '>') {
124
+ if (!min || compareVersions(v, min.v) > 0 || (compareVersions(v, min.v) === 0 && op === '>')) {
125
+ min = { op, v };
126
+ }
127
+ } else {
128
+ if (!max || compareVersions(v, max.v) < 0 || (compareVersions(v, max.v) === 0 && op === '<')) {
129
+ max = { op, v };
130
+ }
131
+ }
132
+ }
133
+ if (exact) return { kind: 'exact', version: exact };
134
+ if (!min && !max) {
135
+ throw new Error(malformedRangeMessage(spec, 'no comparators parsed'));
136
+ }
137
+ return { kind: 'range', raw: s, min, max };
138
+ }
139
+
140
+ throw new Error(malformedRangeMessage(spec, 'unknown version syntax'));
141
+ }
142
+
143
+ function malformedRangeMessage(spec, why) {
144
+ return (
145
+ `cannot parse dependency version ${JSON.stringify(spec)}: ${why}\n` +
146
+ ` supported syntaxes:\n` +
147
+ ` "1.2.3.4" exact version\n` +
148
+ ` "latest" latest-published tag\n` +
149
+ ` "^1.2.3.4" major-pinned range (>= 1.2.3.4 < 2.0.0.0)\n` +
150
+ ` "~1.2.3.4" minor-pinned range (>= 1.2.3.4 < 1.3.0.0)\n` +
151
+ ` ">=1.2.3.4 <2.0.0.0" explicit range (>=, >, <=, <, = separated by spaces)`
152
+ );
153
+ }
154
+
155
+ /**
156
+ * Returns true if `version` satisfies every bound in `parsed`. Exact-kind
157
+ * specs require byte-equal; tag/linked specs always return false here
158
+ * because they are handled upstream.
159
+ *
160
+ * @param {string} version
161
+ * @param {ReturnType<typeof parseRange>} parsed
162
+ * @returns {boolean}
163
+ */
164
+ function matchesRange(version, parsed) {
165
+ if (!parsed) return false;
166
+ if (parsed.kind === 'exact') return version === parsed.version;
167
+ if (parsed.kind !== 'range') return false;
168
+ if (parsed.min) {
169
+ const cmp = compareVersions(version, parsed.min.v);
170
+ if (parsed.min.op === '>=' && cmp < 0) return false;
171
+ if (parsed.min.op === '>' && cmp <= 0) return false;
172
+ }
173
+ if (parsed.max) {
174
+ const cmp = compareVersions(version, parsed.max.v);
175
+ if (parsed.max.op === '<=' && cmp > 0) return false;
176
+ if (parsed.max.op === '<' && cmp >= 0) return false;
177
+ }
178
+ return true;
179
+ }
180
+
181
+ /**
182
+ * Pick the highest version from `publishedVersions` that satisfies
183
+ * `parsed`. Returns null when no version matches. Input is not mutated.
184
+ *
185
+ * @param {string[]} publishedVersions
186
+ * @param {ReturnType<typeof parseRange>} parsed
187
+ * @returns {string|null}
188
+ */
189
+ function pickHighestInRange(publishedVersions, parsed) {
190
+ if (!Array.isArray(publishedVersions) || publishedVersions.length === 0) return null;
191
+ const matches = publishedVersions.filter((v) => matchesRange(v, parsed));
192
+ if (matches.length === 0) return null;
193
+ matches.sort(compareVersions);
194
+ return matches[matches.length - 1];
195
+ }
196
+
197
+ /**
198
+ * Intersect two parsed range specs. Exact specs are treated as the degenerate
199
+ * range `[v, v]`. Returns a new range spec or null when the intersection is
200
+ * empty.
201
+ *
202
+ * `tag` and `linked` kinds cannot participate in intersection — the caller
203
+ * must resolve them to exact first. We throw rather than silently ignore.
204
+ *
205
+ * @param {ReturnType<typeof parseRange>} a
206
+ * @param {ReturnType<typeof parseRange>} b
207
+ * @returns {{ kind: 'range'|'exact', ... }|null}
208
+ */
209
+ function intersectRanges(a, b) {
210
+ const liftExact = (spec) => spec.kind === 'exact'
211
+ ? { kind: 'range', raw: spec.version, min: { op: '>=', v: spec.version }, max: { op: '<=', v: spec.version } }
212
+ : spec;
213
+
214
+ if (!a || !b) throw new Error('intersectRanges: null spec');
215
+ if (a.kind === 'tag' || a.kind === 'linked' || b.kind === 'tag' || b.kind === 'linked') {
216
+ throw new Error('intersectRanges: tag/linked must be resolved before intersection');
217
+ }
218
+
219
+ const ra = liftExact(a);
220
+ const rb = liftExact(b);
221
+
222
+ // Tighter min (higher value; strictly-greater beats equal under gte).
223
+ const tighterMin = pickTighter(ra.min, rb.min, 'min');
224
+ // Tighter max (lower value; strictly-less beats equal under lte).
225
+ const tighterMax = pickTighter(ra.max, rb.max, 'max');
226
+
227
+ if (tighterMin && tighterMax) {
228
+ const cmp = compareVersions(tighterMin.v, tighterMax.v);
229
+ if (cmp > 0) return null;
230
+ if (cmp === 0) {
231
+ // Both bounds reference the same version. Feasible only if both are
232
+ // inclusive; otherwise the point is excluded.
233
+ if (tighterMin.op !== '>=' || tighterMax.op !== '<=') return null;
234
+ return { kind: 'exact', version: tighterMin.v };
235
+ }
236
+ }
237
+
238
+ return {
239
+ kind: 'range',
240
+ raw: `${ra.raw ?? 'range'} ∩ ${rb.raw ?? 'range'}`,
241
+ min: tighterMin,
242
+ max: tighterMax,
243
+ };
244
+ }
245
+
246
+ function pickTighter(a, b, kind) {
247
+ if (!a) return b;
248
+ if (!b) return a;
249
+ const cmp = compareVersions(a.v, b.v);
250
+ if (kind === 'min') {
251
+ if (cmp > 0) return a;
252
+ if (cmp < 0) return b;
253
+ // equal: strictly-greater is tighter than gte
254
+ return a.op === '>' ? a : b;
255
+ } else {
256
+ if (cmp < 0) return a;
257
+ if (cmp > 0) return b;
258
+ return a.op === '<' ? a : b;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Human-readable rendering for error messages. Exact and tag kinds pass
264
+ * through unchanged; ranges render as the original raw string when
265
+ * available, else as a comparator reconstruction.
266
+ *
267
+ * @param {ReturnType<typeof parseRange>} parsed
268
+ * @returns {string}
269
+ */
270
+ function rangeToString(parsed) {
271
+ if (!parsed) return '(unparsed)';
272
+ if (parsed.kind === 'exact') return parsed.version;
273
+ if (parsed.kind === 'tag') return parsed.tag;
274
+ if (parsed.kind === 'linked') return parsed.raw;
275
+ if (parsed.raw && !parsed.raw.includes('∩')) return parsed.raw;
276
+ const parts = [];
277
+ if (parsed.min) parts.push(`${parsed.min.op}${parsed.min.v}`);
278
+ if (parsed.max) parts.push(`${parsed.max.op}${parsed.max.v}`);
279
+ return parts.join(' ');
280
+ }
281
+
282
+ /**
283
+ * Convenience predicate used by the resolver: does this spec require
284
+ * range-based resolution (as opposed to exact / latest / linked)?
285
+ */
286
+ function isRangeSpec(spec) {
287
+ try {
288
+ const parsed = parseRange(spec);
289
+ return parsed.kind === 'range';
290
+ } catch { return false; }
291
+ }
292
+
293
+ module.exports = {
294
+ parseRange,
295
+ matchesRange,
296
+ pickHighestInRange,
297
+ intersectRanges,
298
+ rangeToString,
299
+ isRangeSpec,
300
+ compareVersions,
301
+ };