wyvrnpm 2.8.1 → 2.9.0

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.
package/README.md CHANGED
@@ -4,7 +4,7 @@ A simple, private C++ package manager that works with any static file hosting pr
4
4
 
5
5
  There is no central registry. You control where packages are hosted.
6
6
 
7
- > **README version: 2.8.1** — Highlights since 2.4.0: non-ABI build-time `conf` block (CMake cache variables via recipe / team profile / `wyvrn.local.json` / CLI `--conf`), per-package `source` pin for multi-source defense, profile inheritance via `extends`, pattern-based `settingsOverrides` (`boost*: {...}`), `wyvrnpm cache list` / `cache prune` for selective source-build-cache cleanup, `install --dry-run` (JSON plan without side effects), `install --build=always` to force source-rebuild, `--token-env` for CI credentials, and a raft of security hardening (Zip Slip, MSVC arch allow-list). Full list in `claude/EVALUATION.md`.
7
+ > **README version: 2.8.3** — Highlights since 2.4.0: non-ABI build-time `conf` block (CMake cache variables via recipe / team profile / `wyvrn.local.json` / CLI `--conf`), per-package `source` pin for multi-source defense, profile inheritance via `extends`, pattern-based `settingsOverrides` (`boost*: {...}`), `wyvrnpm cache list` / `cache prune` for selective source-build-cache cleanup, `install --dry-run` (JSON plan without side effects), `install --build=always` to force source-rebuild, `--token-env` for CI credentials, CloudFront/WAF-compatible HTTP provider (sends `User-Agent`, surfaces non-404 errors), publish preserves dependency ranges verbatim with lock snapshot moved to informational `publishedLock`, and a raft of security hardening (Zip Slip, MSVC arch allow-list). Full list in `claude/EVALUATION.md`.
8
8
 
9
9
  ---
10
10
 
@@ -285,6 +285,13 @@ wyvrnpm build
285
285
  # Build Debug, verbose compiler output
286
286
  wyvrnpm build --config Debug --verbose
287
287
 
288
+ # Build multiple configs in one go — configure runs once, build loops per config
289
+ wyvrnpm build --config Debug,Release --install
290
+
291
+ # Build every config the recipe declares (recipe's build.configs, or all four
292
+ # canonical configs if no recipe is present). Pairs naturally with --install.
293
+ wyvrnpm build --all-configs --install
294
+
288
295
  # Use a specific profile's preset
289
296
  wyvrnpm build --profile release
290
297
 
@@ -318,7 +325,8 @@ wyvrnpm build -- -j 8
318
325
  |---|---|---|
319
326
  | `--preset` / `-P` | `wyvrn-<profile>` | CMake configure preset name. |
320
327
  | `--profile` / `-p` | *(config / "default")* | Build profile — determines the default preset name. |
321
- | `--config` / `-c` | `Release` | Build configuration (`Debug` \| `Release` \| `RelWithDebInfo` \| `MinSizeRel`). |
328
+ | `--config` / `-c` | `Release` | Build configuration (`Debug` \| `Release` \| `RelWithDebInfo` \| `MinSizeRel`). Comma-separated to build multiple in one invocation (e.g. `Debug,Release`) — configure runs once, build (+ `--install`) loops per config. |
329
+ | `--all-configs` | `false` | Build every config from the recipe's `build.configs` (or all four canonical configs when no recipe is present). Takes precedence over `--config`. |
322
330
  | `--target` / `-t` | *(all)* | Specific CMake target to build. |
323
331
  | `--verbose` / `-v` | `false` | Pass `--verbose` to `cmake --build`. |
324
332
  | `--clean` | `false` | Remove the build directory before configuring. |
@@ -546,8 +554,6 @@ You only need this if you want Claude Code to pick up the skill. The CLI itself
546
554
  | `--force` | `false` | Overwrite an existing skill install. Without it, the command refuses to clobber. |
547
555
  | `--dry-run` | `false` | Print the destination paths and intended actions without writing. |
