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
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const BaseProvider = require('./base');
6
+ const log = require('../logger');
6
7
 
7
8
  /**
8
9
  * File system provider — handles local directories and SMB/UNC shares.
@@ -56,12 +57,12 @@ class FileProvider extends BaseProvider {
56
57
  }
57
58
 
58
59
  _copy(src, dest) {
59
- console.log(`[wyvrn] → ${dest}`);
60
+ log.info(` → ${dest}`);
60
61
  fs.copyFileSync(src, dest);
61
62
  }
62
63
 
63
64
  _writeJson(filePath, data) {
64
- console.log(`[wyvrn] → ${filePath}`);
65
+ log.info(` → ${filePath}`);
65
66
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
66
67
  }
67
68
 
@@ -87,7 +88,7 @@ class FileProvider extends BaseProvider {
87
88
  this._copy(files.zip, path.join(destDir, 'wyvrn.zip'));
88
89
 
89
90
  const latestPath = path.join(packageDir, 'latest.json');
90
- console.log(`[wyvrn] → ${latestPath}`);
91
+ log.info(` → ${latestPath}`);
91
92
  fs.writeFileSync(latestPath, JSON.stringify({ version }), 'utf8');
92
93
  }
93
94
 
@@ -106,6 +107,7 @@ class FileProvider extends BaseProvider {
106
107
  const {
107
108
  source, name, version, profileHash, buildSettings,
108
109
  contentSha256, gitSha, gitRepo, lockedDependencies,
110
+ updateLatest = true, uploadedBy,
109
111
  } = options;
110
112
 
111
113
  const v2root = this._v2Root(source, name);
@@ -129,6 +131,7 @@ class FileProvider extends BaseProvider {
129
131
  gitSha: gitSha ?? null,
130
132
  gitRepo: gitRepo ?? null,
131
133
  publishedAt: new Date().toISOString(),
134
+ ...(uploadedBy ? { uploadedBy } : {}),
132
135
  };
133
136
  this._writeJson(path.join(buildDir, 'wyvrn.json'), v2Meta);
134
137
 
@@ -154,11 +157,14 @@ class FileProvider extends BaseProvider {
154
157
  if (gitSha || gitRepo) {
155
158
  versionsIdx.versions[version].source = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
156
159
  }
157
- versionsIdx.latest = version;
160
+ if (updateLatest) versionsIdx.latest = version;
158
161
  this._writeJson(versionsPath, versionsIdx);
159
162
 
160
- // 5) v2 latest.json
161
- this._writeJson(path.join(v2root, 'latest.json'), { version, profileHash, contentSha256 });
163
+ // 5) v2 latest.json — skipped for consumer uploads (upload-built) so a
164
+ // historical-version rebuild cannot clobber the real "latest" pointer.
165
+ if (updateLatest) {
166
+ this._writeJson(path.join(v2root, 'latest.json'), { version, profileHash, contentSha256 });
167
+ }
162
168
  }
163
169
 
164
170
  async v2GetMeta({ source, name, version, profileHash }) {
@@ -6,6 +6,7 @@ const https = require('https');
6
6
  const { pipeline } = require('stream/promises');
7
7
  const { Readable } = require('stream');
8
8
  const BaseProvider = require('./base');
9
+ const log = require('../logger');
9
10
 
10
11
  function pinDependencies(rawDeps, lockedDeps) {
11
12
  if (!rawDeps || !lockedDeps || Object.keys(lockedDeps).length === 0) return rawDeps;
@@ -45,7 +46,7 @@ class HttpProvider extends BaseProvider {
45
46
  const parsed = new URL(url);
46
47
  const headers = { 'Content-Type': contentType, 'Content-Length': body.length };
47
48
  if (token) headers['Authorization'] = `Bearer ${token}`;
48
- console.log(`[wyvrn] → PUT ${url}`);
49
+ log.info(` → PUT ${url}`);
49
50
  const req = this._lib(url).request(
50
51
  { hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'PUT', headers },
51
52
  (res) => {
@@ -66,7 +67,7 @@ class HttpProvider extends BaseProvider {
66
67
  const parsed = new URL(url);
67
68
  const headers = { 'Content-Type': contentType, 'Content-Length': buffer.length };
68
69
  if (token) headers['Authorization'] = `Bearer ${token}`;
69
- console.log(`[wyvrn] → PUT ${url}`);
70
+ log.info(` → PUT ${url}`);
70
71
  const req = this._lib(url).request(
71
72
  { hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'PUT', headers },
72
73
  (res) => {
@@ -175,6 +176,7 @@ class HttpProvider extends BaseProvider {
175
176
  const {
176
177
  source, name, version, profileHash, buildSettings,
177
178
  contentSha256, gitSha, gitRepo, lockedDependencies, token,
179
+ updateLatest = true, uploadedBy,
178
180
  } = options;
179
181
  const base = source.replace(/\/$/, '');
180
182
  const v2root = `${base}/v2/${name}`;
@@ -198,6 +200,7 @@ class HttpProvider extends BaseProvider {
198
200
  gitSha: gitSha ?? null,
199
201
  gitRepo: gitRepo ?? null,
200
202
  publishedAt: new Date().toISOString(),
203
+ ...(uploadedBy ? { uploadedBy } : {}),
201
204
  };
202
205
  await this._putBuffer(
203
206
  `${buildRoot}/wyvrn.json`,
@@ -231,7 +234,7 @@ class HttpProvider extends BaseProvider {
231
234
  if (gitSha || gitRepo) {
232
235
  versionsIdx.versions[version].source = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
233
236
  }
234
- versionsIdx.latest = version;
237
+ if (updateLatest) versionsIdx.latest = version;
235
238
  await this._putBuffer(
236
239
  `${v2root}/versions.json`,
237
240
  Buffer.from(JSON.stringify(versionsIdx, null, 2)),
@@ -239,13 +242,15 @@ class HttpProvider extends BaseProvider {
239
242
  token,
240
243
  );
241
244
 
242
- // 5) Update v2 latest.json
243
- await this._putBuffer(
244
- `${v2root}/latest.json`,
245
- Buffer.from(JSON.stringify({ version, profileHash, contentSha256 })),
246
- 'application/json',
247
- token,
248
- );
245
+ // 5) v2 latest.json — skipped for consumer uploads (see FileProvider note).
246
+ if (updateLatest) {
247
+ await this._putBuffer(
248
+ `${v2root}/latest.json`,
249
+ Buffer.from(JSON.stringify({ version, profileHash, contentSha256 })),
250
+ 'application/json',
251
+ token,
252
+ );
253
+ }
249
254
  }
250
255
 
251
256
  async v2GetMeta({ source, name, version, profileHash, token }) {
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const BaseProvider = require('./base');
6
+ const log = require('../logger');
6
7
 
7
8
  const RE_S3_URI = /^s3:\/\//;
8
9
  const RE_S3_VIRTUAL_HOSTED = /^https?:\/\/([^.]+)\.s3[.-][^/]*\.amazonaws\.com/;
@@ -79,7 +80,7 @@ class S3Provider extends BaseProvider {
79
80
  }
80
81
 
81
82
  async _putFile(client, bucket, key, filePath, contentType, PutObjectCommand) {
82
- console.log(`[wyvrn] → s3://${bucket}/${key}`);
83
+ log.info(` → s3://${bucket}/${key}`);
83
84
  await client.send(new PutObjectCommand({
84
85
  Bucket: bucket, Key: key,
85
86
  Body: fs.readFileSync(filePath),
@@ -88,7 +89,7 @@ class S3Provider extends BaseProvider {
88
89
  }
89
90
 
90
91
  async _putBuffer(client, bucket, key, content, contentType, PutObjectCommand) {
91
- console.log(`[wyvrn] → s3://${bucket}/${key}`);
92
+ log.info(` → s3://${bucket}/${key}`);
92
93
  await client.send(new PutObjectCommand({
93
94
  Bucket: bucket, Key: key,
94
95
  Body: content,
@@ -177,6 +178,7 @@ class S3Provider extends BaseProvider {
177
178
  const {
178
179
  source, name, version, profileHash, buildSettings,
179
180
  contentSha256, gitSha, gitRepo, lockedDependencies, awsProfile,
181
+ updateLatest = true, uploadedBy,
180
182
  } = options;
181
183
  const { PutObjectCommand, GetObjectCommand } = this._loadSdk();
182
184
  const client = this._makeClient(awsProfile);
@@ -203,6 +205,7 @@ class S3Provider extends BaseProvider {
203
205
  gitSha: gitSha ?? null,
204
206
  gitRepo: gitRepo ?? null,
205
207
  publishedAt: new Date().toISOString(),
208
+ ...(uploadedBy ? { uploadedBy } : {}),
206
209
  };
207
210
  await this._putBuffer(
208
211
  client, bucket, `${buildRoot}/wyvrn.json`,
@@ -231,18 +234,20 @@ class S3Provider extends BaseProvider {
231
234
  if (gitSha || gitRepo) {
232
235
  versionsIdx.versions[version].source = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
233
236
  }
234
- versionsIdx.latest = version;
237
+ if (updateLatest) versionsIdx.latest = version;
235
238
  await this._putBuffer(
236
239
  client, bucket, `${v2root}/versions.json`,
237
240
  Buffer.from(JSON.stringify(versionsIdx, null, 2)), 'application/json', PutObjectCommand,
238
241
  );
239
242
 
240
- // 5) latest.json
241
- await this._putBuffer(
242
- client, bucket, `${v2root}/latest.json`,
243
- Buffer.from(JSON.stringify({ version, profileHash, contentSha256 })),
244
- 'application/json', PutObjectCommand,
245
- );
243
+ // 5) latest.json — skipped for consumer uploads (see FileProvider note).
244
+ if (updateLatest) {
245
+ await this._putBuffer(
246
+ client, bucket, `${v2root}/latest.json`,
247
+ Buffer.from(JSON.stringify({ version, profileHash, contentSha256 })),
248
+ 'application/json', PutObjectCommand,
249
+ );
250
+ }
246
251
  }
247
252
 
248
253
  async v2GetMeta({ source, name, version, profileHash, awsProfile }) {
package/src/resolve.js CHANGED
@@ -3,6 +3,15 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
+ const log = require('./logger');
7
+ const {
8
+ parseRange,
9
+ matchesRange,
10
+ pickHighestInRange,
11
+ intersectRanges,
12
+ rangeToString,
13
+ } = require('./version-range');
14
+
6
15
  /**
7
16
  * Checks if a version string represents a linked package.
8
17
  * @param {string} version
@@ -63,6 +72,33 @@ function compareVersions(a, b) {
63
72
  return 0;
64
73
  }
65
74
 
75
+ /**
76
+ * Fetch every published version of `name` across the configured sources
77
+ * via each source's `versions.json` (v2 layout). Returns the union of
78
+ * version keys sorted ascending. Used by the range resolver to pick the
79
+ * highest version satisfying `^/~/>=` style specs.
80
+ *
81
+ * @param {string} name
82
+ * @param {string[]} packageSources
83
+ * @param {Function} httpClient
84
+ * @returns {Promise<string[]>}
85
+ */
86
+ async function fetchPublishedVersions(name, packageSources, httpClient) {
87
+ const seen = new Set();
88
+ for (const source of packageSources) {
89
+ const base = source.replace(/\/$/, '');
90
+ try {
91
+ const resp = await httpClient(`${base}/v2/${name}/versions.json`);
92
+ if (!resp.ok) continue;
93
+ const idx = await resp.json();
94
+ for (const v of Object.keys(idx?.versions ?? {})) seen.add(v);
95
+ } catch {
96
+ // Try next source
97
+ }
98
+ }
99
+ return [...seen].sort(compareVersions);
100
+ }
101
+
66
102
  /**
67
103
  * Resolves the "latest" tag for a package by fetching latest.json from the registry.
68
104
  *
@@ -164,7 +200,7 @@ async function fetchPackageManifest(name, version, packageSources, platform, htt
164
200
  }
165
201
  }
166
202
 
167
- console.warn(`[wyvrn] Warning: could not fetch manifest for ${name}@${version} from any source`);
203
+ log.warn(`could not fetch manifest for ${name}@${version} from any source`);
168
204
  cache.set(cacheKey, null);
169
205
  return null;
170
206
  }
@@ -173,13 +209,21 @@ async function fetchPackageManifest(name, version, packageSources, platform, htt
173
209
  * Resolves the full, flattened dependency graph starting from rootDeps.
174
210
  * When the same package is required at multiple versions the highest version wins.
175
211
  *
212
+ * Side-effect parameter: when `manifestsOut` is provided (a Map instance),
213
+ * the resolver populates it with `{name → recipe manifest}` for every
214
+ * fetched dependency. Callers that need recipe-declared `options` (see
215
+ * PLAN-OPTIONS) use this to avoid a second round of HTTP round-trips.
216
+ * Passing `null` / omitting it keeps the legacy behaviour.
217
+ *
176
218
  * @param {Record<string, string>} rootDeps - Direct dependencies { name: version }.
177
219
  * @param {string[]} packageSources - Ordered list of base URLs to try.
178
220
  * @param {string} platform - Target platform string (e.g. "win_x64").
179
221
  * @param {Function} httpClient - fetch-compatible function.
222
+ * @param {Map<string, string>} [lockedVersions]
223
+ * @param {Map<string, object>} [manifestsOut] - populated in-place when provided
180
224
  * @returns {Promise<Map<string, string>>} Resolved map of name -> version.
181
225
  */
