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
@@ -8,44 +8,39 @@ const AdmZip = require('adm-zip');
8
8
  const { readManifest } = require('../manifest');
9
9
  const { getProvider } = require('../providers');
10
10
  const { readConfig } = require('../config');
11
- const { loadProfile, hashProfile, sha256Of, getGitSha, getGitRepo } = require('../profile');
11
+ const { loadProfile, hashProfile, sha256Of, getGitSha, getGitRepo, isShaOnRemote } = require('../profile');
12
+ const { defaultCompatBlock } = require('../compat');
13
+ const {
14
+ normalizeOptionsDeclaration,
15
+ resolveEffectiveOptions,
16
+ parseCliOptions,
17
+ } = require('../options');
18
+ const { loadIgnorePatterns, isIgnored } = require('../ignore');
19
+ const { hasBuildRecipe, normalizeRecipe } = require('../build/recipe');
20
+ const log = require('../logger');
12
21
 
13
- // ---------------------------------------------------------------------------
14
- // .wyvrnignore helpers (unchanged from v1)
15
- // ---------------------------------------------------------------------------
16
-
17
- function loadIgnorePatterns(ignoreFile) {
18
- if (!fs.existsSync(ignoreFile)) return [];
19
- return fs
20
- .readFileSync(ignoreFile, 'utf8')
21
- .split(/\r?\n/)
22
- .map((l) => l.trim())
23
- .filter((l) => l.length > 0 && !l.startsWith('#'));
24
- }
25
-
26
- function patternToRegex(pattern) {
27
- const anchored = pattern.startsWith('/');
28
- const p = anchored ? pattern.slice(1) : pattern;
29
- let re = p
30
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
31
- .replace(/\*\*/g, '\x00')
32
- .replace(/\*/g, '[^/]*')
33
- .replace(/\?/g, '[^/]')
34
- .replace(/\x00/g, '.*');
35
- return anchored ? new RegExp(`^${re}(/|$)`) : new RegExp(`(^|/)${re}(/|$)`);
36
- }
37
-
38
- function addFolderFiltered(zip, dir, base, ignoreRe) {
22
+ /**
23
+ * Walk `dir` recursively and add every file to `zip`, skipping anything that
24
+ * matches `.wyvrnignore` patterns OR whose zip-relative path is already in
25
+ * `reservedRelPaths`. The reserved set lets the install-tree overlay win
26
+ * on path collisions (e.g. `include/foo.h` shipped by both source and
27
+ * install trees).
28
+ */
29
+ function addFolderFiltered(zip, dir, base, patterns, reservedRelPaths = new Set()) {
39
30
  for (const entry of fs.readdirSync(dir)) {
40
31
  const fullPath = path.join(dir, entry);
41
32
  const relPath = path.relative(base, fullPath).replace(/\\/g, '/');
42
- if (ignoreRe.some((re) => re.test(relPath))) {
43
- console.log(`[wyvrn] Ignoring: ${relPath}`);
33
+ const stat = fs.statSync(fullPath);
34
+ const isDir = stat.isDirectory();
35
+ if (isIgnored(relPath, isDir, patterns)) {
36
+ log.info(` Ignoring: ${relPath}${isDir ? '/' : ''}`);
44
37
  continue;
45
38
  }
46
- const stat = fs.statSync(fullPath);
47
- if (stat.isDirectory()) {
48
- addFolderFiltered(zip, fullPath, base, ignoreRe);
39
+ if (isDir) {
40
+ addFolderFiltered(zip, fullPath, base, patterns, reservedRelPaths);
41
+ } else if (reservedRelPaths.has(relPath)) {
42
+ // Install tree has a canonical version of this file; let that win.
43
+ log.info(` Install-tree wins: ${relPath}`);
49
44
  } else {
50
45
  const zipDir = path.dirname(relPath).replace(/\\/g, '/');
51
46
  zip.addLocalFile(fullPath, zipDir === '.' ? '' : zipDir);
@@ -53,6 +48,42 @@ function addFolderFiltered(zip, dir, base, ignoreRe) {
53
48
  }
54
49
  }
55
50
 
51
+ /**
52
+ * Collect every file under `dir` as a `{ fullPath, relPath }` pair. `relPath`
53
+ * is forward-slash-separated and relative to `dir` — so the install tree's
54
+ * `install/cmake/FooConfig.cmake` lands as `cmake/FooConfig.cmake` at the
55
+ * zip root, which is where `find_package(Foo CONFIG)` expects it on the
56
+ * consumer side after extraction into `wyvrn_internal/<name>/`.
57
+ */
58
+ function collectInstallTreeFiles(dir) {
59
+ const files = [];
60
+ function walk(current) {
61
+ for (const entry of fs.readdirSync(current)) {
62
+ const full = path.join(current, entry);
63
+ const stat = fs.statSync(full);
64
+ if (stat.isDirectory()) {
65
+ walk(full);
66
+ } else {
67
+ const rel = path.relative(dir, full).replace(/\\/g, '/');
68
+ files.push({ fullPath: full, relPath: rel });
69
+ }
70
+ }
71
+ }
72
+ walk(dir);
73
+ return files;
74
+ }
75
+
76
+ function addInstallTree(zip, installDir) {
77
+ const files = collectInstallTreeFiles(installDir);
78
+ const added = new Set();
79
+ for (const { fullPath, relPath } of files) {
80
+ const zipDir = path.dirname(relPath).replace(/\\/g, '/');
81
+ zip.addLocalFile(fullPath, zipDir === '.' ? '' : zipDir);
82
+ added.add(relPath);
83
+ }
84
+ return added;
85
+ }
86
+
56
87
  // ---------------------------------------------------------------------------
57
88
  // Source resolution
58
89
  // ---------------------------------------------------------------------------
@@ -64,21 +95,21 @@ function resolveSource(argv) {
64
95
  if (source) {
65
96
  const named = config.publishSources.find((s) => s.name === source);
66
97
  if (named) {
67
- console.log(`[wyvrn] Using publish source "${named.name}" from config`);
98
+ log.info(`Using publish source "${named.name}" from config`);
68
99
  source = named.url;
69
100
  awsProfile = awsProfile ?? named.profile;
70
101
  token = token ?? named.token;
71
102
  }
72
103
  } else {
73
104
  if (config.publishSources.length === 0) {
74
- console.error(
75
- '[wyvrn] Error: --source is required (or configure one with: ' +
105
+ log.error(
106
+ '--source is required (or configure one with: ' +
76
107
  'wyvrnpm configure add-source --kind publish ...)',
77
108
  );
78
109
  process.exit(1);
79
110
  }
80
111
  const first = config.publishSources[0];
81
- console.log(`[wyvrn] Using publish source "${first.name}" from config`);
112
+ log.info(`Using publish source "${first.name}" from config`);
82
113
  source = first.url;
83
114
  awsProfile = awsProfile ?? first.profile;
84
115
  token = token ?? first.token;
@@ -99,24 +130,26 @@ async function publish(argv) {
99
130
  force,
100
131
  manifest: manifestArg,
101
132
  } = argv;
133
+ const jsonOut = argv.format === 'json';
134
+ if (jsonOut) log.setJsonMode(true);
102
135
 
103
136
  // ── Load manifest ─────────────────────────────────────────────────────────
104
137
  const manifestPath = path.resolve(manifestArg || './wyvrn.json');
105
138
  const manifest = await readManifest(manifestPath);
106
139
  if (!manifest) {
107
- console.error(`[wyvrn] Error: Could not read manifest at ${manifestPath}`);
140
+ log.error(`Could not read manifest at ${manifestPath}`);
108
141
  process.exit(1);
109
142
  }
110
143
 
111
144
  const { name, version } = manifest;
112
145
  if (!name || !version) {
113
- console.error('[wyvrn] Error: Manifest must have "name" and "version" fields');
146
+ log.error('Manifest must have "name" and "version" fields');
114
147
  process.exit(1);
115
148
  }
116
149
 
117
150
  const srcDir = path.resolve(publishPath || '.');
118
151
  if (!fs.existsSync(srcDir)) {
119
- console.error(`[wyvrn] Error: Publish path does not exist: ${srcDir}`);
152
+ log.error(`Publish path does not exist: ${srcDir}`);
120
153
  process.exit(1);
121
154
  }
122
155
 
@@ -125,7 +158,7 @@ async function publish(argv) {
125
158
  try {
126
159
  provider = getProvider(source);
127
160
  } catch (err) {
128
- console.error(`[wyvrn] Error: ${err.message}`);
161
+ log.error(err.message);
129
162
  process.exit(1);
130
163
  }
131
164
 
@@ -133,27 +166,44 @@ async function publish(argv) {
133
166
  const config = readConfig();
134
167
  const activeProfileName = profileName ?? config.defaultProfile ?? 'default';
135
168
  const { profile: buildProfile, fromFile } = loadProfile(activeProfileName);
136
- const profileHash = hashProfile(buildProfile);
137
169
 
138
- console.log(
139
- `[wyvrn] Publishing ${name}@${version}\n` +
140
- `[wyvrn] Profile : "${activeProfileName}"${fromFile ? '' : ' (auto-detected save with `wyvrnpm configure profile detect`)'}\n` +
141
- `[wyvrn] ${buildProfile.os}/${buildProfile.arch} ` +
170
+ // ── Resolve this package's own declared options ─────────────────────────
171
+ // Publishing needs to commit to a specific value for every option the
172
+ // recipe declares. Sources for overrides: CLI `-o <pkg>:...` flags
173
+ // (scoped to the package being published), else the recipe's defaults.
174
+ const ownDeclaration = manifest.options
175
+ ? normalizeOptionsDeclaration(manifest.options, `${name}@${version}`)
176
+ : null;
177
+ const cliOptionsByPkg = parseCliOptions(argv.option);
178
+ const effectiveOptions = resolveEffectiveOptions(
179
+ ownDeclaration,
180
+ {}, // consumer-side overrides don't apply to self-publish
181
+ cliOptionsByPkg[name] ?? {},
182
+ `${name}@${version}`,
183
+ );
184
+
185
+ const profileHash = hashProfile(buildProfile, effectiveOptions);
186
+
187
+ log.info(
188
+ `Publishing ${name}@${version}\n` +
189
+ `Profile : "${activeProfileName}"${fromFile ? '' : ' (auto-detected — save with `wyvrnpm configure profile detect`)'}\n` +
190
+ ` ${buildProfile.os}/${buildProfile.arch} ` +
142
191
  `${buildProfile.compiler} ${buildProfile['compiler.version']} ` +
143
192
  `C++${buildProfile['compiler.cppstd']}` +
144
193
  (buildProfile['compiler.runtime'] ? ` [${buildProfile['compiler.runtime']}]` : '') + '\n' +
145
- `[wyvrn] Profile hash: ${profileHash}\n` +
146
- `[wyvrn] Provider : ${provider.constructor.providerName}`,
194
+ (effectiveOptions ? `Options : ${JSON.stringify(effectiveOptions)}\n` : '') +
195
+ `Profile hash: ${profileHash}\n` +
196
+ `Provider : ${provider.constructor.providerName}`,
147
197
  );
148
198
 
149
199
  // ── Check v2 existence ────────────────────────────────────────────────────
150
200
  const v2AlreadyExists = await provider.v2Exists({ source, name, version, profileHash, awsProfile, token });
151
201
  if (v2AlreadyExists) {
152
202
  if (force) {
153
- console.warn(`[wyvrn] Warning: ${name}@${version} [${profileHash}] already exists — overwriting (--force)`);
203
+ log.warn(`${name}@${version} [${profileHash}] already exists — overwriting (--force)`);
154
204
  } else {
155
- console.error(
156
- `[wyvrn] Error: ${name}@${version} [${profileHash}] already exists.\n` +
205
+ log.error(
206
+ `${name}@${version} [${profileHash}] already exists.\n` +
157
207
  ' Bump the version, use a different profile, or pass --force to overwrite.',
158
208
  );
159
209
  process.exit(1);
@@ -170,50 +220,129 @@ async function publish(argv) {
170
220
  const ver = typeof entry === 'string' ? entry : entry.version;
171
221
  if (ver) lockedDependencies[pkgName] = ver;
172
222
  }
173
- console.log(`[wyvrn] Lock file : ${Object.keys(lockedDependencies).length} pinned package(s)`);
223
+ log.info(`Lock file : ${Object.keys(lockedDependencies).length} pinned package(s)`);
174
224
  } catch {
175
- console.warn('[wyvrn] Warning: could not read wyvrn.lock — dependency versions will be published as-is from wyvrn.json');
225
+ log.warn('could not read wyvrn.lock — dependency versions will be published as-is from wyvrn.json');
176
226
  }
177
227
  } else {
178
- console.warn('[wyvrn] Warning: no wyvrn.lock found — run `wyvrnpm install` before publishing to pin dependency versions');
228
+ log.warn('no wyvrn.lock found — run `wyvrnpm install` before publishing to pin dependency versions');
179
229
  }
180
230
 
181
231
  // ── Git metadata ──────────────────────────────────────────────────────────
182
232
  const gitSha = getGitSha(srcDir);
183
233
  const gitRepo = getGitRepo(srcDir);
184
- if (gitSha) console.log(`[wyvrn] Git commit : ${gitSha}`);
185
- if (gitRepo) console.log(`[wyvrn] Git remote : ${gitRepo}`);
234
+ if (gitSha) log.info(`Git commit : ${gitSha}`);
235
+ if (gitRepo) log.info(`Git remote : ${gitRepo}`);
236
+
237
+ // Warn (but don't block) if HEAD isn't reachable from origin. Downstream
238
+ // `--build=missing` consumers will fail to fetch this commit — the fix
239
+ // is almost always `git push` before publishing.
240
+ if (gitSha && gitRepo && !isShaOnRemote(gitSha, srcDir)) {
241
+ log.warn(
242
+ `commit ${gitSha.slice(0, 12)} is not reachable from any remote ` +
243
+ `branch or tag on origin.\n` +
244
+ ` Consumers building this package from source will fail to retrieve it.\n` +
245
+ ` Fix: push your branch (or tag the commit) before publishing.`,
246
+ );
247
+ }
186
248
 
187
249
  // ── .wyvrnignore ──────────────────────────────────────────────────────────
188
250
  const ignoreFile = path.join(srcDir, '.wyvrnignore');
189
251
  const ignorePatterns = loadIgnorePatterns(ignoreFile);
190
- const ignoreRe = ignorePatterns.map(patternToRegex);
191
252
  if (ignorePatterns.length > 0) {
192
- console.log(`[wyvrn] Loaded ${ignorePatterns.length} pattern(s) from .wyvrnignore`);
253
+ log.info(`Loaded ${ignorePatterns.length} pattern(s) from .wyvrnignore`);
193
254
  }
194
255
 
195
256
  // ── Build zip ─────────────────────────────────────────────────────────────
196
257
  const tmpZipPath = path.join(os.tmpdir(), `wyvrnpm-${name}-${version}-${Date.now()}.zip`);
258
+
259
+ // ── Inject default `compatibility` block if the source manifest lacks one ──
260
+ // Written to a temp file so the source wyvrn.json is never modified.
261
+ // A published manifest must always carry a compat block so install-time
262
+ // resolution has something to evaluate (default = all fields `exact`,
263
+ // identical semantics to pre-phase-2 strict behaviour).
264
+ const tmpManifestPath = path.join(
265
+ os.tmpdir(),
266
+ `wyvrnpm-manifest-${name}-${version}-${Date.now()}.json`,
267
+ );
268
+ const manifestForPublish = {
269
+ ...manifest,
270
+ compatibility: manifest.compatibility ?? defaultCompatBlock(),
271
+ };
272
+ fs.writeFileSync(tmpManifestPath, JSON.stringify(manifestForPublish, null, 2), 'utf8');
273
+ if (!manifest.compatibility) {
274
+ log.info('Injecting default `compatibility` block (all fields `exact`) — override by adding one to wyvrn.json');
275
+ } else {
276
+ log.info(`Using package-declared compatibility: ${JSON.stringify(manifest.compatibility)}`);
277
+ }
278
+
279
+ // ── Install-tree overlay ──────────────────────────────────────────────────
280
+ // Publish zips the source tree (filtered by .wyvrnignore), but consumers
281
+ // actually need the install tree — `lib/cmake/<Name>/<Name>Config.cmake`,
282
+ // the installed headers, compiled libs — so find_package() works after
283
+ // extraction. When a build recipe is declared, locate the per-profile
284
+ // install dir (populated by `wyvrnpm build --config <cfg> --install`) and
285
+ // overlay its contents on top of the source zip. Install-tree files win
286
+ // on path collisions (e.g. `include/`).
287
+ let installOverlayDir = null;
288
+ let installOverlayPaths = new Set();
289
+ if (hasBuildRecipe(manifest)) {
290
+ try {
291
+ const recipe = normalizeRecipe(manifest.build, effectiveOptions);
292
+ const candidate = path.join(
293
+ srcDir, 'build', `wyvrn-${activeProfileName}`, recipe.installDir,
294
+ );
295
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
296
+ installOverlayDir = candidate;
297
+ } else {
298
+ log.warn(
299
+ `build recipe is declared but no install tree found at ${candidate}.\n` +
300
+ ' Consumers\' find_package() will likely fail. Fix: run\n' +
301
+ ' wyvrnpm build --config Debug --install\n' +
302
+ ' wyvrnpm build --config Release --install\n' +
303
+ ' wyvrnpm build --config RelWithDebInfo --install\n' +
304
+ ' wyvrnpm build --config MinSizeRel --install\n' +
305
+ ' then re-run publish.',
306
+ );
307
+ }
308
+ } catch (err) {
309
+ log.warn(`could not locate install tree: ${err.message} — publishing source tree only`);
310
+ }
311
+ }
312
+
197
313
  try {
198
- console.log(`[wyvrn] Zipping ${srcDir} ...`);
314
+ log.info(`Zipping ${srcDir} ...`);
199
315
  const zip = new AdmZip();
200
- addFolderFiltered(zip, srcDir, srcDir, ignoreRe);
316
+ if (installOverlayDir) {
317
+ // Collect install-tree paths first so the source-tree walker can skip
318
+ // anything the install tree will add canonically.
319
+ const peek = collectInstallTreeFiles(installOverlayDir);
320
+ installOverlayPaths = new Set(peek.map((f) => f.relPath));
321
+ }
322
+ addFolderFiltered(zip, srcDir, srcDir, ignorePatterns, installOverlayPaths);
323
+ if (installOverlayDir) {
324
+ log.info(`Overlaying install tree: ${installOverlayDir}`);
325
+ const added = addInstallTree(zip, installOverlayDir);
326
+ log.info(` Added ${added.size} file(s) from install tree`);
327
+ }
201
328
  zip.writeZip(tmpZipPath);
202
329
 
203
330
  const contentSha256 = sha256Of(tmpZipPath);
204
- console.log(`[wyvrn] Content SHA256 : ${contentSha256}`);
331
+ log.info(`Content SHA256 : ${contentSha256}`);
205
332
 
206
333
  // ── v2 publish ────────────────────────────────────────────────────────
207
- console.log('[wyvrn] Publishing (v2) ...');
334
+ log.info('Publishing (v2) ...');
208
335
  await provider.v2Publish(
209
- { manifest: manifestPath, zip: tmpZipPath },
336
+ { manifest: tmpManifestPath, zip: tmpZipPath },
210
337
  {
211
338
  source,
212
339
  name,
213
340
  version,
214
341
  profileHash,
215
342
  // buildSettings stored in metadata for debugging — NOT wyvrn.json
216
- buildSettings: buildProfile,
343
+ buildSettings: effectiveOptions
344
+ ? { ...buildProfile, options: effectiveOptions }
345
+ : buildProfile,
217
346
  contentSha256,
218
347
  gitSha,
219
348
  gitRepo,
@@ -223,10 +352,32 @@ async function publish(argv) {
223
352
  },
224
353
  );
225
354
 
226
- console.log(`[wyvrn] Successfully published ${name}@${version} [${profileHash}] to ${source}`);
355
+ log.success(`Successfully published ${name}@${version} [${profileHash}] to ${source}`);
356
+
357
+ if (jsonOut) {
358
+ const payload = {
359
+ command: 'publish',
360
+ wyvrnpmVersion: require('../../package.json').version,
361
+ name,
362
+ version,
363
+ profileHash,
364
+ source,
365
+ contentSha256,
366
+ options: effectiveOptions ?? null,
367
+ gitSha: gitSha ?? null,
368
+ gitRepo: gitRepo ?? null,
369
+ publishedAt: new Date().toISOString(),
370
+ };
371
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
372
+ }
227
373
  } finally {
228
- if (fs.existsSync(tmpZipPath)) fs.unlinkSync(tmpZipPath);
374
+ if (fs.existsSync(tmpZipPath)) fs.unlinkSync(tmpZipPath);
375
+ if (fs.existsSync(tmpManifestPath)) fs.unlinkSync(tmpManifestPath);
229
376
  }
230
377
  }
231
378
 
232
379
  module.exports = publish;
380
+ // Exported for tests only
381
+ module.exports.addFolderFiltered = addFolderFiltered;
382
+ module.exports.addInstallTree = addInstallTree;
383
+ module.exports.collectInstallTreeFiles = collectInstallTreeFiles;
@@ -0,0 +1,237 @@
1
+ 'use strict';
2
+
3
+ const { readConfig } = require('../config');
4
+ const { getProvider } = require('../providers');
5
+ const log = require('../logger');
6
+
7
+ /**
8
+ * `wyvrnpm show <pkg>` — surface registry metadata for a package.
9
+ *
10
+ * Accepts:
11
+ * name → list every published version + profile.
12
+ * name@version → list every profile for that version,
13
+ * fetch full metadata for each, highlight
14
+ * consumer (`uploadedBy`) vs author builds.
15
+ * name@version@profileHash → dump full metadata for one build.
16
+ *
17
+ * Sources: CLI `--source` overrides config.installSources. First source
18
+ * that returns data wins (same first-match policy as `install`).
19
+ *
20
+ * Added for PLAN-UPLOAD-BUILT phase 3 so maintainers can spot-check which
21
+ * profileHashes were uploaded by consumers vs published by an author. The
22
+ * `uploadedBy` block is the signal; its absence means "author publish"
23
+ * by convention.
24
+ *
25
+ * @param {object} argv
26
+ * @param {string} argv.pkg e.g. "zlib", "zlib@1.3.0.0", "zlib@1.3.0.0@a1b2..."
27
+ * @param {string[]} [argv.source] source URLs
28
+ * @param {string} [argv.awsProfile]
29
+ * @param {string} [argv.token]
30
+ * @returns {Promise<void>}
31
+ */
32
+ async function show(argv) {
33
+ const { name, version, profileHash } = parseSpec(argv.pkg);
34
+ if (!name) {
35
+ log.error('Usage: wyvrnpm show <name>[@<version>[@<profileHash>]]');
36
+ process.exit(1);
37
+ }
38
+
39
+ const jsonOut = argv.format === 'json';
40
+ if (jsonOut) log.setJsonMode(true);
41
+
42
+ const { sources, awsProfile, token } = resolveSources(argv);
43
+ if (sources.length === 0) {
44
+ log.error(
45
+ 'no sources configured. ' +
46
+ 'Pass --source <url> or run: wyvrnpm configure add-source --kind install ...',
47
+ );
48
+ process.exit(1);
49
+ }
50
+
51
+ for (const source of sources) {
52
+ let provider;
53
+ try { provider = getProvider(source); } catch { continue; }
54
+
55
+ const idx = await provider.v2GetVersionsIndex({ source, name, awsProfile, token });
56
+ if (!idx?.versions) continue;
57
+
58
+ const versionKeys = version
59
+ ? (idx.versions[version] ? [version] : [])
60
+ : Object.keys(idx.versions).sort();
61
+
62
+ if (version && versionKeys.length === 0) {
63
+ log.error(`Version ${version} not found on ${source}`);
64
+ process.exit(1);
65
+ }
66
+
67
+ // Collect a single structured result as we walk. Text rendering happens
68
+ // inline in the legacy path; JSON mode emits the whole object at the
69
+ // end of the first source that answered.
70
+ const result = {
71
+ command: 'show',
72
+ wyvrnpmVersion: require('../../package.json').version,
73
+ source,
74
+ package: name,
75
+ latest: idx.latest ?? null,
76
+ versions: {},
77
+ };
78
+
79
+ if (!jsonOut) {
80
+ console.log(`${name} (source: ${source})`);
81
+ if (idx.latest) console.log(` latest: ${idx.latest}`);
82
+ }
83
+
84
+ for (const v of versionKeys) {
85
+ const ventry = idx.versions[v];
86
+ const profiles = ventry?.profiles ?? {};
87
+ const profileKeys = profileHash
88
+ ? (profiles[profileHash] ? [profileHash] : [])
89
+ : Object.keys(profiles).sort();
90
+
91
+ if (profileHash && profileKeys.length === 0) {
92
+ log.error(`ProfileHash ${profileHash} not found for ${name}@${v}`);
93
+ process.exit(1);
94
+ }
95
+
96
+ if (!jsonOut) {
97
+ console.log(`\n ${name}@${v} (${profileKeys.length} profile${profileKeys.length === 1 ? '' : 's'})`);
98
+ if (ventry.source?.gitSha) {
99
+ console.log(` git: ${ventry.source.gitRepo ?? '?'} @ ${ventry.source.gitSha}`);
100
+ }
101
+ }
102
+
103
+ // For bare `name` queries we'd fetch O(M×N) manifests (versions ×
104
+ // profiles) — too noisy. Only deep-fetch when the user has narrowed
105
+ // to a specific version.
106
+ const deepFetch = Boolean(version);
107
+
108
+ const profileArr = [];
109
+ for (const hash of profileKeys) {
110
+ const entry = profiles[hash];
111
+ if (!jsonOut) {
112
+ console.log(`\n [${hash}]`);
113
+ if (entry.buildSettings) {
114
+ console.log(` profile : ${formatBuildSettings(entry.buildSettings)}`);
115
+ if (entry.buildSettings.options && Object.keys(entry.buildSettings.options).length > 0) {
116
+ console.log(` options : ${formatOptions(entry.buildSettings.options)}`);
117
+ }
118
+ }
119
+ console.log(` sha256 : ${entry.contentSha256 ?? '?'}`);
120
+ if (entry.publishedAt) console.log(` published: ${entry.publishedAt}`);
121
+ }
122
+
123
+ let meta = null;
124
+ if (deepFetch) {
125
+ meta = await provider.v2GetMeta({
126
+ source, name, version: v, profileHash: hash, awsProfile, token,
127
+ });
128
+ if (!jsonOut) {
129
+ if (meta?.uploadedBy) {
130
+ console.log(` origin : consumer upload (${meta.uploadedBy.kind})`);
131
+ console.log(` uploadedAt : ${meta.uploadedBy.uploadedAt}`);
132
+ console.log(` wyvrnpmVersion : ${meta.uploadedBy.wyvrnpmVersion ?? '?'}`);
133
+ if (meta.uploadedBy.builtFromGit) {
134
+ console.log(` builtFromGit : ${meta.uploadedBy.builtFromGit}`);
135
+ }
136
+ if (meta.uploadedBy.builtFromSha) {
137
+ console.log(` builtFromSha : ${meta.uploadedBy.builtFromSha}`);
138
+ }
139
+ } else if (meta) {
140
+ console.log(` origin : author publish`);
141
+ }
142
+ if (meta?.compatibility) {
143
+ console.log(` compat : ${JSON.stringify(meta.compatibility)}`);
144
+ }
145
+ if (meta?.options && Object.keys(meta.options).length > 0) {
146
+ console.log(` declared: ${formatDeclaredOptions(meta.options)}`);
147
+ }
148
+ }
149
+ }
150
+
151
+ profileArr.push({
152
+ profileHash: hash,
153
+ contentSha256: entry.contentSha256 ?? null,
154
+ publishedAt: entry.publishedAt ?? null,
155
+ buildSettings: entry.buildSettings ?? null,
156
+ origin: meta?.uploadedBy ? 'consumer-source-build' : (meta ? 'author-publish' : null),
157
+ uploadedBy: meta?.uploadedBy ?? null,
158
+ compatibility: meta?.compatibility ?? null,
159
+ declaredOptions: meta?.options ?? null,
160
+ });
161
+ }
162
+
163
+ result.versions[v] = {
164
+ source: ventry.source ?? null,
165
+ profiles: profileArr,
166
+ };
167
+ }
168
+
169
+ if (jsonOut) {
170
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
171
+ }
172
+ return;
173
+ }
174
+
175
+ log.error(`Package ${name} not found in any configured source`);
176
+ process.exit(1);
177
+ }
178
+
179
+ /**
180
+ * Parse a package spec "name[@version[@profileHash]]".
181
+ * @param {string} spec
182
+ * @returns {{ name?: string, version?: string, profileHash?: string }}
183
+ */
184
+ function parseSpec(spec) {
185
+ if (!spec || typeof spec !== 'string') return {};
186
+ const parts = spec.split('@');
187
+ const [name, version, profileHash] = parts;
188
+ return {
189
+ name: name || undefined,
190
+ version: version || undefined,
191
+ profileHash: profileHash || undefined,
192
+ };
193
+ }
194
+
195
+ function resolveSources(argv) {
196
+ const config = readConfig();
197
+ const sources = (argv.source && argv.source.length > 0)
198
+ ? argv.source
199
+ : config.installSources.map((s) => s.url);
200
+
201
+ const firstConfig = config.installSources?.[0];
202
+ return {
203
+ sources,
204
+ awsProfile: argv.awsProfile ?? firstConfig?.profile,
205
+ token: argv.token ?? firstConfig?.token,
206
+ };
207
+ }
208
+
209
+ function formatBuildSettings(bs) {
210
+ const parts = [
211
+ `${bs.os}/${bs.arch}`,
212
+ `${bs.compiler} ${bs['compiler.version']}`,
213
+ `C++${bs['compiler.cppstd']}`,
214
+ ];
215
+ if (bs['compiler.runtime']) parts.push(`[${bs['compiler.runtime']}]`);
216
+ return parts.join(' ');
217
+ }
218
+
219
+ function formatOptions(options) {
220
+ return Object.entries(options)
221
+ .sort(([a], [b]) => a.localeCompare(b))
222
+ .map(([k, v]) => `${k}=${typeof v === 'string' ? v : String(v)}`)
223
+ .join(', ');
224
+ }
225
+
226
+ function formatDeclaredOptions(declared) {
227
+ return Object.entries(declared)
228
+ .sort(([a], [b]) => a.localeCompare(b))
229
+ .map(([name, spec]) => {
230
+ const allowed = Array.isArray(spec?.allowed) ? spec.allowed : [];
231
+ return `${name} (default=${String(spec?.default)}, allowed=${allowed.map(String).join('|')})`;
232
+ })
233
+ .join('; ');
234
+ }
235
+
236
+ module.exports = show;
237
+ module.exports.parseSpec = parseSpec;