548
556
 
549
- **CLAUDE.md §9 note for contributors:** the tested path for deploying a locally-edited skill is `node scripts/repack-skill.js && node bin/wyvrn.js install-skill --claude --force` — never `rm -rf` + `cp -r`.
550
-
551
557
  ---
552
558
 
553
559
  ### `wyvrnpm link`
@@ -1736,6 +1742,15 @@ wyvrnpm build --install --install-prefix C:\out\stage
1736
1742
 
1737
1743
  # Install a specific config
1738
1744
  wyvrnpm build --install --config Debug
1745
+
1746
+ # Install multiple configs in one invocation — configure runs once,
1747
+ # build + install loops per config. Before 2.9.0 this required two
1748
+ # separate `wyvrnpm build --install ...` invocations.
1749
+ wyvrnpm build --install --config Debug,Release
1750
+
1751
+ # Install every config declared in the recipe's build.configs
1752
+ # (or all four canonical configs when no recipe is present).
1753
+ wyvrnpm build --install --all-configs
1739
1754
  ```
1740
1755
 
1741
1756
  ### What it does, in order
package/bin/wyvrn.js CHANGED
@@ -191,9 +191,21 @@ yargs
191
191
  .option('config', {
192
192
  alias: 'c',
193
193
  type: 'string',
194
- choices: ['Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel'],
195
194
  default: 'Release',
196
- description: 'Build configuration',
195
+ description:
196
+ 'Build configuration. Accepts a single value (Release) or a ' +
197
+ 'comma-separated list (Debug,Release) to build multiple configs ' +
198
+ 'in one invocation — configure runs once, build (+ --install) ' +
199
+ 'loops per config. Valid: Debug | Release | RelWithDebInfo | MinSizeRel.',
200
+ })
201
+ .option('all-configs', {
202
+ type: 'boolean',
203
+ default: false,
204
+ description:
205
+ 'Build every config declared in the recipe\'s build.configs ' +
206
+ '(falls back to all four canonical configs when no recipe is ' +
207
+ 'present). Equivalent to spelling out --config Debug,Release,... ' +
208
+ 'by hand. Pairs naturally with --install.',
197
209
  })
198
210
  .option('target', {
199
211
  alias: 't',
@@ -262,6 +274,7 @@ yargs
262
274
  ...argv,
263
275
  autoInstall: argv['auto-install'],
264
276
  installPrefix: argv['install-prefix'],
277
+ allConfigs: argv['all-configs'],
265
278
  }),
266
279
  )
267
280
 
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyvrnpm",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "A simple, static-hosting-compatible C++ package manager",
5
5
  "keywords": [
6
6
  "c++",
@@ -15,6 +15,7 @@ const { hashProfile, mergeProfile, sha256Of } = require('../profile');
15
15
  const { generateToolchain } = require('../toolchain');
16
16
  const { normalizeOptionsDeclaration, resolveEffectiveOptions } = require('../options');
17
17
  const { readRecipeConf, cmakeCacheVariables } = require('../conf');
18
+ const { wyvrnFetch } = require('../http-fetch');
18
19
  const log = require('../logger');
19
20
 
20
21
  /**
@@ -195,7 +196,7 @@ async function buildFromSource(args) {
195
196
  log.info(` resolving ${Object.keys(depsForResolution).length} transitive dep(s)`);
196
197
  const transitiveManifests = new Map();
197
198
  const resolvedDeps = await resolveDependencies(
198
- depsForResolution, packageSources, platform, fetch, new Map(),
199
+ depsForResolution, packageSources, platform, wyvrnFetch, new Map(),
199
200
  transitiveManifests,
200
201
  );
201
202
 
@@ -231,7 +232,7 @@ async function buildFromSource(args) {
231
232
  if (enrichedDeps.size > 0) {
232
233
  await downloadDependencies(
233
234
  enrichedDeps, packageSources, platform, paths.internal,
234
- fetch, Math.max(30, Math.round(timeoutMs / 1000)),
235
+ wyvrnFetch, Math.max(30, Math.round(timeoutMs / 1000)),
235
236
  { ...authOptions, buildMode },
236
237
  );
237
238
  } else {
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const { readManifest, writeManifest } = require('../manifest');
6
6
  const { resolveLatestVersion } = require('../resolve');
7
7
  const { readConfig } = require('../config');
8
+ const { wyvrnFetch } = require('../http-fetch');
8
9
  const log = require('../logger');
9
10
 
10
11
  /**
@@ -99,7 +100,7 @@ async function add(argv) {
99
100
  versionToWrite = spec.version;
100
101
  log.info(`${spec.name}: writing "${versionToWrite}" (as given)`);
101
102
  } else {
102
- const latest = await resolveLatestVersion(spec.name, packageSources, argv.platform, fetch);
103
+ const latest = await resolveLatestVersion(spec.name, packageSources, argv.platform, wyvrnFetch);
103
104
  versionToWrite = `^${latest}`;
104
105
  log.info(`${spec.name}: latest is ${latest} → writing "${versionToWrite}"`);
105
106
  }
@@ -4,14 +4,11 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { spawn } = require('child_process');
6
6
 
7
- const { readConfig } = require('../config');
8
- const { loadProfile } = require('../profile');
9
- const { readManifest } = require('../manifest');
10
- const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
7
+ const { normalizeRecipe, hasBuildRecipe, VALID_CMAKE_CONFIGS, DEFAULT_RECIPE } = require('../build/recipe');
11
8
  const { loadMsvcEnv } = require('../build/msvc-env');
12
9
  const { archToVsPlatform, isVisualStudioGenerator } = require('../toolchain');
13
- const { parseCliConf } = require('../conf');
14
10
  const { LOCAL_OVERLAY_FILENAME } = require('../conf');
11
+ const { buildContext } = require('../context');
15
12
  const install = require('./install');
16
13
  const log = require('../logger');
17
14
 
@@ -141,6 +138,51 @@ function buildConfigureArgs({
141
138
  return args;
142
139
  }
143
140
 
141
+ /**
142
+ * Resolve the list of CMake configs to build (and optionally install).
143
+ *
144
+ * Precedence:
145
+ * 1. `--all-configs` → recipe.configs (falls back to all four canonical
146
+ * configs when no recipe declares any).
147
+ * 2. `--config` → comma-separated list, validated against
148
+ * VALID_CMAKE_CONFIGS. Order preserved. Duplicates removed.
149
+ * 3. default → `['Release']`.
150
+ *
151
+ * Configure runs once per `wyvrnpm build` invocation regardless — multi-
152
+ * config generators (VS, Xcode, Ninja Multi-Config) produce all configs
153
+ * from one configure step. Single-config generators (plain Ninja, Make)
154
+ * bake CMAKE_BUILD_TYPE into the cache, so `cmake --build --config X`
155
+ * effectively ignores X for them; building two configs against a single-
156
+ * config generator rebuilds the same binary twice. That's wasteful but
157
+ * correct; fixing it properly requires per-config build dirs, which is
158
+ * out of scope for this helper.
159
+ *
160
+ * @param {{ argv: object, recipeConfigs?: string[] }} args
161
+ * @returns {string[]}
162
+ */
163
+ function resolveConfigList({ argv, recipeConfigs = null }) {
164
+ if (argv.allConfigs) {
165
+ const fromRecipe = (recipeConfigs ?? []).filter((c) => VALID_CMAKE_CONFIGS.has(c));
166
+ return fromRecipe.length > 0 ? [...fromRecipe] : [...DEFAULT_RECIPE.configs];
167
+ }
168
+
169
+ const raw = typeof argv.config === 'string' && argv.config.trim() ? argv.config : 'Release';
170
+ const list = raw.split(',').map((s) => s.trim()).filter(Boolean);
171
+
172
+ const invalid = list.filter((c) => !VALID_CMAKE_CONFIGS.has(c));
173
+ if (invalid.length > 0) {
174
+ const valid = [...VALID_CMAKE_CONFIGS].join(' | ');
175
+ throw new Error(`invalid --config "${invalid.join(', ')}" — expected ${valid}`);
176
+ }
177
+
178
+ const seen = new Set();
179
+ const deduped = [];
180
+ for (const c of list) {
181
+ if (!seen.has(c)) { seen.add(c); deduped.push(c); }
182
+ }
183
+ return deduped.length > 0 ? deduped : ['Release'];
184
+ }
185
+
144
186
  /**
145
187
  * Args passed to `cmake` for the build step.
146
188
  *
@@ -225,13 +267,34 @@ function runCmake(args, env = null) {
225
267
  * @param {object} argv
226
268
  */
227
269
  async function build(argv) {
228
- const rootDir = path.resolve(argv.root);
229
- const manifestPath = path.resolve(argv.manifest);
270
+ // Shared per-invocation state. See src/context.js +
271
+ // claude/PLAN-COMMAND-CONTEXT.md. Parses --conf up front so a malformed
272
+ // flag fails here instead of after cmake's already configured.
273
+ let ctx;
274
+ try {
275
+ ctx = buildContext(argv);
276
+ } catch (err) {
277
+ log.error(err.message);
278
+ process.exit(1);
279
+ }
230
280
 
231
- const config = readConfig();
232
- const profileName = argv.profile ?? config.defaultProfile ?? 'default';
281
+ const { rootDir, manifestPath } = ctx;
282
+ const { name: profileName, profile: activeProfile } = ctx.profiles.host;
233
283
  const presetName = resolvePresetName({ presetOverride: argv.preset, profileName });
234
- const buildConfig = argv.config ?? 'Release';
284
+
285
+ // Resolve the list of configs up front so a malformed --config fails
286
+ // before auto-install kicks off. `--all-configs` consults the recipe
287
+ // (if any); otherwise comma-separated --config, defaulting to Release.
288
+ let configList;
289
+ try {
290
+ const recipe = hasBuildRecipe(ctx.manifest)
291
+ ? (() => { try { return normalizeRecipe(ctx.manifest.build, null); } catch { return null; } })()
292
+ : null;
293
+ configList = resolveConfigList({ argv, recipeConfigs: recipe?.configs ?? null });
294
+ } catch (err) {
295
+ log.error(err.message);
296
+ process.exit(1);
297
+ }
235
298
 
236
299
  // ── Stale check / auto-install ────────────────────────────────────────────
237
300
  // `--auto-install` gates the "run `wyvrnpm install` first when the
@@ -278,13 +341,12 @@ async function build(argv) {
278
341
  let effectiveGenerator = argv.generator ?? null;
279
342
  let effectiveProfile = null;
280
343
  try {
281
- const { profile: activeProfile } = loadProfile(profileName);
282
344
  effectiveProfile = activeProfile;
283
345
  // Which generator will the preset actually use? Read it from the
284
346
  // project's own recipe if one exists. If not, default to null and let
285
347
  // cmake/CMakePresets decide — in which case MSVC env activation is
286
348
  // skipped (because VS is usually the default on Windows).
287
- const manifest = readManifest(manifestPath);
349
+ const manifest = ctx.manifest;
288
350
  let presetGenerator = null;
289
351
  if (hasBuildRecipe(manifest)) {
290
352
  try {
@@ -321,16 +383,10 @@ async function build(argv) {
321
383
  // CLI --conf → -D / -U passthrough (PLAN-CONF.md §7). The preset's
322
384
  // baked-in cacheVariables (from the last install) already reflect the
323
385
  // recipe + local-overlay layers; CLI conf layers on top for this
324
- // invocation only.
325
- let cliConfSets, cliConfUnsets;
326
- try {
327
- const cli = parseCliConf(argv.conf);
328
- cliConfSets = cli.sets;
329
- cliConfUnsets = cli.unsets;
330
- } catch (err) {
331
- log.error(`--conf parse error: ${err.message}`);
332
- process.exit(1);
333
- }
386
+ // invocation only. ctx.cliConf is pre-parsed by buildContext; malformed
387
+ // values already threw above.
388
+ const cliConfSets = ctx.cliConf.sets;
389
+ const cliConfUnsets = ctx.cliConf.unsets;
334
390
 
335
391
  const configureArgs = buildConfigureArgs({
336
392
  presetName,
@@ -342,35 +398,43 @@ async function build(argv) {
342
398
  log.info(`cmake ${configureArgs.join(' ')}`);
343
399
  await runCmake(configureArgs, cmakeEnv);
344
400
 
345
- // ── Build ─────────────────────────────────────────────────────────────────
346
- const buildArgs = buildBuildArgs({
347
- binaryDir,
348
- config: buildConfig,
349
- target: argv.target,
350
- verbose: argv.verbose,
351
- passthru: argv['--'] ?? [],
352
- });
353
- log.info(`cmake ${buildArgs.join(' ')}`);
354
- await runCmake(buildArgs, cmakeEnv);
355
-
356
- // ── Install (optional) ────────────────────────────────────────────────────
357
- // `--install` runs `cmake --install <binaryDir>` after the build. Prefix
358
- // defaults to whatever was baked into the cache at configure time; pass
359
- // `--install-prefix` to override without reconfiguring.
360
- if (argv.install) {
361
- const installArgs = buildInstallArgs({
401
+ // ── Build (+ optional install) per config ─────────────────────────────────
402
+ // Configure ran once above. Multi-config generators (VS, Xcode, Ninja
403
+ // Multi-Config) produce every config from that one configure step; for
404
+ // single-config generators each iteration simply re-`cmake --build`s the
405
+ // same baked CMAKE_BUILD_TYPE (harmless duplication). `--install` runs
406
+ // per config too — the usual reason to want multi-config is so that a
407
+ // downstream `find_package()` can pick whichever it needs.
408
+ if (configList.length > 1) {
409
+ log.info(`Building ${configList.length} configs: ${configList.join(', ')}`);
410
+ }
411
+
412
+ for (const config of configList) {
413
+ const buildArgs = buildBuildArgs({
362
414
  binaryDir,
363
- config: buildConfig,
364
- prefix: argv.installPrefix,
415
+ config,
416
+ target: argv.target,
417
+ verbose: argv.verbose,
418
+ passthru: argv['--'] ?? [],
365
419
  });
366
- log.info(`cmake ${installArgs.join(' ')}`);
367
- await runCmake(installArgs, cmakeEnv);
420
+ log.info(`cmake ${buildArgs.join(' ')}`);
421
+ await runCmake(buildArgs, cmakeEnv);
422
+
423
+ if (argv.install) {
424
+ const installArgs = buildInstallArgs({
425
+ binaryDir,
426
+ config,
427
+ prefix: argv.installPrefix,
428
+ });
429
+ log.info(`cmake ${installArgs.join(' ')}`);
430
+ await runCmake(installArgs, cmakeEnv);
431
+ }
368
432
  }
369
433
 
370
434
  const suffix = argv.install
371
435
  ? ` + installed${argv.installPrefix ? ` to ${argv.installPrefix}` : ''}`
372
436
  : '';
373
- log.success(`Build complete: ${binaryDir} (${buildConfig})${suffix}`);
437
+ log.success(`Build complete: ${binaryDir} (${configList.join(', ')})${suffix}`);
374
438
  }
375
439
 
376
440
  module.exports = build;
@@ -381,6 +445,7 @@ module.exports._helpers = {
381
445
  isToolchainStale,
382
446
  resolvePresetName,
383
447
  resolveBinaryDir,
448
+ resolveConfigList,
384
449
  buildConfigureArgs,
385
450
  buildBuildArgs,
386
451
  buildInstallArgs,