npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

wyvrnpm

v2.18.0

Published

A simple, static-hosting-compatible C++ package manager

Readme

wyvrnpm

A simple, private C++ package manager that works with any static file hosting provider — Amazon S3, a plain HTTP/HTTPS server, a local directory, or an SMB/UNC network share.

There is no central registry. You control where packages are hosted.

README version: 2.15.0

New in 2.15.0:

  • buildDependencies block + whenOption gating. Recipes declare author-private deps (asio / websocketpp / boost-internals — header-only libs that compile into a StaticLib; protoc / flex / bison — tools invoked at configure time) in a sibling block to dependencies. Walked at the author's root and at source-build of a transitive, but never walked transitively when a downstream consumer pulls the prebuilt binary — they get the lean runtime closure. whenOption: "websocket" (or "api=legacy") gates a dep on a recipe-declared option value. Subsumes Conan's test_requires: options.tests + gtest in buildDependencies with whenOption: "tests" → consumers of the prebuilt binary never see it. Closes EVALUATION.md F3; cross-compilation half stays open as F4.
  • npm self-update notifier. When a newer wyvrnpm is on the npm registry, the next interactive run prints [wyvrn] warn wyvrnpm <old> → <new> available on stderr. The fetch runs in a detached background process — the user's command never blocks on the network; the banner appears on the following invocation. Auto-suppressed in CI, non-TTY, and --format json; opt out via WYVRNPM_NO_UPDATE_CHECK=1 or --no-update-check. Hand-rolled (~120 lines, zero new deps).
  • Resolver caches @latest lookups within a single wyvrnpm install. When a package is referenced as @latest from multiple parents (or already resolved via a range), the registry round-trip is reused. Eliminates ~12 redundant HTTPS round-trips on a typical 16-dep graph behind CloudFront. Brings latestCache into line with the existing manifestCache / publishedCache.
  • Per-config artefact slicing. Recipes opt in via build.publishPerConfig: true; wyvrnpm publish emits one wyvrn-<Config>.zip per declared CMake config under one profileHash URL. Consumers fetch only what they need — wyvrnpm install --request-configs Release pulls 1.93 GB of gRPC instead of 14.96 GB across all four configs (~87% saving). Driven by CMake's own *Targets-<config>.cmake exports, so both the MSVC convention (lib/<Config>/foo.lib) and the postfix convention (lib/foo.lib vs lib/food.lib) classify correctly. wyvrn.local.json gains requestConfigs + depRequestConfigs. Attestation v2 binds an artefactSha256: { config → sha } map under one signature.
  • wyvrnpm publish --dry-run materialises the would-be-uploaded artefacts to <build>/dry-run-publish/ for inspection (override with --dry-run-out), and tolerates expired AWS credentials (wouldOverwrite: null instead of crashing).

Highlights since 2.8.3:

  • S1 artefact signing. Ed25519 detached signatures over a canonical attestation, verified against a per-registry .trust/keys.json. wyvrnpm key gen / show-pub, wyvrnpm trust list / refresh, wyvrnpm configure signing set-default / show / unset. --require-signatures on install; --no-sign / --signing-key-env / --signing-key-file on publish. Pure-Node crypto — no external tooling, identical on Windows / Linux / macOS. Closes S1 + S3.
  • compiler.cppstd is authoritative. CMAKE_CXX_STANDARD is set unconditionally before project(); cpp.cmake no longer touches it post-project(). A -DCMAKE_CXX_STANDARD=23 on the cmake CLI cannot defeat a cppstd=20 profile.
  • Honest install exit codes. wyvrnpm install exits non-zero on partial failure (sha256-mismatch, extract-failed, no-exact-match, source-build-failed, not-found) with a per-dep failure summary, instead of misreporting "Done — N installed".
  • Streaming publish via archiver. Handles >2 GiB artefacts that adm-zip choked on — e.g. gRPC's RelWithDebInfo static libs.
  • --build-dir <path> + wyvrn.local.json:binaryDirRoot. Sidesteps build/ case-insensitive-FS collisions on repos with a Bazel/Buck BUILD file at the root (gRPC, …).
  • wyvrnpm version — read / set / partially update the top-level version field from CI (wyvrnpm version --build "$BUILD_ID").
  • wyvrnpm bootstrap <git-url> — scaffold a first-draft wyvrn.json for OSS C++ libraries (fmt, spdlog, zlib, …) with cookbook-driven defaults.
  • wyvrnpm publish --dry-run — npm-style preview of the upload plan; wouldOverwrite flag in --format json.
  • wyvrnpm build --config Debug,Release / --all-configs — build multiple configurations from a single configure.
  • CloudFront brotli-vs-identity cache split fix — resolved a class of bugs where show listed a freshly-published version that install claimed didn't exist.

Full list in claude/EVALUATION.md.


Install

npm install -g wyvrnpm

Requires Node.js >= 18.

Update notifier

When a newer wyvrnpm is published to npm, the next interactive run prints a single-line banner on stderr:

[wyvrn] warn  wyvrnpm 2.13.12 → 2.14.0 available. Run `npm i -g wyvrnpm` to update.

The check is non-blocking — the registry round-trip happens in a detached background process, so your command never waits on the network. The banner appears on the following invocation. Auto-suppressed in CI ($CI set), non-TTY runs (redirected stdout, pipelines), and --format json mode. Opt out manually via WYVRNPM_NO_UPDATE_CHECK=1 or --no-update-check.


Quick Start

# 1. Initialise a manifest in your project
wyvrnpm init

# 2. Configure your package sources once
wyvrnpm configure add-source --kind install --name corp-s3 --url s3://my-bucket/packages --profile my-sso
wyvrnpm configure add-source --kind publish --name default  --url s3://my-bucket/packages --profile my-sso

# 3. Detect and save your build environment as the default profile
wyvrnpm configure profile detect

# 4. Add dependencies (resolves latest from your configured sources)
wyvrnpm add zlib fmt@^10.0.0

# 5. Install dependencies (resolved against your build profile)
wyvrnpm install

# 6. Configure and build via the generated CMake preset
wyvrnpm build

# 7. Publish your package (uses the active profile automatically)
wyvrnpm publish

# 8. (Optional) Link local packages for development
cd ../my-local-lib && wyvrnpm link
cd ../my-app && wyvrnpm link my-local-lib

Build Profiles

v2 introduces build profiles — named snapshots of your build environment (OS, architecture, compiler, C++ standard, runtime linkage). Each published binary is tagged with a profile hash so installs can resolve the correct prebuilt binary automatically.

Profile fields mirror Conan 2.0 settings:

| Field | Example values | |---|---| | os | Windows Linux Macos | | arch | x86_64 x86 armv8 | | compiler | msvc gcc clang apple-clang | | compiler.version | 193 13 17 | | compiler.cppstd | 14 17 20 23 | | compiler.runtime | dynamic static (MSVC only) |

Profiles are stored in the platform config directory:

| Platform | Path | |---|---| | Windows | %LOCALAPPDATA%\wyvrnpm\profiles\ | | Linux / macOS | ~/.config/wyvrnpm/profiles/ |

Typical profile workflow

# Auto-detect your current environment and save it as "default"
wyvrnpm configure profile detect

# Or save under a specific name (e.g. for a release build with static runtime)
wyvrnpm configure profile detect --name release
wyvrnpm configure profile set --name release --runtime static

# List all saved profiles (* marks the default)
wyvrnpm configure profile list

# Switch the default profile
wyvrnpm configure profile set-default release

Once a default profile is saved, install and publish use it automatically — no flags required.

