wyvrnpm 2.4.1 → 2.8.1

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.
@@ -7,6 +7,8 @@ const { readManifest, normalizeDependencies } = require('../manife
7
7
  const { resolveDependencies } = require('../resolve');
8
8
  const { downloadDependencies } = require('../download');
9
9
  const { readConfig } = require('../config');
10
+ const { resolveSourceAuth } = require('../auth');
11
+ const { resolveEffectiveConf, cmakeCacheVariables, unflattenConf } = require('../conf');
10
12
  const { loadProfile, hashProfile, formatProfile, mergeProfile } = require('../profile');
11
13
  const { generateToolchain, generateCMakePresets } = require('../toolchain');
12
14
  const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
@@ -15,6 +17,10 @@ const {
15
17
  resolveEffectiveOptions,
16
18
  parseCliOptions,
17
19
  } = require('../options');
20
+ const {
21
+ normalizeSettingsOverrides,
22
+ resolveOverrideSettings,
23
+ } = require('../settings-overrides');
18
24
  const {
19
25
  resolveUploadDestination,
20
26
  createUploadStats,
@@ -119,6 +125,45 @@ async function install(argv) {
119
125
  Object.entries(normalizedDeps).map(([name, d]) => [name, d.version]),
120
126
  );
121
127
 
128
+ // Per-package source pinning (EVALUATION.md S4). Deps declaring
129
+ // `"source": "primary-s3"` restrict to that named source's URL from
130
+ // config — typosquat / mirror-shadow defense. Unknown name → fail
131
+ // fast (silent fallback to fan-out would defeat the point).
132
+ //
133
+ // `argv.source` overrides config entirely; if the user passes
134
+ // `--source <url>` but a dep pins a name, we can't resolve the name
135
+ // → warn and fall back to fan-out across the CLI-supplied URLs.
136
+ //
137
+ // Transitive deps continue to fan out — a pinned top-level dep's
138
+ // recipe can reference whatever it wants.
139
+ const sourcesByName = new Map();
140
+ if (!argv.source || argv.source.length === 0) {
141
+ for (const s of config.installSources ?? []) {
142
+ if (s.name && s.url) sourcesByName.set(s.name, s.url);
143
+ }
144
+ }
145
+ const pinnedSources = new Map();
146
+ for (const [name, d] of Object.entries(normalizedDeps)) {
147
+ if (!d.source) continue;
148
+ if (argv.source && argv.source.length > 0) {
149
+ log.warn(
150
+ `"${name}" pins source "${d.source}" but --source was passed on CLI — ` +
151
+ `pin ignored for this run`,
152
+ );
153
+ continue;
154
+ }
155
+ const url = sourcesByName.get(d.source);
156
+ if (!url) {
157
+ log.error(
158
+ `"${name}" pins source "${d.source}" but no install source by that name is configured. ` +
159
+ `Run "wyvrnpm configure list" to see configured sources.`,
160
+ );
161
+ process.exit(1);
162
+ }
163
+ pinnedSources.set(name, url);
164
+ log.info(`${name}: pinned to source "${d.source}"`);
165
+ }
166
+
122
167
  log.info(
123
168
  `Installing dependencies for "${manifest.name}" (platform: ${argv.platform})`,
124
169
  );
@@ -134,11 +179,31 @@ async function install(argv) {
134
179
  fetch,
135
180
  lockedVersions,
136
181
  depManifests,
182
+ // F13: prefer the consumer's own profileHash when picking which build
183
+ // entry to read wyvrn.json from. Today every published build for a
184
+ // given (name, version) ships the same manifest, so this is harmless;
185
+ // it future-proofs against per-profile manifest variance.
186
+ hashProfile(baseProfile),
187
+ pinnedSources,
137
188
  );
138
189
 
139
190
  // CLI `-o <pkg>:<name>=<value>` overrides, parsed into { pkg → {name: value} }.
140
191
  const cliOptionsByPkg = parseCliOptions(argv.option);
141
192
 
193
+ // F11: pattern-based settings overrides. `manifest.settingsOverrides`
194
+ // is { "<glob>": { <setting>: <value> }, ... }. Validated + sorted by
195
+ // specificity once here; `resolveOverrideSettings(depName, ...)` then
196
+ // returns the merged-from-patterns settings for each dep. Literal
197
+ // per-dep `settings` still override these (authors pinning a specific
198
+ // dep win over a team-wide pattern).
199
+ let settingsOverrides;
200
+ try {
201
+ settingsOverrides = normalizeSettingsOverrides(manifest.settingsOverrides);
202
+ } catch (err) {
203
+ log.error(err.message);
204
+ process.exit(1);
205
+ }
206
+
142
207
  // ── Build per-dependency enriched map ─────────────────────────────────────
143
208
  // Each dep gets:
144
209
  // - an effective profile (base + per-dep settings overrides)
@@ -146,7 +211,12 @@ async function install(argv) {
146
211
  // - profileHash computed from both (options only hashed in when declared)
147
212
  const enrichedDeps = new Map();
148
213
  for (const [name, version] of resolvedDeps) {
149
- const depOverrides = normalizedDeps[name]?.settings ?? {};
214
+ // F11: layer pattern overrides first (less-specific applied first
215
+ // inside resolveOverrideSettings), then the literal per-dep
216
+ // settings on top. Literal wins over any pattern.
217
+ const patternOverrides = resolveOverrideSettings(name, settingsOverrides);
218
+ const literalOverrides = normalizedDeps[name]?.settings ?? {};
219
+ const depOverrides = { ...patternOverrides, ...literalOverrides };
150
220
  const effectiveProfile = mergeProfile(baseProfile, depOverrides);
151
221
 
152
222
  const depManifest = depManifests.get(name);
@@ -177,16 +247,85 @@ async function install(argv) {
177
247
  profileHash,
178
248
  profile: effectiveProfile,
179
249
  options: effectiveOptions,
250
+ // S4: pinned install source (name + URL). Only set for direct
251
+ // deps that declared `"source": "..."`; transitive deps stay
252
+ // undefined so they fan out across configured sources.
253
+ pinnedSource: pinnedSources.has(name)
254
+ ? { name: normalizedDeps[name].source, url: pinnedSources.get(name) }
255
+ : undefined,
180
256
  });
181
257
  }
182
258
 
259
+ // ── Resolve effective conf (PLAN-CONF.md §4) ──────────────────────────────
260
+ // Four layers (phase 2): CLI > wyvrn.local.json > profile > recipe.
261
+ // Non-ABI; does not fold into profileHash. Feeds into CMakePresets.json
262
+ // cacheVariables below. `baseProfile` may carry its own `conf` block —
263
+ // an auto-detected profile (no on-disk file) does not.
264
+ let effectiveConfResult;
265
+ try {
266
+ effectiveConfResult = resolveEffectiveConf({
267
+ manifest,
268
+ rootDir,
269
+ cliConf: argv.conf, // yargs array: string[]
270
+ profile: baseProfile, // named profile may carry a `conf` block (phase 2)
271
+ });
272
+ } catch (err) {
273
+ log.error(`conf resolution failed: ${err.message}`);
274
+ process.exit(1);
275
+ }
276
+ const effectiveConf = effectiveConfResult.flat;
277
+ if (effectiveConfResult.localOverlayPath) {
278
+ log.info(`Local overlay: ${effectiveConfResult.localOverlayPath}`);
279
+ // One-line nudge when wyvrn.local.json exists but isn't gitignored —
280
+ // catching the existing-project case init.js's scaffold can't cover.
281
+ // Non-fatal; we never rewrite the user's .gitignore at install time.
282
+ const gitignorePath = path.join(rootDir, '.gitignore');
283
+ if (fs.existsSync(gitignorePath)) {
284
+ const gi = fs.readFileSync(gitignorePath, 'utf8');
285
+ const ignored = gi.split(/\r?\n/).some((l) => l.trim() === 'wyvrn.local.json');
286
+ if (!ignored) {
287
+ log.warn(
288
+ `wyvrn.local.json is present but not in ${gitignorePath} — ` +
289
+ `add a line "wyvrn.local.json" to keep dev-local overrides out of version control.`,
290
+ );
291
+ }
292
+ }
293
+ }
294
+ if (Object.keys(effectiveConf).length > 0) {
295
+ log.info(`Effective conf: ${JSON.stringify(effectiveConf)}`);
296
+ }
297
+
298
+ // ── --dry-run: print the resolved plan and exit (EVALUATION.md F6) ────────
299
+ // Runs the resolver + profile/options math end-to-end, then reports what
300
+ // WOULD be installed — no download, no wyvrn.lock, no wyvrn_internal/, no
301
+ // toolchain. Preserves --format=json for CI ingestion.
302
+ if (argv.dryRun) {
303
+ emitDryRunPlan({
304
+ enrichedDeps,
305
+ resolvedDeps,
306
+ baseProfile,
307
+ profileName,
308
+ buildMode,
309
+ packageSources,
310
+ effectiveConf,
311
+ jsonOut,
312
+ });
313
+ return;
314
+ }
315
+
183
316
  const razerDir = path.join(rootDir, 'wyvrn_internal');
184
317
 
185
- // Extract auth from the first install source (if any)
318
+ // Extract auth from the first install source (if any). Prefer
319
+ // `tokenEnv` over a literal `token` stored in config — closes
320
+ // EVALUATION.md S8 (no plaintext secret persisted, value is looked up
321
+ // fresh each run).
186
322
  const firstInstallSrc = config.installSources?.[0];
187
323
  const downloadOptions = {
188
324
  awsProfile: firstInstallSrc?.profile ?? undefined,
189
- token: firstInstallSrc?.token ?? undefined,
325
+ token: resolveSourceAuth(
326
+ { token: argv.token, tokenEnv: argv.tokenEnv },
327
+ firstInstallSrc,
328
+ ).token,
190
329
  buildMode,
191
330
  profileName,
192
331
  };
@@ -199,7 +338,10 @@ async function install(argv) {
199
338
  {
200
339
  uploadSource: argv.uploadSource,
201
340
  awsProfile: argv.awsProfile ?? firstInstallSrc?.profile,
202
- token: argv.token ?? firstInstallSrc?.token,
341
+ token: resolveSourceAuth(
342
+ { token: argv.token, tokenEnv: argv.tokenEnv },
343
+ firstInstallSrc,
344
+ ).token,
203
345
  },
204
346
  config,
205
347
  );
@@ -249,6 +391,12 @@ async function install(argv) {
249
391
  profile: baseProfile,
250
392
  profileHash: hashProfile(baseProfile),
251
393
  packages: sortedPackages,
394
+ // PLAN-CONF.md §4.8.1: informational only, not consulted during
395
+ // resolution. Omitted entirely when empty so pre-conf locks
396
+ // remain byte-identical to today's output.
397
+ ...(Object.keys(effectiveConf).length > 0
398
+ ? { effectiveConf: unflattenConf(effectiveConf) }
399
+ : {}),
252
400
  };
253
401
 
254
402
  fs.writeFileSync(lockPath, JSON.stringify(lockData, null, 2) + '\n', 'utf8');
@@ -278,6 +426,7 @@ async function install(argv) {
278
426
  let presetGenerator = null;
279
427
  let presetCacheVariables = null;
280
428
  let presetInstallDir = null;
429
+ let presetConfigs = null;
281
430
  if (hasBuildRecipe(manifest)) {
282
431
  try {
283
432
  // Resolve the project's own options to expand ${options.*} in its
@@ -291,7 +440,7 @@ async function install(argv) {
291
440
  const projectEffectiveOptions = resolveEffectiveOptions(
292
441
  projectDeclaration, {}, projectCliOverrides, manifest.name,
293
442
  );
294
- const recipe = normalizeRecipe(manifest.build, projectEffectiveOptions);
443
+ const recipe = normalizeRecipe(manifest.build, projectEffectiveOptions, manifest);
295
444
 
296
445
  if (recipe.generators.length > 0) {
297
446
  // First entry in the generator fallback chain. Users whose system
@@ -316,6 +465,14 @@ async function install(argv) {
316
465
  // (C:/Program Files on Windows, /usr/local on Unix). Same semantics as
317
466
  // the source-build path which passes -DCMAKE_INSTALL_PREFIX explicitly.
318
467
  presetInstallDir = recipe.installDir;
468
+
469
+ // Propagate `build.configs` so multi-config generators (Ninja
470
+ // Multi-Config, Visual Studio) generate build files for every
471
+ // config the recipe declares. Pre-2.8.1 the preset only carried
472
+ // Debug/Release, so a recipe listing MinSizeRel would fail with
473
+ // `ninja: error: loading 'build-MinSizeRel.ninja'`. CLAUDE.md §8
474
+ // documented that sharp edge; this closes it.
475
+ presetConfigs = recipe.configs;
319
476
  } catch (err) {
320
477
  log.warn(
321
478
  `could not derive CMake preset config from the project's build recipe: ${err.message}\n` +
@@ -325,6 +482,31 @@ async function install(argv) {
325
482
  }
326
483
  }
327
484
 
485
+ // PLAN-CONF.md §4.3 / §5 presets sink: fold effectiveConf's
486
+ // cmake.cache namespace into the preset's cacheVariables. Runs
487
+ // regardless of whether the project has its own build recipe —
488
+ // a consumer with no recipe but a wyvrn.local.json conf still gets
489
+ // their overrides into the generated preset.
490
+ //
491
+ // Conf wins on collisions with build.configure-derived vars. The
492
+ // recipe-normaliser blocks author-side collisions (§4.7); a
493
+ // collision here can only come from a consumer's CLI --conf or
494
+ // wyvrn.local.json overriding a value the author also sets via
495
+ // build.configure. Warn so the override is visible.
496
+ const confCacheVars = cmakeCacheVariables(effectiveConf);
497
+ if (Object.keys(confCacheVars).length > 0) {
498
+ presetCacheVariables = presetCacheVariables ?? {};
499
+ for (const [k, v] of Object.entries(confCacheVars)) {
500
+ if (k in presetCacheVariables && presetCacheVariables[k] !== v) {
501
+ log.warn(
502
+ `conf override: cmake.cache.${k} = ${JSON.stringify(v)} ` +
503
+ `supersedes build.configure's ${JSON.stringify(presetCacheVariables[k])}`,
504
+ );
505
+ }
506
+ presetCacheVariables[k] = v;
507
+ }
508
+ }
509
+
328
510
  const presetResult = generateCMakePresets({
329
511
  projectRoot: rootDir,
330
512
  profileName,
@@ -334,6 +516,7 @@ async function install(argv) {
334
516
  extraCacheVariables: presetCacheVariables,
335
517
  installDir: presetInstallDir,
336
518
  profileArch: baseProfile.arch ?? null,
519
+ configs: presetConfigs,
337
520
  });
338
521
  if (presetResult.action === 'skipped') {
339
522
  log.warn(
@@ -397,4 +580,86 @@ async function install(argv) {
397
580
  }
398
581
  }
399
582
 
583
+ // ---------------------------------------------------------------------------
584
+ // --dry-run plan emitter (EVALUATION.md F6)
585
+ // ---------------------------------------------------------------------------
586
+
587
+ /**
588
+ * Render the resolved install plan WITHOUT touching disk. Called from
589
+ * `install()` when `--dry-run` is passed; exits the install flow before
590
+ * download, lockfile write, or toolchain generation.
591
+ *
592
+ * Text mode: human-readable per-dep summary.
593
+ * JSON mode (`--format=json`): structured payload on stdout with
594
+ * `command: "install-dry-run"` so log consumers can disambiguate it
595
+ * from a real install.
596
+ */
597
+ function emitDryRunPlan({
598
+ enrichedDeps, resolvedDeps, baseProfile, profileName, buildMode,
599
+ packageSources, effectiveConf = {}, jsonOut,
600
+ }) {
601
+ const baseHash = hashProfile(baseProfile);
602
+ const packages = [...resolvedDeps.keys()].sort().map((name) => {
603
+ const entry = enrichedDeps.get(name) ?? {};
604
+ return {
605
+ name,
606
+ version: resolvedDeps.get(name),
607
+ profileHash: entry.profileHash ?? null,
608
+ options: entry.options ?? null,
609
+ // Whether the dep's effective profile differs from the consumer's
610
+ // base (i.e. per-dep settings override produced a distinct hash).
611
+ overridesBaseProfile: entry.profileHash && entry.profileHash !== baseHash,
612
+ };
613
+ });
614
+
615
+ if (jsonOut) {
616
+ const payload = {
617
+ command: 'install-dry-run',
618
+ wyvrnpmVersion: require('../../package.json').version,
619
+ profile: baseProfile,
620
+ profileName,
621
+ profileHash: baseHash,
622
+ buildMode,
623
+ sources: packageSources,
624
+ effectiveConf: Object.keys(effectiveConf).length > 0
625
+ ? unflattenConf(effectiveConf)
626
+ : {},
627
+ packages,
628
+ };
629
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
630
+ return;
631
+ }
632
+
633
+ log.info('── install plan (--dry-run) ─────────────────────────────────────');
634
+ log.info(`Profile : "${profileName}" [${baseHash}]`);
635
+ log.info(`Build : ${buildMode}`);
636
+ log.info(`Sources : ${packageSources.length > 0 ? packageSources.join(', ') : '(none)'}`);
637
+ const confKeys = Object.keys(effectiveConf).sort();
638
+ if (confKeys.length > 0) {
639
+ log.info(`Conf : ${confKeys.map((k) => `${k}=${effectiveConf[k]}`).join(', ')}`);
640
+ }
641
+ if (packages.length === 0) {
642
+ log.info('(no dependencies)');
643
+ return;
644
+ }
645
+
646
+ const nameW = Math.max(4, ...packages.map((p) => p.name.length));
647
+ const versionW = Math.max(7, ...packages.map((p) => p.version.length));
648
+ const header = ` ${'name'.padEnd(nameW)} ${'version'.padEnd(versionW)} profileHash options`;
649
+ log.info(header);
650
+ log.info(` ${'-'.repeat(nameW)} ${'-'.repeat(versionW)} ---------------- --------------------`);
651
+ for (const p of packages) {
652
+ const override = p.overridesBaseProfile ? ' *' : ' ';
653
+ const optStr = p.options ? JSON.stringify(p.options) : '-';
654
+ log.info(
655
+ ` ${p.name.padEnd(nameW)} ${p.version.padEnd(versionW)} ${p.profileHash ?? '(none) '}${override} ${optStr}`,
656
+ );
657
+ }
658
+ log.info('');
659
+ log.info('(nothing was downloaded; wyvrn.lock / wyvrn_internal / toolchain untouched)');
660
+ if (packages.some((p) => p.overridesBaseProfile)) {
661
+ log.info(' * = per-dep settings override produces a profileHash distinct from the base.');
662
+ }
663
+ }
664
+
400
665
  module.exports = install;
@@ -10,8 +10,10 @@ const {
10
10
  listProfiles,
11
11
  getProfilesDir,
12
12
  mergeProfile,
13
+ resolveProfileChain,
13
14
  } = require('../profile');
14
15
  const { readConfig, writeConfig } = require('../config');
16
+ const { parseCliConf, flattenConf, unflattenConf } = require('../conf');
15
17
  const log = require('../logger');
16
18
 
17
19
  // ---------------------------------------------------------------------------
@@ -39,11 +41,13 @@ function list() {
39
41
 
40
42
  for (const name of names) {
41
43
  const marker = name === defName ? ' *' : '';
42
- const p = readProfile(name);
44
+ let p;
45
+ try { p = resolveProfileChain(name); }
46
+ catch { p = null; } // cycle / missing parent — fall through to (unresolved)
43
47
  const summary = p
44
48
  ? `${p.os}/${p.arch} ${p.compiler} ${p['compiler.version']} C++${p['compiler.cppstd']}` +
45
49
  (p['compiler.runtime'] ? ` [${p['compiler.runtime']}]` : '')
46
- : '(unreadable)';
50
+ : '(unresolved — check `extends`)';
47
51
  console.log(` ${(name + marker).padEnd(24)} ${summary}`);
48
52
  }
49
53
  }
@@ -59,12 +63,26 @@ function list() {
59
63
  function show(argv) {
60
64
  const config = readConfig();
61
65
  const name = argv.name ?? config.defaultProfile ?? 'default';
62
- const stored = readProfile(name);
63
66
 
64
- if (stored) {
67
+ // Raw → displays `extends:` chain for visibility. Resolved → what
68
+ // loadProfile() actually hands to install / publish / build.
69
+ const raw = readProfile(name);
70
+ let resolved;
71
+ try {
72
+ resolved = raw ? resolveProfileChain(name) : null;
73
+ } catch (err) {
74
+ log.error(`Cannot resolve profile "${name}": ${err.message}`);
75
+ process.exit(1);
76
+ }
77
+
78
+ if (resolved) {
65
79
  log.info(`Profile "${name}":`);
66
- console.log(formatProfile(stored));
67
- console.log(`\nProfile hash : ${hashProfile(stored)}`);
80
+ if (raw.extends !== undefined && raw.extends !== null) {
81
+ const chain = Array.isArray(raw.extends) ? raw.extends.join(', ') : raw.extends;
82
+ log.info(` extends: ${chain}`);
83
+ }
84
+ console.log(formatProfile(resolved));
85
+ console.log(`\nProfile hash : ${hashProfile(resolved)}`);
68
86
  } else {
69
87
  const detected = detectProfile();
70
88
  log.info(`Profile "${name}" not found — showing auto-detected environment:`);
@@ -103,9 +121,16 @@ function detect(argv) {
103
121
  // ---------------------------------------------------------------------------
104
122
 
105
123
  /**
106
- * wyvrnpm configure profile set [--name <name>] [field options]
124
+ * wyvrnpm configure profile set [--name <name>] [field options] [--conf KEY=VAL]
107
125
  * Manually override individual fields in a named profile.
108
126
  * Starts from the existing saved profile, or auto-detects if none exists yet.
127
+ *
128
+ * --conf <namespace.leaf>=<value> (repeatable) — set a non-ABI build-time
129
+ * knob in the profile's `[conf]` block (PLAN-CONF.md phase 2). Same
130
+ * syntax as `install --conf` / `build --conf`; `KEY` alone means
131
+ * `KEY=ON`, `KEY=` removes any inherited value from this profile.
132
+ * Profile conf never folds into `profileHash`. Allow-listed
133
+ * namespaces only — unknown keys fail fast.
109
134
  */
110
135
  function set(argv) {
111
136
  const config = readConfig();
@@ -121,6 +146,44 @@ function set(argv) {
121
146
  'compiler.runtime': argv.runtime ?? null,
122
147
  });
123
148
 
149
+ // Apply --conf edits, if any. Writes at the leaf level, preserving
150
+ // sibling keys that weren't mentioned. `KEY=` removes the leaf.
151
+ const cliConf = Array.isArray(argv.conf) ? argv.conf : (argv.conf ? [argv.conf] : []);
152
+ if (cliConf.length > 0) {
153
+ let { sets, unsets } = { sets: {}, unsets: new Set() };
154
+ try { ({ sets, unsets } = parseCliConf(cliConf)); }
155
+ catch (err) {
156
+ log.error(`--conf: ${err.message}`);
157
+ process.exit(1);
158
+ }
159
+
160
+ const existingFlat = (() => {
161
+ try { return updated.conf ? flattenConf(updated.conf) : {}; }
162
+ catch (err) {
163
+ log.error(`existing profile conf is malformed: ${err.message}`);
164
+ process.exit(1);
165
+ }
166
+ })();
167
+
168
+ const nextFlat = { ...existingFlat, ...sets };
169
+ for (const k of unsets) delete nextFlat[k];
170
+
171
+ try {
172
+ // Re-validate as a whole so namespace/leaf checks catch both the
173
+ // edits AND any previously-saved pre-phase-2 garbage.
174
+ require('../conf/namespaces').validateFlatConfKeys(nextFlat);
175
+ } catch (err) {
176
+ log.error(`--conf: ${err.message}`);
177
+ process.exit(1);
178
+ }
179
+
180
+ if (Object.keys(nextFlat).length === 0) {
181
+ delete updated.conf;
182
+ } else {
183
+ updated.conf = unflattenConf(nextFlat);
184
+ }
185
+ }
186
+
124
187
  writeProfile(name, updated);
125
188
 
126
189
  log.info(`Profile "${name}" updated:`);
@@ -8,6 +8,13 @@ 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 { resolveSourceAuth } = require('../auth');
12
+ const {
13
+ readRecipeConf,
14
+ parseCliConf,
15
+ mergeConfLayers,
16
+ unflattenConf,
17
+ } = require('../conf');
11
18
  const { loadProfile, hashProfile, sha256Of, getGitSha, getGitRepo, isShaOnRemote } = require('../profile');
12
19
  const { defaultCompatBlock } = require('../compat');
13
20
  const {
@@ -89,8 +96,9 @@ function addInstallTree(zip, installDir) {
89
96
  // ---------------------------------------------------------------------------
90
97
 
91
98
  function resolveSource(argv) {
92
- let { source, awsProfile, token } = argv;
99
+ let { source, awsProfile } = argv;
93
100
  const config = readConfig();
101
+ let sourceEntry = null;
94
102
 
95
103
  if (source) {
96
104
  const named = config.publishSources.find((s) => s.name === source);
@@ -98,7 +106,7 @@ function resolveSource(argv) {
98
106
  log.info(`Using publish source "${named.name}" from config`);
99
107
  source = named.url;
100
108
  awsProfile = awsProfile ?? named.profile;
101
- token = token ?? named.token;
109
+ sourceEntry = named;
102
110
  }
103
111
  } else {
104
112
  if (config.publishSources.length === 0) {
@@ -112,9 +120,15 @@ function resolveSource(argv) {
112
120
  log.info(`Using publish source "${first.name}" from config`);
113
121
  source = first.url;
114
122
  awsProfile = awsProfile ?? first.profile;
115
- token = token ?? first.token;
123
+ sourceEntry = first;
116
124
  }
117
125
 
126
+ // Literal --token beats --token-env beats config.token beats config.tokenEnv.
127
+ const { token } = resolveSourceAuth(
128
+ { token: argv.token, tokenEnv: argv.tokenEnv },
129
+ sourceEntry,
130
+ );
131
+
118
132
  return { source, awsProfile, token };
119
133
  }
120
134
 
@@ -265,9 +279,32 @@ async function publish(argv) {
265
279
  os.tmpdir(),
266
280
  `wyvrnpm-manifest-${name}-${version}-${Date.now()}.json`,
267
281
  );
282
+ // ── publishedConf (PLAN-CONF.md §4.6) ────────────────────────────────────
283
+ // Publish reads ONLY the recipe conf + CLI --conf — wyvrn.local.json is
284
+ // intentionally ignored (plan §4.5 / §8.4) so dev-local overlays never
285
+ // leak into shared artefacts. Recorded on the published manifest as
286
+ // informational metadata: it tells maintainers what the publisher's
287
+ // CMake cache vars were at build time but does NOT drive resolution on
288
+ // the consumer side.
289
+ let publishedConf = {};
290
+ try {
291
+ const recipeConfFlat = readRecipeConf(manifest);
292
+ const cli = parseCliConf(argv.conf);
293
+ publishedConf = mergeConfLayers(recipeConfFlat, {}, cli);
294
+ } catch (err) {
295
+ log.error(`conf resolution failed: ${err.message}`);
296
+ process.exit(1);
297
+ }
298
+ if (Object.keys(publishedConf).length > 0) {
299
+ log.info(`Published conf: ${JSON.stringify(publishedConf)}`);
300
+ }
301
+
268
302
  const manifestForPublish = {
269
303
  ...manifest,
270
304
  compatibility: manifest.compatibility ?? defaultCompatBlock(),
305
+ ...(Object.keys(publishedConf).length > 0
306
+ ? { publishedConf: unflattenConf(publishedConf) }
307
+ : {}),
271
308
  };
272
309
  fs.writeFileSync(tmpManifestPath, JSON.stringify(manifestForPublish, null, 2), 'utf8');
273
310
  if (!manifest.compatibility) {
@@ -288,7 +325,7 @@ async function publish(argv) {
288
325
  let installOverlayPaths = new Set();
289
326
  if (hasBuildRecipe(manifest)) {
290
327
  try {
291
- const recipe = normalizeRecipe(manifest.build, effectiveOptions);
328
+ const recipe = normalizeRecipe(manifest.build, effectiveOptions, manifest);
292
329
  const candidate = path.join(
293
330
  srcDir, 'build', `wyvrn-${activeProfileName}`, recipe.installDir,
294
331
  );
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { readConfig } = require('../config');
4
- const { getProvider } = require('../providers');
5
- const log = require('../logger');
3
+ const { readConfig } = require('../config');
4
+ const { getProvider } = require('../providers');
5
+ const { resolveSourceAuth } = require('../auth');
6
+ const log = require('../logger');
6
7
 
7
8
  /**
8
9
  * `wyvrnpm show <pkg>` — surface registry metadata for a package.
@@ -145,6 +146,11 @@ async function show(argv) {
145
146
  if (meta?.options && Object.keys(meta.options).length > 0) {
146
147
  console.log(` declared: ${formatDeclaredOptions(meta.options)}`);
147
148
  }
149
+ // PLAN-CONF.md §4.6 — surface publisher's publishedConf.
150
+ // Informational only; absent for pre-2.6.0 author publishes.
151
+ if (meta?.publishedConf && Object.keys(meta.publishedConf).length > 0) {
152
+ console.log(` conf : ${JSON.stringify(meta.publishedConf)}`);
153
+ }
148
154
  }
149
155
  }
150
156
 
@@ -157,6 +163,7 @@ async function show(argv) {
157
163
  uploadedBy: meta?.uploadedBy ?? null,
158
164
  compatibility: meta?.compatibility ?? null,
159
165
  declaredOptions: meta?.options ?? null,
166
+ publishedConf: meta?.publishedConf ?? null,
160
167
  });
161
168
  }
162
169
 
@@ -202,7 +209,10 @@ function resolveSources(argv) {
202
209
  return {
203
210
  sources,
204
211
  awsProfile: argv.awsProfile ?? firstConfig?.profile,
205
- token: argv.token ?? firstConfig?.token,
212
+ token: resolveSourceAuth(
213
+ { token: argv.token, tokenEnv: argv.tokenEnv },
214
+ firstConfig,
215
+ ).token,
206
216
  };
207
217
  }
208
218