wyvrnpm 2.0.4 → 2.3.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 (44) hide show
  1. package/README.md +639 -5
  2. package/bin/wyvrn.js +220 -10
  3. package/claude/skills/wyvrnpm.skill +0 -0
  4. package/cmake/cpp.cmake +9 -0
  5. package/cmake/functions.cmake +224 -0
  6. package/cmake/macros.cmake +233 -0
  7. package/cmake/options.cmake +23 -0
  8. package/cmake/variables.cmake +171 -0
  9. package/package.json +3 -1
  10. package/src/build/cache.js +148 -0
  11. package/src/build/clone.js +170 -0
  12. package/src/build/cmake.js +342 -0
  13. package/src/build/index.js +275 -0
  14. package/src/build/msvc-env.js +217 -0
  15. package/src/build/recipe.js +155 -0
  16. package/src/commands/build.js +283 -0
  17. package/src/commands/clean.js +56 -16
  18. package/src/commands/configure.js +6 -5
  19. package/src/commands/init.js +3 -2
  20. package/src/commands/install-skill.js +107 -0
  21. package/src/commands/install.js +262 -19
  22. package/src/commands/link.js +18 -15
  23. package/src/commands/profile.js +15 -12
  24. package/src/commands/publish.js +216 -65
  25. package/src/commands/show.js +237 -0
  26. package/src/compat.js +261 -0
  27. package/src/config.js +3 -1
  28. package/src/download.js +431 -87
  29. package/src/ignore.js +118 -0
  30. package/src/logger.js +94 -0
  31. package/src/manifest.js +12 -7
  32. package/src/options.js +303 -0
  33. package/src/profile.js +56 -4
  34. package/src/providers/base.js +16 -1
  35. package/src/providers/file.js +12 -6
  36. package/src/providers/http.js +15 -10
  37. package/src/providers/s3.js +14 -9
  38. package/src/resolve.js +179 -19
  39. package/src/toolchain/deps.js +164 -0
  40. package/src/toolchain/index.js +141 -0
  41. package/src/toolchain/presets.js +263 -0
  42. package/src/toolchain/template.cmake +66 -0
  43. package/src/upload-built.js +256 -0
  44. package/src/version-range.js +301 -0