Profile inheritance — extends (2.8.0+)

Teams often end up with a small fleet of near-duplicate profiles (windows-msvc-debug, windows-msvc-release, windows-msvc-x86, …) that drift over time. A profile can inherit from another via extends:

{
  "extends": "windows-msvc-base",
  "compiler.cppstd": "23"
}

Or from multiple parents (later entries override earlier):

{
  "extends": ["windows-base", "team-defaults"],
  "compiler.runtime": "static"
}

Rules:

  • [settings] fields merge per-key — child wins.
  • The conf block (if present) merges per-leaf — child overrides single keys without dropping siblings.
  • Overlay value null explicitly unsets a parent field (Conan's None semantics).
  • Inheritance cycles are detected and fail with the full trail (alpha → beta → alpha).
  • Missing parent names fail fast — no silent drop.
  • extends itself is stripped from the resolved profile and is not folded into profileHash, so { "extends": "base" } with no overrides hashes byte-identically to base.

wyvrnpm configure profile show <name> prints the raw extends: chain alongside the resolved [settings] so you can inspect inheritance without grepping JSON.


Commands

wyvrnpm init

Creates a wyvrn.json manifest in the current directory.

wyvrnpm init
wyvrnpm init --root ./my-project

wyvrnpm bootstrap

Scaffolds a first-draft wyvrn.json for an open-source C++ library so it can be packaged into your wyvrnpm registry with a single review pass instead of hand-writing a manifest from scratch. Clones the upstream git repo, detects name / version / kind from the checkout, applies known-library CMake flag hints (skip tests, skip examples, flip install gates), and prints a TODO report for everything a machine can't decide for you.

Designed for batch-converting a list of forked upstreams (fmt, spdlog, zlib, Catch2, etc.) into your private registry.

# Typical single-repo conversion
wyvrnpm bootstrap https://github.com/fmtlib/fmt.git --dest ./pkgs/fmt
cd ./pkgs/fmt                           # review wyvrn.json, settle TODOs
wyvrnpm install
wyvrnpm build --install --all-configs
wyvrnpm publish --source team-s3

# Override the derived name / version / kind
wyvrnpm bootstrap https://github.com/nlohmann/json.git \
    --dest ./pkgs/nlohmann-json \
    --name nlohmann-json \
    --kind HeaderOnlyLib

# Work on a directory you already cloned
wyvrnpm bootstrap --no-clone --dest ./pkgs/fmt --force

# Preview without writing — TODO report only
wyvrnpm bootstrap https://github.com/google/re2.git --dest /tmp/re2 --dry-run

# Batch driver — one shell loop, no wyvrnpm subcommand needed
while read url; do
  wyvrnpm bootstrap "$url" --dest "./pkgs/$(basename "$url" .git)" --force
done < urls.txt
# then review, build, and publish each in a loop of your own

Options

| Option | Default | Description | |---|---|---| | <url> positional | (required) | Upstream git URL. Accepts HTTPS or SSH form. | | --dest <dir> | basename(url) | Destination directory for the clone. | | --ref <ref> | (remote HEAD) | Clone at a specific branch / tag / SHA. | | --name | (from URL) | Override the derived package name. | | --pkg-version | (nearest git tag) | Override the version. Named oddly so yargs doesn't confuse it with the built-in --version. | | --kind | (auto-detect) | Force ConsoleApp / StaticLib / DynamicLib / HeaderOnlyLib / etc. | | --description | (TODO placeholder) | One-line description written into wyvrn.json. | | --no-clone | false | Skip the clone step; operate on an already-populated --dest. | | --shallow | false | --depth 1 clone. Faster, but git tag detection degrades — pair with --pkg-version when using this. | | --dry-run | false | Print the proposed manifest + TODOs; don't write wyvrn.json. | | --force | false | Overwrite an existing destination and/or wyvrn.json. | | --format | text | Output format: text (human) or json (CI — one object on stdout). |

The cookbook of per-library hints lives in src/bootstrap/cookbook.js. Current coverage is ~30 popular libraries (fmt, spdlog, nlohmann/json, yaml-cpp, zlib, Catch2, lz4, brotli, hiredis, redis-plus-plus, GLM, SQLiteCpp, re2, …). Adding a new entry is a two-line change — submit a PR.

The patterns bootstrap detects (header-only with INTERFACE target, standard CMake static lib, CMake with wyvrnpm dep, ABI-option'd lib, non-CMake upstream) are catalogued in the bundled Agent Skill at claude/skills/wyvrnpm/references/oss-conversion-patterns.md — Claude reads it when you ask "how do I convert X to a wyvrnpm package?".


wyvrnpm add

Adds one or more dependencies to wyvrn.json without downloading them. Useful when you know the package name but not the latest version.

  • Bare name — resolves latest.json from your configured install sources and writes "^<latest>" (caret range, major-pinned).
  • name@<version> — writes the given string verbatim. Accepts exact pins (1.2.3.4), caret/tilde ranges (^1.2.3.4, ~1.2.3.4), and explicit comparators (>=1.80.0 <2.0.0).

If the dependency is already listed, it is replaced. When the prior entry was an object ({ version, settings, options }), the settings and options are preserved — only the version field is updated.

add does not run install. Chain wyvrnpm install afterwards to download.

# Resolve latest, write "^<latest>"
wyvrnpm add zlib

# Pin exactly
wyvrnpm add [email protected]

# Use a caret / tilde / comparator range
wyvrnpm add fmt@^10.0.0
wyvrnpm add boost@">=1.80.0 <2.0.0"

# Multiple in one call
wyvrnpm add zlib [email protected] spdlog

Options

| Option | Default | Description | |---|---|---| | --source / -s | (config) | Base URL used for the latest lookup. Repeat for multiple. | | --platform | win_x64 | v1 legacy platform sub-path (used as a fallback during latest lookup). | | --manifest | ./wyvrn.json | Path to the manifest file. |


wyvrnpm version

Read, set, or partially update the top-level version field in wyvrn.json. Designed for CI/CD — bumps the build component from a pipeline variable, prints the result for capture, and never touches git (CI pipelines own tagging and commits).

  • Bare — prints the current version on stdout, naked (no [wyvrn] prefix), so VER=$(wyvrnpm version) works straight away.
  • Full setwyvrnpm version 1.2.3.4 writes the value verbatim. Strict 4-part x.y.z.b validation; leading zeros and 3-part semver are rejected.
  • Component flags--major / --minor / --patch / --build N replace one component while leaving the others alone. Combine flags freely (--minor 3 --build 0).
  • Positional and component flags are mutually exclusive — pick a mode.
# Read the current version (CI capture)
VER=$(wyvrnpm version)

# Inject the build number from a pipeline variable
wyvrnpm version --build "$BUILD_ID"

# Cut a minor release — reset the build component to 0
wyvrnpm version --minor 3 --build 0

# Set a full version verbatim
wyvrnpm version 2.0.0.0

# CI-friendly capture
wyvrnpm version --build "$BUILD_ID" --format json
# {"command":"version","previous":"1.2.3.4","version":"1.2.3.99","written":true,...}

# Preview without writing
wyvrnpm version --build 99 --dry-run

Options

| Option | Default | Description | |---|---|---| | <newVersion> positional | — | Full 4-part version (e.g. 1.2.3.4). Mutex with --major/--minor/--patch/--build. | | --major | — | Replace the 1st component. | | --minor | — | Replace the 2nd component. | | --patch | — | Replace the 3rd component. | | --build | — | Replace the 4th component (typical CI use). | | --dry-run | false | Compute and print the proposed version without writing. | | --format | text | Output format: text (human) or json (CI — single object on stdout). | | --manifest | ./wyvrn.json | Path to the manifest file. |


wyvrnpm install

Resolves the full dependency graph and downloads all packages.

Source priority: CLI --source values are used if provided. If omitted, sources are loaded from the configuration file.

Profile resolution: --profile selects the named build profile. If omitted, config.defaultProfile is used, falling back to "default". v2 binaries are resolved in this order:

  1. Exact profile-hash match — the preferred path.
  2. Declarative compatibility — each published build may advertise a compatibility block (e.g. "compatible with any msvc.version ≥ 193 on Windows/x86_64"). The best compatible candidate is picked deterministically.
  3. Source build (with --build=missing) — clones the package's source at the published gitSha, resolves its own dependencies, invokes CMake through the shared toolchain generator, and caches the result under %LOCALAPPDATA%\wyvrnpm\bc\. On Windows/MSVC with a Ninja or Make generator, vcvarsall.bat is auto-activated.
  4. v1 legacy path — only when no v2 entry exists for the package at all.

The pre-2.1 "same OS + arch, any compiler" loose fallback is no longer available. Publish a build for your profile, add a declarative compatibility block to the recipe, or rerun with --build=missing to source-build.

# Use configured sources and the default build profile
wyvrnpm install

# Use a specific named profile
wyvrnpm install --profile release

# Override with explicit sources
wyvrnpm install --source https://pkg.example.com/cpp

# Multiple sources — tried in order, first to respond wins
wyvrnpm install \
  --source https://primary.example.com/cpp \
  --source https://mirror.example.com/cpp

# Build any missing binary from source
wyvrnpm install --build=missing

# Force every dep to be rebuilt from source (debugging / reproducibility check)
wyvrnpm install --build=always

# Print the resolved install plan WITHOUT downloading (CI-friendly with --format=json)
wyvrnpm install --dry-run
wyvrnpm install --dry-run --format=json

Options

| Option | Default | Description | |---|---|---| | --source / -s | (config) | Base URL of a package source. Repeat for multiple. Overrides config when provided. | | --profile / -p | (config / "default") | Named build profile to use for v2 binary resolution. | | --build | never | Resolution mode when no exact or compatible profile match exists: never fails; missing clones and builds the package from its published gitRepo@gitSha; always rebuilds even when a binary exists. | | --platform | win_x64 | v1 legacy platform sub-path (used as a fallback when no v2 binary is found). | | --timeout | 300 | HTTP request timeout in seconds. | | --manifest | ./wyvrn.json | Path to the manifest file. | | --root | ./ | Project root (packages extracted to {root}/wyvrn_internal/). | | --dry-run | false | Run the resolver + profile/options math, print the install plan, and exit. No download, no wyvrn.lock, no wyvrn_internal/, no toolchain. Combine with --format=json for CI ingestion (payload carries command: "install-dry-run"). | | --conf | — | Non-ABI build-time override. Repeatable. Format: <namespace.leaf>=<value> — e.g. --conf cmake.cache.CHROMA_ENABLE_TEST=ON. Layered: CLI > wyvrn.local.json > named-profile conf > recipe. Does not fold into profileHash. See Build-time configuration (conf). | | --build-dir | build | Project-relative subdir for the generated preset's binaryDir (${sourceDir}/<root>/wyvrn-<profile>). Use when the repo has a BUILD Bazel/Buck file at root that collides with build/ on case-insensitive filesystems (gRPC, …). Persistent override: set binaryDirRoot in wyvrn.local.json. Validated — must be relative, no .. segments. | | --require-signatures | false | Force S1 signature verification on every dep regardless of the registry's mode.json. Equivalent to mode=require for this run — useful for hardened CI. See Signing & verification. |

Packages are extracted to wyvrn_internal/{name}/. A wyvrn.lock file is written alongside the manifest recording the exact resolved versions, profile, and per-package SHA256. On subsequent installs the lock file pins all previously resolved versions — only newly added dependencies are resolved fresh.

After download, install also generates:

  • wyvrn_internal/wyvrn_toolchain.cmake — CMake toolchain pointing at the resolved packages.
  • wyvrn_internal/wyvrn_deps.cmake — post-project() include for find_package() wiring and runtime DLL copy.
  • wyvrn_internal/wyvrn_profile.json — snapshot of the active build profile.
  • CMakePresets.json (or CMakeUserPresets.json if the project root file is user-owned) with a wyvrn-<profile> configure preset. Presets for other profiles are preserved so multiple profiles coexist.

Toolchain variables exposed to your CMakeLists.txt (valid BEFORE project()):

| Variable | Value | |---|---| | WYVRN_PROFILE_NAME / WYVRN_PROFILE_HASH | Active profile name + 16-char hash | | WYVRN_PROFILE_OS / WYVRN_PROFILE_ARCH | OS + architecture from the profile | | WYVRN_PROFILE_COMPILER / WYVRN_PROFILE_COMPILER_VERSION | Compiler identity | | WYVRN_PROFILE_CPPSTD / WYVRN_PROFILE_RUNTIME | C++ standard + MSVC runtime linkage | | WYVRN_ARCH_SUFFIX | "64" for 64-bit arches, "32" for 32-bit, empty for unmapped. Derived at toolchain-generation time so it's usable before project() — unlike CMAKE_SIZEOF_VOID_P. |

The optional helper WYVRN_APPLY_ARCH_SUFFIX(<target> [BASE_NAME <name>]) from the bundled macros.cmake applies the suffix to a target's OUTPUT_NAME so x64 and x86 builds of the same library coexist on disk without overwriting each other. Target linkage (target_link_libraries) is unaffected — only the produced filename changes.

The bundled variables.cmake puts compiled artefacts inside the CMake binary dir so wyvrnpm clean --all (which sweeps <binaryDirRoot>/wyvrn-*/) catches them:

| Variable | Value | |---|---| | CMAKE_ARCHIVE_OUTPUT_DIRECTORY | ${CMAKE_BINARY_DIR}/lib/ | | CMAKE_LIBRARY_OUTPUT_DIRECTORY | ${CMAKE_BINARY_DIR}/lib/ | | CMAKE_RUNTIME_OUTPUT_DIRECTORY | ${CMAKE_BINARY_DIR}/bin |

With per-profile build dirs (build/wyvrn-x64-msvc/ vs build/wyvrn-x86-msvc/) already isolating multi-arch builds, the output directories themselves don't carry an arch suffix — but WYVRN_APPLY_ARCH_SUFFIX still stamps the suffix onto artefact names (String64.lib vs String32.lib) for the cross-arch ship-together case.

Prior to 2.17.0 outputs landed in ${CMAKE_SOURCE_DIR}/output/{bin,lib}${WYVRN_ARCH_SUFFIX}/ — outside the build dir, so wyvrnpm clean --all left binaries behind. Scripts that hard-code output/bin64/foo.exe must switch to <binaryDir>/bin/<CONFIG>/foo.exe (multi-config generators) or <binaryDir>/bin/foo.exe (single-config). Run wyvrnpm clean --all after upgrade — it sweeps the legacy output/ tree as a migration aid.

project(String CXX)
add_library(String STATIC ...)
WYVRN_APPLY_ARCH_SUFFIX(String)                 # → String64.lib / String32.lib
WYVRN_APPLY_ARCH_SUFFIX(String BASE_NAME my-str) # → my-str64.lib / my-str32.lib

wyvrnpm build

Configures and builds the project through the wyvrnpm-generated CMake preset. Auto-runs install first if wyvrn_toolchain.cmake is missing or wyvrn.lock is newer than it.

# Configure + build with the default profile's preset (wyvrn-default)
wyvrnpm build

# Build Debug, verbose compiler output
wyvrnpm build --config Debug --verbose

# Build multiple configs in one go — configure runs once, build loops per config
wyvrnpm build --config Debug,Release --install

# Build every config the recipe declares (recipe's build.configs, or all four
# canonical configs if no recipe is present). Pairs naturally with --install.
wyvrnpm build --all-configs --install

# Use a specific profile's preset
wyvrnpm build --profile release

# Explicit preset name (overrides profile-derived default)
wyvrnpm build --preset wyvrn-release

# Build a single target
wyvrnpm build --target my-lib

# Wipe the build directory first
wyvrnpm build --clean

# Override the preset's generator (pair with --clean when switching generators)
wyvrnpm build --clean --generator "Visual Studio 17 2022"
wyvrnpm build --clean -G "Ninja Multi-Config"

# Build, then install to the CMake install prefix
wyvrnpm build --install
wyvrnpm build --install --install-prefix C:\out\stage

# Skip the auto-install step
wyvrnpm build --no-auto-install

# Pass extra args through to cmake --build
wyvrnpm build -- -j 8

Options

| Option | Default | Description | |---|---|---| | --preset / -P | wyvrn-<profile> | CMake configure preset name. | | --profile / -p | (config / "default") | Build profile — determines the default preset name. | | --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. | | --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. | | --target / -t | (all) | Specific CMake target to build. | | --verbose / -v | false | Pass --verbose to cmake --build. | | --clean | false | Remove the build directory before configuring. | | --generator / -G | (from preset) | Override the CMake generator used at configure time (e.g. "Visual Studio 17 2022", "Ninja", "Ninja Multi-Config"). CMake bakes the generator into the cache, so pair with --clean when switching. When the effective generator is Visual Studio, -A <platform> is auto-derived from the profile's arch (x86→Win32, x86_64→x64, armv8→ARM64, armv7→ARM) so VS doesn't silently default to x64 on 32-bit profiles. | | --install | false | Run cmake --install after a successful build. | | --install-prefix | (baked in) | Override CMAKE_INSTALL_PREFIX for --install without reconfiguring. | | --auto-install | true | Auto-run wyvrnpm install when the toolchain is stale. Disable with --no-auto-install. | | --source / -s | (config) | Package sources — passed through to install when auto-installing. | | --build-dir | build | Project-relative subdir for the build tree. Must match the value used at wyvrnpm install time — the baked preset is the source of truth for binaryDir. Persist via wyvrn.local.json:binaryDirRoot so install + build agree without repeating the flag. See Per-developer overlay. |

Everything after -- is forwarded verbatim to cmake --build (e.g. -j 8).


wyvrnpm test (2.18.0+)

Runs ctest against the wyvrnpm-generated test preset (wyvrn-<profile>-<config>). Auto-runs wyvrnpm build first if the toolchain or build outputs are stale — disable with --no-build for a pure ctest pass.

wyvrnpm test                                  # Release config, auto-builds if stale
wyvrnpm test --all-configs                    # Loop over every recipe config
wyvrnpm test --config Debug,Release           # Comma-separated config list
wyvrnpm test -R BasicSpatialGrid              # Filter by test-name regex
wyvrnpm test -L fast                          # Filter by label
wyvrnpm test --format json                    # JSON summary on stdout for CI
wyvrnpm test --no-build                       # Skip auto-build, just run ctest
wyvrnpm test -- --output-on-failure -j 8      # Passthru to ctest

| Flag | Default | Description | |---|---|---| | --preset / -P | wyvrn-<profile>-<config> | Override the test preset name | | --profile / -p | (config) | Build profile — selects the preset family | | --config / -c | Release | Single or comma-separated config list | | --all-configs | false | Loop over every config the recipe declares (or all four canonical configs) | | --verbose / -v | false | Forwards -V to ctest (per-test detail) | | --tests-regex / -R | — | Run only tests whose name matches the regex | | --label-regex / -L | — | Run only tests whose label matches the regex | | --auto-build | true | Auto-run wyvrnpm build when toolchain is stale. --no-build opts out. | | --source / -s | (config) | Forwarded to auto-build when triggered | | --build-dir | build | Must match the value used at install time. Persist via wyvrn.local.json:binaryDirRoot. | | --format | text | text (default) streams ctest verbatim; json emits a structured summary on stdout, logs to stderr |

Everything after -- is forwarded verbatim to ctest (e.g. --repeat until-pass:3, --output-junit results.xml).

--format json summary:

{
  "command":        "test",
  "wyvrnpmVersion": "2.18.0",
  "profile":        "default",
  "configs": [
    { "config": "Release", "preset": "wyvrn-default-release",
      "total": 42, "passed": 40, "failed": 2,
      "durationMs": 1234, "exitCode": 8 }
  ],
  "totalConfigs":  1,
  "passedConfigs": 0,
  "failedConfigs": 1,
  "ok": false
}

Exit code is 0 iff every config passes; 1 otherwise. CI scripts should gate on the ok field or the exit code, not on the text output.

Test-only deps pattern. For projects that need a test framework like Catch2 / GoogleTest only during the build, the canonical recipe is to declare an options.tests toggle and list the framework in buildDependencies with whenOption: "tests" (see §4.1 of CLAUDE.md). Consumers pulling the prebuilt artefact never see the test deps; running wyvrnpm install -o <Pkg>:tests=true && wyvrnpm test from the project root fetches them and runs the suite.


wyvrnpm publish

Zips the project directory and uploads wyvrn.json + wyvrn.zip to the destination source, tagged with the active build profile.

Source priority: CLI --source is used if provided (as a URL/URI or a configured source name). If omitted, the first configured publish source is used.

Profile: --profile selects the named build profile to publish under. If omitted, config.defaultProfile is used, falling back to "default". The profile hash is included in the upload path so multiple builds of the same version (different compilers, runtimes, etc.) can coexist.

# Use configured source and default profile
wyvrnpm publish

# Use a specific profile
wyvrnpm publish --profile release

# Reference a configured source by name
wyvrnpm publish --source default

# Explicit destination URL
wyvrnpm publish --source s3://my-bucket/packages --aws-profile my-sso

# HTTP server with token auth (CI-preferred: --token-env avoids argv leakage)
wyvrnpm publish --source https://pkg.corp.com --token-env WYVRN_HTTP_TOKEN
# Equivalent for interactive use; --token lands in shell history / ps output
wyvrnpm publish --source https://pkg.corp.com --token mytoken

# Local directory or SMB share
wyvrnpm publish --source \\fileserver\packages
wyvrnpm publish --source /mnt/packages

# Publish build artifacts from a specific directory
wyvrnpm publish --path ./dist

# Overwrite an existing build (same version + profile hash)
wyvrnpm publish --force

# Dry-run — show the upload plan without touching the registry
wyvrnpm publish --dry-run
wyvrnpm publish --dry-run --format json | jq '.wouldOverwrite, .plan'

Options

| Option | Default | Description | |---|---|---| | --source / -s | (config) | Destination URL/URI or configured source name. | | --profile / -p | (config / "default") | Named build profile to publish under. | | --aws-profile | (config) | AWS SSO profile for S3 authentication. | | --token | (config) | Bearer token for HTTP server authentication. | | --token-env | (config) | Name of an env var holding the bearer token. Preferred for CI — the literal value never lands in argv or shell history. | | --path | . | Directory to zip and publish. | | --manifest | ./wyvrn.json | Path to the manifest file. | | --force / -f | false | Overwrite an existing published version (same version + profile hash). | | --dry-run | false | Run every step (profile, options, zip, SHA256, v2Exists check) and print the upload plan, but skip the provider write. An existing published version is reported as a warning (not an error) so the full plan still prints. Pair with --format json to gate CI on wouldOverwrite. | | --no-sign | false | Publish unsigned even when defaultSigning is configured. Logs a warning. Useful for local testing — never recommended in CI. See ## Signing & verification. | | --signing-key-env | (config) | Override the env var holding the publisher private key for this run. Inherits keyId from defaultSigning / per-source signing. | | --signing-key-file | (config) | Override the file holding the publisher private key for this run. Inherits keyId from defaultSigning / per-source signing. |

During publish the tool also:

  • reads wyvrn.lock to pin locked dependency versions into the uploaded manifest
  • captures the current git commit SHA and remote URL as metadata
  • computes a SHA256 of the zip and stores it in the upload metadata
  • when signing is configured, signs an Ed25519 attestation over (name, version, profileHash, contentSha256, manifestSha256, publisherKeyId, publishedAt) and uploads wyvrn.att.json + wyvrn.sig alongside the zip. See ## Signing & verification.

.wyvrnignore

Place a .wyvrnignore file in the directory being published to exclude files from the zip. Uses the same glob syntax as .gitignore:

# Build artefacts that shouldn't ship
build/
*.obj
*.pdb

# Anchored to root with leading /
/CMakeCache.txt
/CMakeFiles/

wyvrnpm clean

Removes the wyvrn_internal/ package cache and wyvrn.lock.

# Project-local cleanup
wyvrnpm clean

# Also wipe the machine-wide source-build cache used by --build=missing
wyvrnpm clean --build-cache

Options

| Option | Default | Description | |---|---|---| | --build-cache | false | Also remove the source-build cache under %LOCALAPPDATA%\wyvrnpm\bc\ (Unix: ~/.cache/wyvrnpm/bc/). | | --uploaded-built | false | Wipe only the local record of what this machine has uploaded via --upload-built (the .uploaded-to.json sidecars). Does not touch the registry, the artefacts, or wyvrn_internal/. |

For selective cache pruning (keep last N variants, drop entries older than N days), use wyvrnpm cache prune instead — see below.


wyvrnpm cache (2.8.0+)

Inspect and selectively prune the machine-wide source-build cache at %LOCALAPPDATA%\wyvrnpm\bc\ (Unix: ~/.cache/wyvrnpm/bc/). Useful once --upload-built adoption balloons cache size on dev rigs — clean --build-cache is the blunt alternative that wipes everything.

# List every cached entry as a table
wyvrnpm cache list

# Filter by package name (glob)
wyvrnpm cache list "boost*"

# Machine-readable output for CI
wyvrnpm cache list --format json

# Keep the 2 most-recently-touched variants per (name, version)
wyvrnpm cache prune --keep-last 2

# Drop entries older than 30 days
wyvrnpm cache prune --older-than 30d

# Combine: keep 1 recent per version AND drop anything stale
wyvrnpm cache prune --keep-last 1 --older-than 30d

# Preview — print what would be removed, delete nothing
wyvrnpm cache prune --keep-last 2 --dry-run

cache list options

| Option | Default | Description | |---|---|---| | [pattern] / --pattern | (none) | Glob over package names (* for any chars, ? for one). | | --format | text | text (table) or json (structured payload for CI). |

cache prune options

| Option | Default | Description | |---|---|---| | --keep-last <N> | (none) | Retain the N most-recently-touched entries per (name, version) group. A four-profile rig keeping-last-2 still retains the two newest profile variants of each version. | | --older-than <dur> | (none) | Remove entries whose mtime is older than the threshold. Format: <number><unit>, unit ∈ s / m / h / d (e.g. 30d, 72h). | | --pattern <glob> | (none) | Restrict scope to package names matching the glob. | | --dry-run | false | Print what would be removed without deleting. |

At least one of --keep-last / --older-than is required — calling prune bare errors out with a pointer to clean --build-cache (the nuke-everything path).


wyvrnpm show

Read-only registry-metadata inspector. Given <name>[@<version>[@<profileHash>]], queries the first configured install source that knows the package (override with --source) and prints what's published.

# Index of every published version (one HTTP round trip per source)
wyvrnpm show zlib

# Narrow to one version — fans out a per-profile query so uploadedBy,
# compatibility, and declared options show up per build
wyvrnpm show [email protected]

# Narrow to one specific build
wyvrnpm show [email protected]@a1b2c3d4e5f60718

# Pick the source explicitly (skips install-source lookup order)
wyvrnpm show [email protected] --source s3://team-pkgs

# Machine-readable for CI ingestion
wyvrnpm show [email protected] --format json

What gets surfaced per profile:

  • originauthor-publish vs consumer-upload (derived from whether the manifest carries an uploadedBy block).
  • uploadedBy — when set, the consumer-upload provenance (kind, builtFromGit, builtFromSha, uploadedAt, wyvrnpmVersion).
  • compatibility — the recipe's declarative compat block, if any.
  • declaredOptions — the author-declared options map from the recipe.
  • resolvedOptions — the resolved option values that produced this profile's profileHash.
  • publishedConf — recipe + CLI --conf the publisher captured (informational; doesn't affect resolution).
  • signature — when PLAN-SIGNING phase 1 ships, signer identity; absent today.

Useful for: auditing consumer uploads (who pushed this artefact, from which commit), spotting profile drift between two consumers, verifying a package's declared options before adding it to a recipe.

Options

| Option | Default | Description | |---|---|---| | pkg (positional) | (required) | <name>[@<version>[@<profileHash>]] | | --source / -s | (first configured install source) | URL or configured source name. Repeatable. | | --format | text | text or json (see Shape — show below). | | --token / --token-env / --aws-profile | | Auth forwarded to the provider the same way install does. |


wyvrnpm install-skill

Installs the bundled wyvrnpm Agent Skill — the authoring guide Claude Code uses when it sees a wyvrn.json in the workspace. Ships as a .skill zip inside the npm package (claude/skills/wyvrnpm.skill) and is extracted into ~/.claude/skills/wyvrnpm/ on demand.

# One-shot: install the bundled skill for Claude Code
wyvrnpm install-skill --claude

# Overwrite an existing installation (e.g. after upgrading wyvrnpm)
wyvrnpm install-skill --claude --force

# Preview without writing
wyvrnpm install-skill --claude --dry-run

You only need this if you want Claude Code to pick up the skill. The CLI itself works without it.

Options

| Option | Default | Description | |---|---|---| | --claude | false | Install for Claude Code (extracts to ~/.claude/skills/wyvrnpm/ + writes the .skill zip alongside). Required — present so a future --cursor / --vscode target can slot in without CLI churn. | | --force | false | Overwrite an existing skill install. Without it, the command refuses to clobber. | | --dry-run | false | Print the destination paths and intended actions without writing. |


wyvrnpm link

Links a local package for development, similar to npm link. This allows you to test packages from local source directories without publishing to the registry.

Register a package globally

Run wyvrnpm link in a package directory to register it for linking:

cd C:\Dev\my-library
wyvrnpm link
# [wyvrn] Registered "my-library" -> C:\Dev\my-library

For CMake packages where the installable content is in a subdirectory:

cd C:\Dev\my-cmake-lib
wyvrnpm link --subdir output/install
# [wyvrn] Registered "my-cmake-lib" -> C:\Dev\my-cmake-lib\output\install

Link a package into your project

Link a globally registered package:

cd C:\Projects\my-app
wyvrnpm link my-library
# [wyvrn] Linked: my-library -> C:\Dev\my-library

Or link directly from a path (skip global registration):

wyvrnpm link my-library C:\Dev\my-library
wyvrnpm link my-library C:\Dev\my-library --subdir output/install

List registered packages

wyvrnpm link --list
# [wyvrn] Globally registered packages:
#   my-library -> C:\Dev\my-library
#   my-cmake-lib -> C:\Dev\my-cmake-lib\output\install [subdir: output/install]

Options

| Option | Description | |---|---| | --list | List all globally registered packages. | | --subdir | Subdirectory within the package to link (e.g., output/install for CMake packages). |

How it works:

  • Creates a symlink (Unix) or junction (Windows) in wyvrn_internal/
  • Junctions on Windows don't require admin rights
  • Linked packages are recorded in wyvrn.lock with a linked: prefix
  • wyvrnpm install automatically skips downloading linked packages

wyvrnpm unlink

Removes a linked package from the current project.

wyvrnpm unlink my-library
# [wyvrn] Unlinked: my-library

To unlink and re-download from the registry:

wyvrnpm unlink my-library --restore
# [wyvrn] Unlinked: my-library
# [wyvrn] Downloading: [email protected]

Options

| Option | Default | Description | |---|---|---| | --restore | false | Re-download the package from registry after unlinking. | | --source | (config) | Package source for --restore. | | --platform | win_x64 | Target platform for v1 fallback when using --restore. |


wyvrnpm configure

Manages the configuration file. All sub-commands read and write config.json in the platform config directory.

wyvrnpm configure list

Prints all configured sources.

Config: C:\Users\you\AppData\Local\wyvrnpm\config.json

Install Sources:
  corp-s3              s3://my-bucket/packages  [profile: my-sso]
  fallback             https://pkg.corp.com      [token: ***]

Publish Sources:
  default              s3://my-bucket/packages  [profile: my-sso]

wyvrnpm configure add-source

Adds a new source or updates an existing one with the same name.

# S3 with AWS SSO profile (install)
wyvrnpm configure add-source \
  --kind install \
  --name corp-s3 \
  --url s3://my-bucket/packages \
  --profile my-sso-profile

# HTTPS server with token auth (install)
wyvrnpm configure add-source \
  --kind install \
  --name fallback \
  --url https://pkg.corp.com \
  --token mytoken

# Local or SMB path (no auth required)
wyvrnpm configure add-source \
  --kind install \
  --name local \
  --url \\fileserver\packages

# Publish destination
wyvrnpm configure add-source \
  --kind publish \
  --name default \
  --url s3://my-bucket/packages \
  --profile my-sso-profile

Options

| Option | Required | Description | |---|---|---| | --kind | Yes | install or publish. | | --name | Yes | Unique name for this source. | | --url | Yes | Destination URL, URI, or file path. | | --profile | No | AWS SSO profile (S3 sources). | | --token | No | Bearer token (HTTP sources). Persisted in plaintext in config.json — prefer --token-env for CI and shared setups. | | --token-env | No | Name of an env var whose value is the bearer token. Only the name lands in config.json; the value is resolved fresh on every wyvrnpm call. |

wyvrnpm configure remove-source

Removes a source by name.

wyvrnpm configure remove-source --kind install --name fallback
wyvrnpm configure remove-source --kind publish --name default

wyvrnpm configure profile

Manages named build profiles stored in the profiles directory.

# Auto-detect the current build environment and save it
wyvrnpm configure profile detect
wyvrnpm configure profile detect --name msvc_release --dry-run

# List all saved profiles (* = default)
wyvrnpm configure profile list

# Show a saved profile
wyvrnpm configure profile show
wyvrnpm configure profile show --name msvc_release

# Manually set individual fields in a profile
wyvrnpm configure profile set --name msvc_release --runtime static --cppstd 20

# Change which profile is the default
wyvrnpm configure profile set-default msvc_release

# Delete a profile
wyvrnpm configure profile delete msvc_release

configure profile detect options

| Option | Description | |---|---| | --name / -n | Profile name to save to (default: "default"). | | --dry-run | Print detected values without saving. |

configure profile set options

| Option | Description | |---|---| | --name / -n | Profile name to update (default: "default"). | | --os | OS (Windows | Linux | Macos). | | --arch | Architecture (x86_64 | x86 | armv8 | ...). | | --compiler | Compiler (msvc | gcc | clang | apple-clang). | | --compiler-version | Compiler version (193 | 13 | 17 | ...). | | --cppstd | C++ standard (14 | 17 | 20 | 23). | | --runtime | Runtime linkage (static | dynamic). |

wyvrnpm configure signing (S1, 2.13.0+)

Manages the publisher signing context for wyvrnpm publish. Wyvrnpm never stores the private key — only the retrieval path (env-var name or file path).

# Persist the keyId + retrieval path. CI form:
wyvrnpm configure signing set-default --key-id razer-2026 --key-env WYVRNPM_SIGNING_KEY

# Local-dev form:
wyvrnpm configure signing set-default --key-id razer-2026 --key-file ~/.wyvrnpm/keys/razer-2026.priv.pem

wyvrnpm configure signing show
wyvrnpm configure signing unset

See Signing & verification for the full design.


wyvrnpm key (S1, 2.13.0+)

Generate / inspect publisher signing keys. Pure-Node Ed25519, no external tooling required.

# Generate a fresh keypair. Refuses to overwrite existing files.
wyvrnpm key gen --out ./keys --id razer-2026
# → ./keys/razer-2026.priv.pem (PKCS#8 PEM, mode 0600 on POSIX)
# → ./keys/razer-2026.pub.pem  (SPKI PEM, paste into trust anchor)
# → ./keys/razer-2026.pub.b64  (base64 SPKI DER, the literal value for keys.json)

# Re-derive the public key from a private key file (read-only sanity check).
wyvrnpm key show-pub --in ./keys/razer-2026.priv.pem

Pair with wyvrnpm configure signing set-default to persist the keyId + retrieval path locally, then add the printed keys.json block to the registry's .trust/keys.json to authorise the publisher.


wyvrnpm trust (S1, 2.13.0+)

Inspect or refresh the locally-cached per-registry trust anchor (.trust/keys.json + .trust/mode.json).

# Show what's cached locally (read-only — no fetch).
wyvrnpm trust list --source corp-s3

# Fetch fresh from the registry. Pure additions accepted silently;
# removals or modifications of an existing keyId fail closed unless
# --accept-breaking is passed (TOFU phase 1).
wyvrnpm trust refresh --source corp-s3
wyvrnpm trust refresh --source corp-s3 --accept-breaking

--source accepts a configured-source name or a literal URL.


JSON output (--format json)

install, publish, and show accept --format json to emit a single machine-readable JSON object on stdout. Log lines ([wyvrn] prefix) move to stderr in this mode, so stdout is reserved exclusively for the JSON payload — no scraping required.

wyvrnpm install --format json > install-result.json
wyvrnpm publish --format json 2>/dev/null           # swallow logs, keep JSON
wyvrnpm show [email protected] --format json | jq '.versions["1.3.0.0"].profiles[].origin'

Shape — install

{
  "command": "install",
  "wyvrnpmVersion": "2.2.1",
  "profile": { "os": "Windows", "arch": "x86_64", "compiler": "msvc", "compiler.version": "193", "compiler.cppstd": "23", "compiler.runtime": "dynamic" },
  "profileName": "default",
  "profileHash": "bf341c08af0aee2d",
  "packages": [
    {
      "name": "zlib",
      "version": "1.3.0.0",
      "versionRange": "^1.3.0.0",
      "profileHash": "a1b2c3d4e5f60718",
      "options": { "shared": false, "minizip": true },
      "contentSha256": "…",
      "gitSha": "…",
      "resolvedFrom": "v2"
    }
  ],
  "lockFile": "/abs/path/to/wyvrn.lock",
  "uploadSummary": { "uploaded": 0, "skipped": 0, "failed": 0 }
}
  • versionRange is null when the user pinned an exact version.
  • options is null for packages that declare no options.
  • uploadSummary is null unless --upload-built was passed.
  • packages is sorted by name.

Shape — publish

{
  "command": "publish",
  "wyvrnpmVersion": "2.2.1",
  "name": "zlib",
  "version": "1.3.0.0",
  "profileHash": "a1b2c3d4e5f60718",
  "source": "s3://team-pkgs",
  "contentSha256": "…",
  "options": { "shared": false, "minizip": true },
  "gitSha": "…",
  "gitRepo": "…",
  "publishedAt": "2026-04-21T12:34:56.789Z"
}

Shape — show

{
  "command": "show",
  "wyvrnpmVersion": "2.2.1",
  "source": "s3://team-pkgs",
  "package": "zlib",
  "latest": "1.3.0.0",
  "versions": {
    "1.3.0.0": {
      "source": { "gitRepo": "…", "gitSha": "…" },
      "profiles": [
        {
          "profileHash": "a1b2c3d4e5f60718",
          "contentSha256": "…",
          "publishedAt": "…",
          "buildSettings": { "os": "Windows", "...": "...", "options": { "minizip": true } },
          "origin": "author-publish",
          "uploadedBy": null,
          "compatibility": { "compiler.cppstd": "lte" },
          "declaredOptions": { "minizip": { "default": true, "allowed": [true, false] } }
        }
      ]
    }
  }
}
  • origin is "author-publish" | "consumer-source-build" | null (the last when the summary view can't tell without a deep fetch).
  • Bare-name queries (wyvrnpm show zlib --format json) skip the per-profile deep fetch; origin, uploadedBy, compatibility, declaredOptions are null for each profile.

Exit codes and error behaviour are identical to text mode. Fatal errors leave stdout empty rather than emitting partial JSON.


Configuration File

wyvrnpm stores persistent configuration in a config.json file:

| Platform | Path | |---|---| | Windows | %LOCALAPPDATA%\wyvrnpm\config.json | | Linux / macOS | ~/.config/wyvrnpm/config.json |

Example config.json

{
  "defaultProfile": "default",
  "installSources": [
    {
      "name": "corp-s3",
      "url": "s3://my-bucket/packages",
      "profile": "my-sso-profile"
    },
    {
      "name": "fallback",
      "url": "https://pkg.corp.com",
      "token": "mytoken"
    }
  ],
  "publishSources": [
    {
      "name": "default",
      "url": "s3://my-bucket/packages",
      "profile": "my-sso-profile"
    }
  ],
  "linkedPackages": {
    "my-library": { "path": "C:\\Dev\\my-library" },
    "my-cmake-lib": { "path": "C:\\Dev\\my-cmake-lib", "subdir": "output/install" }
  }
}

The defaultProfile field sets which named build profile is used when --profile is not provided on the CLI.

The linkedPackages field stores globally registered packages for wyvrnpm link. Each entry maps a package name to its local path and optional subdirectory.

CLI options always take priority over config. Config values are only used when the corresponding CLI option is not provided.


Signing & verification (S1, 2.13.0+)

wyvrnpm signs published artefacts with Ed25519 detached signatures and verifies them at install time against a per-registry trust anchor. The signing layer is pure-Node (crypto.sign / crypto.verify from the built-in module) — no external tooling, no shell-outs, identical behaviour on Windows / Linux / macOS.

Why this matters. Without signing, contentSha256 proves transit integrity against itself — a stolen S3 credential can republish a trojanised zip and update the SHA in versions.json to match. Signing pins the publisher's identity to a registry-side trust anchor (.trust/keys.json) that an attacker with bucket-write access cannot rewrite without also holding the publisher's private key.

Threat model

| Protects against | Does not protect against | |---|---| | Stolen registry credentials (S3 / HTTP token) | A trusted signer publishing maliciously | | Mirror impersonation / MitM | Compromise of the publisher's signing key (use rotation) | | Consumer-upload masquerade (--upload-built attribution) | First-contact TOFU bootstrapping (phase 1; closed by phase 2 root-pinning) | | URL-swap attacks (a sig moved between profiles) | Malicious code that's correctly signed by an authorised publisher |

Quick start (publisher)

# 1. Generate an Ed25519 keypair
wyvrnpm key gen --out ./keys --id razer-2026

# 2. Persist the retrieval path. Wyvrnpm never stores the private key
#    itself — only the env-var name (preferred for CI) or the file path.
export WYVRNPM_SIGNING_KEY=$(cat ./keys/razer-2026.priv.pem)
wyvrnpm configure signing set-default --key-id razer-2026 --key-env WYVRNPM_SIGNING_KEY

# 3. Add the public half to the registry's trust anchor — exactly once,
#    out-of-band. The block printed by `key gen` is the literal value to
#    drop into {source}/v2/.trust/keys.json. Set mode to "warn" while
#    rolling out signing across publishers; flip to "require" once every
#    publisher is on board.

# 4. Publish — signing happens automatically when defaultSigning is set.
wyvrnpm publish --source corp-s3
# → signs wyvrn.att.json + wyvrn.sig and uploads them alongside the zip

Quick start (consumer)

Consumers don't have to do anything when the registry is in mode: warn — installs proceed and signed artefacts log signed by <keyId> (<identityHint>). To check what's cached locally:

wyvrnpm trust list  --source corp-s3
wyvrnpm trust refresh --source corp-s3       # pull latest .trust/keys.json

To force strict verification regardless of registry mode (e.g., hardened CI):

wyvrnpm install --require-signatures

Modes ({source}/v2/.trust/mode.json)

| mode | missing sig | invalid sig | |---|---|---| | off (default — no .trust/ dir) | accept silently | n/a | | warn (phase-1 default) | accept with one-line warning | abort (bad sig is stricter than missing) | | require (phase 2+) | abort | abort |

Trust-anchor rotation (TOFU)

Phase 1 uses trust-on-first-use — the first wyvrn install against a registry caches keys.json locally at %LOCALAPPDATA%\wyvrnpm\trust\<source-hash>\. Subsequent fetches diff against the cache:

  • Pure additions → accepted silently (the common rotation event: bring on a new publisher).
  • Removals or modifications of an existing key → fail closed; install aborts and prompts for wyvrnpm trust refresh --accept-breaking.

Phase 2 will allow pinning a root key in installSources[].trustRootPub so the registry's keys.json itself carries a root signature — closes the TOFU window.

What the attestation binds

Each signed artefact gets a wyvrn.att.json body:

{
  "wyvrnAttestationVersion": 1,
  "name":           "demo",
  "version":        "1.0.0.0",
  "profileHash":    "a1b2c3d4e5f60718",
  "contentSha256":  "<sha256 of wyvrn.zip>",
  "manifestSha256": "<sha256 of canonical wyvrn.json>",
  "publishedAt":    "2026-04-29T12:34:56.789Z",
  "publisherKeyId": "razer-2026",
  "uploadedBy":     null
}

Plus a sidecar wyvrn.sig:

{ "alg": "ed25519", "keyId": "razer-2026", "sig": "<base64 64-byte signature>" }

The verifier checks the cryptographic signature, then asserts (name, version, profileHash, contentSha256, manifestSha256) match what the consumer downloaded. Dropping a valid attestation for pkg-A into pkg-B's URL fails the bound-identity check; swapping the zip after publish fails the bound-contentSha256 check; rewriting the manifest fails the bound-manifestSha256 check.

Backward compatibility

A registry without a .trust/ directory behaves as mode: off — pre-S1 clients ignore the new files entirely, S1 clients install identically to today. Pre-S1 buckets need zero migration to keep working.


Publish Providers

wyvrnpm uses a provider architecture to support different destination types. The correct provider is selected automatically based on the source URL format.

| Provider | Detected from | Auth | |---|---|---| | S3 | s3:// URI, or S3 HTTPS endpoint | --aws-profile (AWS SSO) | | HTTP | http:// or https:// | --token (Bearer) | | File | Local path or UNC/SMB share | none |

S3 Provider

Accepts any of these source formats:

s3://bucket-name/optional/prefix
https://bucket-name.s3.amazonaws.com/prefix
https://bucket-name.s3.us-east-1.amazonaws.com/prefix
https://s3.amazonaws.com/bucket-name/prefix
https://s3.us-east-1.amazonaws.com/bucket-name/prefix

Authentication uses AWS SSO via --aws-profile. If no profile is given, the default AWS credential chain is used.

HTTP Provider

Accepts http:// and https:// URLs. Uploads files via HTTP PUT. Optionally authenticates with a Bearer token via --token.

File Provider

Accepts local directory paths and SMB/UNC network shares. Files are copied with no authentication required.

/absolute/unix/path
./relative/path
C:\Windows\path
\\server\share\path
//server/share/path
file:///absolute/path

Adding a Custom Provider

  1. Create src/providers/myprovider.js extending BaseProvider:
'use strict';
const BaseProvider = require('./base');

class MyProvider extends BaseProvider {
  static canHandle(source) {
    return source.startsWith('myscheme://');
  }

  static get providerName() { return 'MyProvider'; }

  async v2Publish(files, options) {
    // files.manifest — path to wyvrn.json
    // files.zip      — path to wyvrn.zip
    // options.source, options.name, options.version, options.profileHash
    // options.contentSha256, options.gitSha, options.gitRepo
    // options.buildSettings, options.lockedDependencies
  }

  async v2Exists(options) {
    // Return true if this version+profileHash already exists
    return false;
  }
}

module.exports = MyProvider;
  1. Register it in src/providers/index.js:
const MyProvider = require('./myprovider');

const PROVIDERS = [
  S3Provider,
  HttpProvider,
  FileProvider,
  MyProvider,   // add here
];

Registry Layout

v2 layout (current)

Packages published by v2 tooling are stored under a v2/ prefix, with the profile hash as a sub-directory:

{source}/v2/{name}/{version}/{profileHash}/wyvrn.json   <- package manifest + metadata
{source}/v2/{name}/{version}/{profileHash}/wyvrn.zip    <- package archive
{source}/v2/{name}/versions.json                        <- version index
{source}/v2/{name}/latest.json                          <- latest version pointer

The profile hash is a 16-character SHA256 prefix computed from the build profile fields. This allows multiple compiler/platform variants of the same version to coexist in the same registry.

Example (S3):

s3://my-bucket/packages/v2/OpenSSL/3.0.0.0/a1b2c3d4e5f60718/wyvrn.json
s3://my-bucket/packages/v2/OpenSSL/3.0.0.0/a1b2c3d4e5f60718/wyvrn.zip

v1 layout (legacy)

v2 tooling also dual-publishes to the v1 path so that older clients can still consume packages:

{source}/{platform}/{name}/{version}/wyvrn.json
{source}/{platform}/{name}/{version}/wyvrn.zip

Example (HTTP):

https://pkg.corp.com/win_x64/OpenSSL/3.0.0.0/wyvrn.json
https://pkg.corp.com/win_x64/OpenSSL/3.0.0.0/wyvrn.zip

The tool also falls back to razer.json / razer.zip for legacy compatibility.

Package manifest (wyvrn.json hosted on source)

{
  "Name": "OpenSSL",
  "Version": "3.0.0.0",
  "Description": "TLS/SSL toolkit",
  "Dependencies": [
    { "Name": "zlib", "Version": "1.3.0.0" }
  ]
}

Fields use PascalCase. Dependencies is an array — transitive dependencies are resolved automatically.


Project Manifest (wyvrn.json)

{
  "name": "my-app",
  "version": "1.0.0.0",
  "description": "My C++ application",
  "kind": "ConsoleApp",
  "dependencies": {
    "OpenSSL": "3.0.0.0",
    "zlib": {
      "version": "1.3.0.0",
      "settings": {
        "compiler.runtime": "static"
      }
    }
  }
}

kind values: ConsoleApp, StaticLib, DynamicLib, HeaderOnlyLib, WebApp, Utility, Service, TestProject

dependencies can be either a plain version string ("major.minor.patch.build") or an object with version, an optional settings map, and an optional options map. The settings map overrides individual build profile fields for that specific dependency — useful when a dependency must be consumed with a different runtime linkage or C++ standard than your default profile. The options map is covered in the next section.

Version ranges

The version field accepts ranges in addition to exact versions and the "latest" tag:

| Spec | Meaning | |---|---| | "1.2.3.4" | exact — must match byte-identically | | "latest" | resolves to whatever latest.json points at | | "^1.2.3.4" | major-pinned — >= 1.2.3.4 and < 2.0.0.0 | | "~1.2.3.4" | minor-pinned — >= 1.2.3.4 and < 1.3.0.0 | | ">=1.2.3.4 <2.0.0.0" | explicit comparators — space-separated, supports >=, >, <=, <, = |

Ranges resolve at install time against each source's versions.json. wyvrnpm picks the highest version that satisfies the range. The resolved exact version is pinned into wyvrn.lock alongside the original range string, so re-installs are byte-reproducible — ranges are only re-evaluated when the lock file's entry for that dependency is removed.

Error handling:

  • No version satisfies the range → strict failure with every published version listed so you can see what's available.
  • Conflicting ranges across the graph (direct consumer wants ^1.x but a transitive wants ^2.x) → precise error naming both contributors and their ranges.
  • Malformed range string → fail fast with a cheat-sheet of supported syntaxes.

Example:

{
  "dependencies": {
    "OpenSSL": "^3.0.0.0",
    "zlib":    "~1.3.0.0",
    "boost":   ">=1.80.0.0 <2.0.0.0"
  }
}

Per-dependency source pin (2.8.0+)

When you have more than one configured install source (e.g. a primary S3 bucket plus a mirror), you can pin a security-sensitive dep to its canonical source by name:

{
  "dependencies": {
    "OpenSSL": {
      "version": "3.0.0.0",
      "source":  "primary-s3"
    }
  }
}

"primary-s3" must match a name from wyvrnpm configure list; an unknown name fails install with exit code 1 rather than silently fanning out. The pin closes two common multi-source holes:

  • Mirror shadowing. A package accidentally published to the mirror can't substitute for the primary's copy.
  • Dependency confusion. A squatter on a secondary source can't poison the build.

Pins are direct-only — they don't cascade to the pinned package's own transitive deps. CLI --source overrides config entirely, so on a run with --source, any manifest pin is ignored with a warning rather than an error. The lockfile records source: "<name>" on pinned entries so reviewers can spot at a glance that the artefact came from the declared source; absent on non-pinned