182
- async function resolveDependencies(rootDeps, packageSources, platform, httpClient, lockedVersions = new Map()) {
226
+ async function resolveDependencies(rootDeps, packageSources, platform, httpClient, lockedVersions = new Map(), manifestsOut = null) {
183
227
  /** @type {Map<string, string>} Final resolved versions. */
184
228
  const resolved = new Map();
185
229
 
@@ -187,13 +231,45 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
187
231
  const manifestCache = new Map();
188
232
 
189
233
  /**
190
- * Pending work queue: each entry is { name, version }.
234
+ * Per-package effective range. For a package that only ever appears with
235
+ * exact versions, this stores `{ kind: 'exact', version }` — which still
236
+ * intersects cleanly with later range requests. For packages with range
237
+ * specs, we intersect contributors so conflicting ranges fail fast.
238
+ * @type {Map<string, ReturnType<typeof parseRange>>}
239
+ */
240
+ const effectiveRange = new Map();
241
+
242
+ /**
243
+ * Who contributed which raw spec for which package — used only for the
244
+ * conflict error message so the user knows whose declaration to fix.
245
+ * @type {Map<string, Array<{ by: string, spec: string }>>}
246
+ */
247
+ const rangeContributors = new Map();
248
+
249
+ /**
250
+ * Cache the published-versions list per package — one HTTP round trip
251
+ * per name regardless of how often it appears in the graph.
252
+ * @type {Map<string, string[]>}
253
+ */
254
+ const publishedCache = new Map();
255
+ const fetchPublished = async (name) => {
256
+ if (publishedCache.has(name)) return publishedCache.get(name);
257
+ const list = await fetchPublishedVersions(name, packageSources, httpClient);
258
+ publishedCache.set(name, list);
259
+ return list;
260
+ };
261
+
262
+ /**
263
+ * Pending work queue: each entry is { name, version, by? }.
264
+ * `by` is the contributor label for conflict diagnostics.
191
265
  * We process iteratively to avoid deep recursion on large dependency trees.
192
266
  */
193
- const queue = Object.entries(rootDeps).map(([name, version]) => ({ name, version }));
267
+ const queue = Object.entries(rootDeps).map(
268
+ ([name, version]) => ({ name, version, by: 'wyvrn.json' }),
269
+ );
194
270
 
195
271
  while (queue.length > 0) {
196
- let { name, version } = queue.shift();
272
+ let { name, version, by } = queue.shift();
197
273
 
198
274
  // Lock file always wins — use pinned version regardless of what was requested.
199
275
  if (lockedVersions.has(name)) {
@@ -205,36 +281,115 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
205
281
  if (version === 'latest') {
206
282
  try {
207
283
  version = await resolveLatestVersion(name, packageSources, platform, httpClient);
208
- console.log(`[wyvrn] Resolved "${name}@latest" → ${version}`);
284
+ log.info(`Resolved "${name}@latest" → ${version}`);
209
285
  } catch (err) {
210
- console.warn(`[wyvrn] Warning: ${err.message}`);
286
+ log.warn(err.message);
211
287
  continue;
212
288
  }
213
289
  }
214
290
 
291
+ // Parse the version spec. Ranges (`^`, `~`, explicit comparators) resolve
292
+ // to an exact version via `fetchPublishedVersions` + highest-in-range;
293
+ // intersected against any previously-seen range for the same package.
294
+ let parsedSpec;
295
+ try {
296
+ parsedSpec = parseRange(version);
297
+ } catch (err) {
298
+ log.error(`Dependency "${name}": ${err.message}`);
299
+ continue;
300
+ }
301
+
302
+ if (parsedSpec.kind === 'linked') {
303
+ // Linked packages bypass range resolution entirely.
304
+ parsedSpec = { kind: 'exact', version };
305
+ }
306
+
307
+ // Intersect with any previously-seen range for this package.
308
+ //
309
+ // Backward-compat nuance: when both the previous and current spec are
310
+ // exact but differ, we preserve the legacy "pick higher with warning"
311
+ // behaviour rather than erroring. Intersection-strict conflict only
312
+ // applies when at least one side is an actual range — ranges are
313
+ // opt-in new territory where a declared range that cannot be satisfied
314
+ // IS a bug, not a soft conflict.
315
+ let effective = parsedSpec;
316
+ const prev = effectiveRange.get(name);
317
+ if (prev) {
318
+ const bothExact = prev.kind === 'exact' && parsedSpec.kind === 'exact';
319
+ if (bothExact) {
320
+ // Legacy behaviour: keep the HIGHER of the two exact versions.
321
+ // The warning is emitted in the "already resolved" branch below.
322
+ effective = compareVersions(parsedSpec.version, prev.version) > 0
323
+ ? parsedSpec
324
+ : prev;
325
+ } else {
326
+ const combined = intersectRanges(prev, parsedSpec);
327
+ if (!combined) {
328
+ const sources = rangeContributors.get(name) ?? [];
329
+ const lines = sources.map((c) => ` required by ${c.by}: ${c.spec}`);
330
+ lines.push(` required by ${by}: ${version}`);
331
+ log.error(
332
+ `cannot satisfy conflicting version ranges for "${name}":\n` +
333
+ lines.join('\n') + '\n' +
334
+ ` no version satisfies all ranges`,
335
+ );
336
+ continue;
337
+ }
338
+ effective = combined;
339
+ }
340
+ }
341
+ effectiveRange.set(name, effective);
342
+ if (!rangeContributors.has(name)) rangeContributors.set(name, []);
343
+ rangeContributors.get(name).push({ by, spec: version });
344
+
345
+ // Resolve the (possibly intersected) spec down to an exact version.
346
+ let exactVersion;
347
+ if (effective.kind === 'exact') {
348
+ exactVersion = effective.version;
349
+ } else {
350
+ const published = await fetchPublished(name);
351
+ const picked = pickHighestInRange(published, effective);
352
+ if (!picked) {
353
+ log.error(
354
+ `no published version of "${name}" satisfies ${rangeToString(effective)}\n` +
355
+ (published.length > 0
356
+ ? ` published versions: ${published.join(', ')}`
357
+ : ` no versions published on the configured sources`),
358
+ );
359
+ continue;
360
+ }
361
+ exactVersion = picked;
362
+ if (!prev) log.info(`Resolved "${name}@${version}" → ${exactVersion}`);
363
+ }
364
+
215
365
  if (resolved.has(name)) {
216
366
  const existing = resolved.get(name);
217
- if (existing === version) {
367
+ if (existing === exactVersion) {
218
368
  continue;
219
369
  }
220
370
  // Locked packages never lose to a conflict.
221
371
  if (lockedVersions.has(name)) {
222
372
  continue;
223
373
  }
224
- // Conflict: keep the latest version.
225
- const winner = compareVersions(version, existing) > 0 ? version : existing;
226
- if (winner !== existing) {
227
- console.warn(
228
- `[wyvrn] Version conflict for "${name}": ${existing} vs ${version} — using ${winner}`,
374
+ // After range-intersection resolution, we prefer the
375
+ // intersected-range's highest over a naive version bump the
376
+ // intersection already enforces "both contributors agree", so
377
+ // trust it.
378
+ if (exactVersion !== existing) {
379
+ log.warn(
380
+ `Version conflict for "${name}": ${existing} vs ${exactVersion} — using ${exactVersion}`,
229
381
  );
230
- resolved.set(name, winner);
382
+ resolved.set(name, exactVersion);
231
383
  // Re-fetch the winning version's manifest to pull in its transitive deps.
232
- queue.push({ name, version: winner });
384
+ queue.push({ name, version: exactVersion, by });
233
385
  }
234
386
  continue;
235
387
  }
236
388
 
237
- resolved.set(name, version);
389
+ resolved.set(name, exactVersion);
390
+ // Shadow `version` with the resolved exact for the rest of this block
391
+ // — linked handling, manifest fetch, etc. all expect an exact string.
392
+ version = exactVersion;
238
393
 
239
394
  // Handle linked packages: read local manifest instead of fetching from remote
240
395
  let manifest;
@@ -242,9 +397,9 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
242
397
  const localPath = getLinkedPath(version);
243
398
  manifest = readLocalManifest(localPath);
244
399
  if (manifest) {
245
- console.log(`[wyvrn] Linked: ${name} → ${localPath}`);
400
+ log.info(`Linked: ${name} → ${localPath}`);
246
401
  } else {
247
- console.log(`[wyvrn] Linked: ${name} → ${localPath} (no manifest found)`);
402
+ log.info(`Linked: ${name} → ${localPath} (no manifest found)`);
248
403
  }
249
404
  } else {
250
405
  manifest = await fetchPackageManifest(
@@ -261,6 +416,10 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
261
416
  continue;
262
417
  }
263
418
 
419
+ if (manifestsOut instanceof Map) {
420
+ manifestsOut.set(name, manifest);
421
+ }
422
+
264
423
  // Support both formats:
265
424
  // PascalCase array : { Dependencies: [ { Name, Version }, ... ] }
266
425
  // string object : { dependencies: { name: "version", ... } }
@@ -279,7 +438,7 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
279
438
  }));
280
439
  }
281
440
  for (const dep of deps) {
282
- if (dep.version) queue.push({ name: dep.name, version: dep.version });
441
+ if (dep.version) queue.push({ name: dep.name, version: dep.version, by: `${name}@${version}` });
283
442
  }
284
443
  }
285
444
 
@@ -290,6 +449,7 @@ module.exports = {
290
449
  compareVersions,
291
450
  resolveLatestVersion,
292
451
  resolveDependencies,
452
+ fetchPublishedVersions,
293
453
  isLinkedVersion,
294
454
  getLinkedPath,
295
455
  };
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Generate the contents of `wyvrn_deps.cmake`.
5
+ *
6
+ * This file is loaded via `CMAKE_PROJECT_INCLUDE` — it runs after the
7
+ * consumer's first `project()` call, which is when CMake variables like
8
+ * `CMAKE_CXX_COMPILER_ID`, `CMAKE_SIZEOF_VOID_P`, `CMAKE_SYSTEM_NAME`, and
9
+ * `WIN32` / `APPLE` / `UNIX` become valid. The bundled utility scripts
10
+ * (`variables.cmake` etc.) rely on those, so they cannot be included from
11
+ * the toolchain body.
12
+ *
13
+ * @param {object} args
14
+ * @param {string[]} args.packageNames Resolved dep names in topological order.
15
+ * @param {string} args.profileHash 16-char profile hash (for logging).
16
+ * @returns {string} CMake file content.
17
+ */
18
+ function generateDepsCmake({ packageNames, profileHash }) {
19
+ const findPackageCalls = packageNames
20
+ .map((name) => ` find_package(${name} CONFIG QUIET)`)
21
+ .join('\n');
22
+
23
+ const resolvedList = packageNames.map((n) => ` ${n}`).join('\n');
24
+
25
+ return `# Generated by wyvrnpm — do not edit.
26
+ # Loaded after project() via CMAKE_PROJECT_INCLUDE.
27
+ # Profile hash: ${profileHash}
28
+
29
+ if(WYVRN_DEPS_INCLUDED)
30
+ return()
31
+ endif()
32
+ set(WYVRN_DEPS_INCLUDED TRUE)
33
+
34
+ # List of every package resolved by \`wyvrnpm install\`, in topological order.
35
+ set(WYVRN_RESOLVED_PACKAGES
36
+ ${resolvedList || ' # (none)'}
37
+ )
38
+
39
+ # ── Bundled wyvrnpm CMake utilities ───────────────────────────────────────────
40
+ # Included in the order the utility scripts expect.
41
+ include("\${WYVRNPM_CMAKE_DIR}/variables.cmake")
42
+ include("\${WYVRNPM_CMAKE_DIR}/options.cmake")
43
+ include("\${WYVRNPM_CMAKE_DIR}/cpp.cmake")
44
+ include("\${WYVRNPM_CMAKE_DIR}/functions.cmake")
45
+ include("\${WYVRNPM_CMAKE_DIR}/macros.cmake")
46
+
47
+ # ── Sanity checks (warn only — do not override the consumer's environment) ───
48
+ if(DEFINED CMAKE_CXX_COMPILER_ID AND DEFINED WYVRN_PROFILE_COMPILER)
49
+ set(_wyvrn_expected_compiler_id "")
50
+ if(WYVRN_PROFILE_COMPILER STREQUAL "msvc")
51
+ set(_wyvrn_expected_compiler_id "MSVC")
52
+ elseif(WYVRN_PROFILE_COMPILER STREQUAL "gcc")
53
+ set(_wyvrn_expected_compiler_id "GNU")
54
+ elseif(WYVRN_PROFILE_COMPILER STREQUAL "clang")
55
+ set(_wyvrn_expected_compiler_id "Clang")
56
+ elseif(WYVRN_PROFILE_COMPILER STREQUAL "apple-clang")
57
+ set(_wyvrn_expected_compiler_id "AppleClang")
58
+ endif()
59
+ if(_wyvrn_expected_compiler_id AND NOT CMAKE_CXX_COMPILER_ID STREQUAL _wyvrn_expected_compiler_id)
60
+ message(WARNING
61
+ "wyvrnpm profile expects compiler '\${WYVRN_PROFILE_COMPILER}' "
62
+ "(CMake id '\${_wyvrn_expected_compiler_id}') but the active compiler "
63
+ "is '\${CMAKE_CXX_COMPILER_ID}'. Installed binaries may be ABI-incompatible."
64
+ )
65
+ endif()
66
+ endif()
67
+
68
+ if(DEFINED CMAKE_SIZEOF_VOID_P AND DEFINED WYVRN_PROFILE_ARCH)
69
+ if(WYVRN_PROFILE_ARCH STREQUAL "x86_64" OR WYVRN_PROFILE_ARCH STREQUAL "armv8")
70
+ if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
71
+ message(WARNING
72
+ "wyvrnpm profile expects 64-bit arch '\${WYVRN_PROFILE_ARCH}' "
73
+ "but CMake is configuring a 32-bit build (CMAKE_SIZEOF_VOID_P=\${CMAKE_SIZEOF_VOID_P})."
74
+ )
75
+ endif()
76
+ elseif(WYVRN_PROFILE_ARCH STREQUAL "x86")
77
+ if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
78
+ message(WARNING
79
+ "wyvrnpm profile expects 32-bit arch 'x86' "
80
+ "but CMake is configuring a 64-bit build (CMAKE_SIZEOF_VOID_P=\${CMAKE_SIZEOF_VOID_P})."
81
+ )
82
+ endif()
83
+ endif()
84
+ endif()
85
+
86
+ # ── wyvrnpm_find_dependencies() ───────────────────────────────────────────────
87
+ # One-shot helper: resolves every package that wyvrnpm installed.
88
+ # Packages that don't ship a CMake config are silently skipped (CONFIG QUIET).
89
+ function(wyvrnpm_find_dependencies)
90
+ ${findPackageCalls || ' # (no dependencies)'}
91
+
92
+ # Post-find aliasing: ensure every resolved package has a wyvrn::<name>
93
+ # target, creating an INTERFACE alias when the package exports only
94
+ # <name>::<name>. Skipped when wyvrn::<name> already exists.
95
+ foreach(_wyvrn_pkg IN LISTS WYVRN_RESOLVED_PACKAGES)
96
+ if(TARGET wyvrn::\${_wyvrn_pkg})
97
+ # Already provided by the package's own CMake config.
98
+ elseif(TARGET \${_wyvrn_pkg}::\${_wyvrn_pkg})
99
+ add_library(wyvrn::\${_wyvrn_pkg} INTERFACE IMPORTED)
100
+ set_target_properties(wyvrn::\${_wyvrn_pkg} PROPERTIES
101
+ INTERFACE_LINK_LIBRARIES \${_wyvrn_pkg}::\${_wyvrn_pkg}
102
+ )
103
+ endif()
104
+ endforeach()
105
+ endfunction()
106
+
107
+ # ── wyvrnpm_enable_runtime_dll_copy(target) ───────────────────────────────────
108
+ # Adds a POST_BUILD command that copies every runtime DLL the target depends on
109
+ # (transitively, via \$<TARGET_RUNTIME_DLLS:...>) next to the target's own
110
+ # binary — so consumers can run the executable straight from the build dir
111
+ # without \`PATH\` tinkering. No-op on non-Windows platforms and on non-
112
+ # executable targets.
113
+ #
114
+ # Respects the per-target opt-out property WYVRN_COPY_RUNTIME_DLLS OFF.
115
+ # Requires CMake ≥ 3.21 (for TARGET_RUNTIME_DLLS).
116
+ function(wyvrnpm_enable_runtime_dll_copy target)
117
+ if(NOT WIN32)
118
+ return()
119
+ endif()
120
+ if(NOT TARGET \${target})
121
+ message(WARNING "wyvrnpm_enable_runtime_dll_copy: target '\${target}' does not exist")
122
+ return()
123
+ endif()
124
+ get_target_property(_wyvrn_type \${target} TYPE)
125
+ if(NOT _wyvrn_type STREQUAL "EXECUTABLE")
126
+ return()
127
+ endif()
128
+ get_target_property(_wyvrn_optout \${target} WYVRN_COPY_RUNTIME_DLLS)
129
+ if(_wyvrn_optout STREQUAL "OFF" OR _wyvrn_optout STREQUAL "FALSE")
130
+ return()
131
+ endif()
132
+ add_custom_command(TARGET \${target} POST_BUILD
133
+ COMMAND \${CMAKE_COMMAND} -E copy_if_different
134
+ "\$<TARGET_RUNTIME_DLLS:\${target}>"
135
+ "\$<TARGET_FILE_DIR:\${target}>"
136
+ COMMAND_EXPAND_LISTS
137
+ VERBATIM
138
+ )
139
+ endfunction()
140
+
141
+ # ── wyvrnpm_finalize_targets() ────────────────────────────────────────────────
142
+ # Discovers all executable targets in the calling directory (top-level only,
143
+ # not recursive) and wires each one up for runtime-DLL copy. Call this at the
144
+ # end of the consumer's top-level CMakeLists.txt, AFTER all add_executable()
145
+ # calls, so every target is visible.
146
+ #
147
+ # Scope: uses the BUILDSYSTEM_TARGETS directory property of the calling
148
+ # directory. For multi-subdirectory projects where executables are added from
149
+ # subdirectories, consumers should either:
150
+ # (a) call wyvrnpm_enable_runtime_dll_copy(<tgt>) explicitly for each exe, or
151
+ # (b) call wyvrnpm_finalize_targets() from each subdirectory.
152
+ function(wyvrnpm_finalize_targets)
153
+ get_property(_wyvrn_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
154
+ foreach(_wyvrn_t IN LISTS _wyvrn_targets)
155
+ get_target_property(_wyvrn_type \${_wyvrn_t} TYPE)
156
+ if(_wyvrn_type STREQUAL "EXECUTABLE")
157
+ wyvrnpm_enable_runtime_dll_copy(\${_wyvrn_t})
158
+ endif()
159
+ endforeach()
160
+ endfunction()
161
+ `;
162
+ }
163
+
164
+ module.exports = { generateDepsCmake };