package/src/compat.js ADDED
@@ -0,0 +1,261 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Declarative compatibility evaluator for v2 build profiles.
5
+ *
6
+ * Publishers declare a `compatibility` block in their package's `wyvrn.json`
7
+ * stating which profile fields must match exactly vs which may differ.
8
+ * At install time, when no exact profileHash match exists, this module
9
+ * evaluates each registry-side build against the consumer's profile and
10
+ * picks a compatible one if any.
11
+ *
12
+ * The block format is per-field: either a string shorthand or an object.
13
+ *
14
+ * "compatibility": {
15
+ * "compiler.cppstd": "lte",
16
+ * "compiler.runtime": "exact",
17
+ * "compiler.version": "exact-or-newer"
18
+ * }
19
+ *
20
+ * equivalent object form (allows future extensions):
21
+ *
22
+ * "compatibility": {
23
+ * "compiler.cppstd": { "mode": "lte" }
24
+ * }
25
+ *
26
+ * Modes:
27
+ * exact | Package and consumer values must be equal.
28
+ * any | Field is ignored.
29
+ * lte | Package value ≤ consumer value (consumer is at least as
30
+ * | new/high — e.g. cppstd=20 package usable by cppstd=23).
31
+ * gte | Package value ≥ consumer value (symmetry; rarely useful).
32
+ * exact-or-newer | Semantically identical to `lte` at MVP — kept as a
33
+ * | distinct name because it reads naturally for compiler
34
+ * | version fields and leaves room for future "same major"
35
+ * | semantics without renaming.
36
+ *
37
+ * Missing fields default to `exact`. `os` and `arch` are ALWAYS evaluated
38
+ * as `exact` regardless of what the block says — cross-arch/cross-OS
39
+ * binaries are never ABI-compatible.
40
+ */
41
+
42
+ const OS_ARCH_ALWAYS_EXACT = new Set(['os', 'arch']);
43
+
44
+ const DEFAULT_COMPAT_FIELDS = [
45
+ 'os',
46
+ 'arch',
47
+ 'compiler',
48
+ 'compiler.version',
49
+ 'compiler.cppstd',
50
+ 'compiler.runtime',
51
+ ];
52
+
53
+ const VALID_MODES = new Set(['exact', 'any', 'lte', 'gte', 'exact-or-newer']);
54
+
55
+ /**
56
+ * Normalise a raw compatibility block into `{ field → mode }`.
57
+ * Missing fields fall back to `exact`. Unknown modes fall back to `exact`.
58
+ * `os` and `arch` are forced to `exact`.
59
+ *
60
+ * @param {object|null|undefined} rawCompat
61
+ * @returns {Record<string, string>}
62
+ */
63
+ function normalizeCompat(rawCompat) {
64
+ const result = {};
65
+ for (const field of DEFAULT_COMPAT_FIELDS) result[field] = 'exact';
66
+
67
+ if (!rawCompat || typeof rawCompat !== 'object') return result;
68
+
69
+ for (const [field, spec] of Object.entries(rawCompat)) {
70
+ if (OS_ARCH_ALWAYS_EXACT.has(field)) continue;
71
+ const mode = typeof spec === 'string' ? spec : (spec && spec.mode);
72
+ if (mode && VALID_MODES.has(mode)) result[field] = mode;
73
+ }
74
+ return result;
75
+ }
76
+
77
+ /**
78
+ * Return the default compatibility block — all fields `exact`.
79
+ * Used by `publish` when the source manifest lacks a block.
80
+ * @returns {Record<string, string>}
81
+ */
82
+ function defaultCompatBlock() {
83
+ const result = {};
84
+ for (const field of DEFAULT_COMPAT_FIELDS) result[field] = 'exact';
85
+ return result;
86
+ }
87
+
88
+ /**
89
+ * Evaluate a single field under a given mode.
90
+ *
91
+ * @returns {'match'|'compat-lte'|'compat-gte'|'compat-newer'|'compat-any'|'incompatible'}
92
+ */
93
+ function evaluateField(mode, consumerVal, packageVal) {
94
+ switch (mode) {
95
+ case 'any':
96
+ return 'compat-any';
97
+
98
+ case 'exact':
99
+ return consumerVal === packageVal ? 'match' : 'incompatible';
100
+
101
+ case 'lte': {
102
+ if (consumerVal === packageVal) return 'match';
103
+ const c = Number(consumerVal);
104
+ const p = Number(packageVal);
105
+ if (Number.isFinite(c) && Number.isFinite(p)) {
106
+ return p <= c ? 'compat-lte' : 'incompatible';
107
+ }
108
+ return 'incompatible';
109
+ }
110
+
111
+ case 'gte': {
112
+ if (consumerVal === packageVal) return 'match';
113
+ const c = Number(consumerVal);
114
+ const p = Number(packageVal);
115
+ if (Number.isFinite(c) && Number.isFinite(p)) {
116
+ return p >= c ? 'compat-gte' : 'incompatible';
117
+ }
118
+ return 'incompatible';
119
+ }
120
+
121
+ case 'exact-or-newer': {
122
+ if (consumerVal === packageVal) return 'match';
123
+ const c = Number(consumerVal);
124
+ const p = Number(packageVal);
125
+ if (Number.isFinite(c) && Number.isFinite(p)) {
126
+ return c >= p ? 'compat-newer' : 'incompatible';
127
+ }
128
+ return 'incompatible';
129
+ }
130
+
131
+ default:
132
+ // Unknown mode → fail safe
133
+ return 'incompatible';
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Evaluate a package's compatibility block against a consumer's profile.
139
+ *
140
+ * If either `consumerProfile` or `packageProfile` carries an `options`
141
+ * sub-object, options participate in compatibility: every option present
142
+ * on either side is evaluated, with mode `exact` by default. The compat
143
+ * block may override a specific option's mode via `options.<name>` (for
144
+ * example `"options.shared": "any"` to say "this build has no ABI
145
+ * dependence on `shared`"). See PLAN-OPTIONS §3.5.
146
+ *
147
+ * @param {Record<string, any>} consumerProfile may include `options: {...}`
148
+ * @param {Record<string, any>} packageProfile may include `options: {...}`
149
+ * @param {object|null|undefined} rawCompat compatibility block from the package manifest
150
+ * @returns {{
151
+ * compatible: boolean,
152
+ * reasons: Array<{ field: string, mode: string, consumer: any, package: any, outcome: string }>,
153
+ * }}
154
+ */
155
+ function evaluateCompat(consumerProfile, packageProfile, rawCompat) {
156
+ const compat = normalizeCompat(rawCompat);
157
+ const reasons = [];
158
+ let compatible = true;
159
+
160
+ for (const [field, mode] of Object.entries(compat)) {
161
+ // `options.<name>` keys in the compat block are handled below,
162
+ // together with any options present on either profile.
163
+ if (field.startsWith('options.')) continue;
164
+
165
+ const cVal = consumerProfile[field];
166
+ const pVal = packageProfile[field];
167
+ const outcome = evaluateField(mode, cVal, pVal);
168
+ reasons.push({ field, mode, consumer: cVal, package: pVal, outcome });
169
+ if (outcome === 'incompatible') compatible = false;
170
+ }
171
+
172
+ // Evaluate every option the consumer or package declares. Default mode
173
+ // is `exact`; the compat block may relax to `any` (and the other modes
174
+ // work when the values are numeric strings). Forward-compat: an option
175
+ // present on one side only is treated as `incompatible` under exact —
176
+ // the recipe shapes differ.
177
+ const consumerOpts = consumerProfile.options ?? {};
178
+ const packageOpts = packageProfile.options ?? {};
179
+ const optionKeys = new Set([...Object.keys(consumerOpts), ...Object.keys(packageOpts)]);
180
+ for (const name of optionKeys) {
181
+ const field = `options.${name}`;
182
+ const mode = compat[field] ?? 'exact';
183
+ const cVal = consumerOpts[name];
184
+ const pVal = packageOpts[name];
185
+ const outcome = evaluateField(mode, cVal, pVal);
186
+ reasons.push({ field, mode, consumer: cVal, package: pVal, outcome });
187
+ if (outcome === 'incompatible') compatible = false;
188
+ }
189
+
190
+ return { compatible, reasons };
191
+ }
192
+
193
+ /**
194
+ * Deterministic tiebreaker when multiple compatible candidates exist.
195
+ *
196
+ * 1. Prefer candidates whose `compiler` matches the consumer's.
197
+ * 2. Prefer the highest numeric `compiler.version`.
198
+ * 3. Prefer candidates whose `compiler.runtime` matches the consumer's.
199
+ * 4. Prefer the lexicographically earliest profile hash (stable fallback).
200
+ *
201
+ * @param {Array<{ profileHash: string, packageProfile: Record<string, any>, reasons: Array<any> }>} candidates
202
+ * @param {Record<string, string|null>} consumerProfile
203
+ * @returns {object|null} The preferred candidate (or null if list is empty).
204
+ */
205
+ function pickBestCandidate(candidates, consumerProfile) {
206
+ if (!candidates || candidates.length === 0) return null;
207
+ if (candidates.length === 1) return candidates[0];
208
+
209
+ const sorted = candidates.slice().sort((a, b) => {
210
+ const compilerA = a.packageProfile.compiler === consumerProfile.compiler ? 0 : 1;
211
+ const compilerB = b.packageProfile.compiler === consumerProfile.compiler ? 0 : 1;
212
+ if (compilerA !== compilerB) return compilerA - compilerB;
213
+
214
+ const verA = Number(a.packageProfile['compiler.version']) || 0;
215
+ const verB = Number(b.packageProfile['compiler.version']) || 0;
216
+ if (verA !== verB) return verB - verA; // descending
217
+
218
+ const runtimeA = a.packageProfile['compiler.runtime'] === consumerProfile['compiler.runtime'] ? 0 : 1;
219
+ const runtimeB = b.packageProfile['compiler.runtime'] === consumerProfile['compiler.runtime'] ? 0 : 1;
220
+ if (runtimeA !== runtimeB) return runtimeA - runtimeB;
221
+
222
+ return a.profileHash < b.profileHash ? -1 : 1;
223
+ });
224
+
225
+ return sorted[0];
226
+ }
227
+
228
+ /**
229
+ * Render a reasons array into a compact human-readable summary — used for
230
+ * the "chose candidate X because …" log line at install time.
231
+ *
232
+ * @param {Array<{ field, mode, consumer, package, outcome }>} reasons
233
+ * @returns {string}
234
+ */
235
+ function summarizeReasons(reasons) {
236
+ return reasons
237
+ .filter((r) => r.outcome !== 'compat-any' || r.mode !== 'any') // skip trivial any
238
+ .map((r) => {
239
+ switch (r.outcome) {
240
+ case 'match': return `${r.field}=${r.consumer}`;
241
+ case 'compat-lte': return `${r.field} ${r.package}→${r.consumer} (lte)`;
242
+ case 'compat-gte': return `${r.field} ${r.package}←${r.consumer} (gte)`;
243
+ case 'compat-newer': return `${r.field} ${r.package}→${r.consumer} (newer)`;
244
+ case 'compat-any': return `${r.field}=* (any)`;
245
+ case 'incompatible': return `${r.field} INCOMPAT (need ${r.consumer}, have ${r.package})`;
246
+ default: return `${r.field} ${r.outcome}`;
247
+ }
248
+ })
249
+ .join(', ');
250
+ }
251
+
252
+ module.exports = {
253
+ normalizeCompat,
254
+ defaultCompatBlock,
255
+ evaluateCompat,
256
+ evaluateField,
257
+ pickBestCandidate,
258
+ summarizeReasons,
259
+ DEFAULT_COMPAT_FIELDS,
260
+ VALID_MODES,
261
+ };
package/src/config.js CHANGED
@@ -4,6 +4,8 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
 
7
+ const log = require('./logger');
8
+
7
9
  /**
8
10
  * Returns the directory where wyvrnpm stores its config.
9
11
  * Windows : %LOCALAPPDATA%\wyvrnpm
@@ -41,7 +43,7 @@ function readConfig() {
41
43
  defaultProfile: raw.defaultProfile ?? 'default',
42
44
  };
43
45
  } catch {
44
- console.warn('[wyvrn] Warning: could not parse config.json — using defaults');
46
+ log.warn('could not parse config.json — using defaults');
45
47
  return { installSources: [], publishSources: [], linkedPackages: {}, defaultProfile: 'default' };
46
48
  }
47
49
  }