wyvrnpm 2.13.11 → 2.14.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.
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.13.0** — **New:** 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. Highlights since 2.8.3: profile's `compiler.cppstd` is now authoritative for the toolchain — `CMAKE_CXX_STANDARD` is set unconditionally before `project()` and `cpp.cmake` no longer touches it post-`project()`, so a `-DCMAKE_CXX_STANDARD=23` on the cmake CLI cannot defeat a `cppstd=20` profile; `wyvrnpm install` exits non-zero on partial failure (sha256-mismatch, extract-failed, no-exact-match, source-build-failed, not-found) and prints 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` to sidestep `build/` case-insensitive-FS collisions on repos with a Bazel/Buck `BUILD` file at the root (gRPC, …); `wyvrnpm version` to read / set / partially update the top-level `version` field from CI pipelines (`wyvrnpm version --build "$BUILD_ID"`); `wyvrnpm bootstrap <git-url>` to 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, plus `wouldOverwrite` flag in `--format json`); `wyvrnpm build --config Debug,Release` / `--all-configs` to build multiple configurations from a single configure; resolver hardened against the CloudFront brotli-vs-identity cache split (resolved a class of bugs where `show` listed a freshly-published version that `install` claimed didn't exist). Full list in `claude/EVALUATION.md`.
7
+ > **README version: 2.14.0** — **New:** npm self-update notifier — when a newer `wyvrnpm` is on the npm registry, the next interactive run prints a one-line `[wyvrn] warn wyvrnpm <old> → <new> available` banner on stderr. The fetch happens in a detached background process so the user's command never blocks on the network; the banner appears on the *following* invocation after the cache (`%LOCALAPPDATA%\wyvrnpm\update-check.json`) is populated. Auto-suppressed in CI (`$CI`), non-TTY runs, and `--format json` mode; opt out via `WYVRNPM_NO_UPDATE_CHECK=1` or `--no-update-check`. Hand-rolled (~120 lines, zero new deps) instead of pulling in `update-notifier` and its 10+ transitives. Closes the gap where users on stale CLIs silently missed bug fixes (the `2.8.4` CloudFront cache-split fix and the `2.12.2` install-failure exit code, in particular). **Resolver caches `@latest` lookups** within a single `wyvrnpm install` — when a package is referenced as `@latest` from multiple parents in the dep graph (or already resolved earlier via a range), the registry round-trip is reused instead of re-probing `latest.json` per encounter. Eliminates ~12 redundant HTTPS round-trips on a typical 16-dep graph; visible win on resolution wall-time when the registry sits behind CloudFront. Closes the parallel gap to `manifestCache` / `publishedCache` — all three S3-backed helpers in `resolve.js` now memoise per-name within one install. **Per-config artefact slicing.** Recipes opt in via `build.publishPerConfig: true`; `wyvrnpm publish` then emits one `wyvrn-<Config>.zip` per declared CMake config under one `profileHash` URL instead of a single fat zip. Consumers fetch only the configs they need: `wyvrnpm install --request-configs Release` on a CI runner 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 (`IMPORTED_LOCATION_<CONFIG>`) so both the MSVC convention (`lib/<Config>/foo.lib`) and the postfix convention (`lib/foo.lib` Release vs `lib/food.lib` Debug, via `CMAKE_DEBUG_POSTFIX`) classify correctly. `wyvrn.local.json` gains `requestConfigs` (global) + `depRequestConfigs` (per-dep override — "all four configs of fmt, but MinSizeRel of grpc"). Attestation v2 binds an `artefactSha256: { config → sha }` map; one signature covers every slice. `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 (the existence check degrades to `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. Profile's `compiler.cppstd` is now authoritative for the toolchain — `CMAKE_CXX_STANDARD` is set unconditionally before `project()` and `cpp.cmake` no longer touches it post-`project()`, so a `-DCMAKE_CXX_STANDARD=23` on the cmake CLI cannot defeat a `cppstd=20` profile; `wyvrnpm install` exits non-zero on partial failure (sha256-mismatch, extract-failed, no-exact-match, source-build-failed, not-found) and prints 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` to sidestep `build/` case-insensitive-FS collisions on repos with a Bazel/Buck `BUILD` file at the root (gRPC, …); `wyvrnpm version` to read / set / partially update the top-level `version` field from CI pipelines (`wyvrnpm version --build "$BUILD_ID"`); `wyvrnpm bootstrap <git-url>` to 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, plus `wouldOverwrite` flag in `--format json`); `wyvrnpm build --config Debug,Release` / `--all-configs` to build multiple configurations from a single configure; resolver hardened against the CloudFront brotli-vs-identity cache split (resolved a class of bugs where `show` listed a freshly-published version that `install` claimed didn't exist). Full list in `claude/EVALUATION.md`.
8
8
 
9
9
  ---
10
10
 
@@ -22,6 +22,16 @@ For S3 publishing, also install the AWS SDK:
22
22
  npm install -g @aws-sdk/client-s3 @aws-sdk/credential-providers
23
23
  ```
24
24
 
25
+ ### Update notifier
26
+
27
+ When a newer `wyvrnpm` is published to npm, the next interactive run prints a single-line banner on stderr:
28
+
29
+ ```
30
+ [wyvrn] warn wyvrnpm 2.13.12 → 2.14.0 available. Run `npm i -g wyvrnpm` to update.
31
+ ```
32
+
33
+ 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`.
34
+
25
35
  ---
26
36
 
27
37
  ## Quick Start
package/bin/wyvrnpm.js CHANGED
@@ -1,7 +1,11 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  'use strict';
4
4
 
5
+ // npm self-update notifier — synchronous, non-blocking, suppressed in CI /
6
+ // non-TTY / JSON mode. Runs before yargs so it gets first crack at argv.
7
+ require('../src/update-check').maybeNotify();
8
+
5
9
  const yargs = require('yargs');
6
10
  const init = require('../src/commands/init');
7
11
  const add = require('../src/commands/add');
@@ -192,28 +192,34 @@ function(SETUP_LIBRARY target type)
192
192
 
193
193
  endfunction()
194
194
 
195
- # ── SETUP_EXE(target [EXCLUDE pat...] [extra_sources...]) ─────────────────────
195
+ # ── SETUP_EXE(target [CONSOLE] [EXCLUDE pat...] [extra_sources...]) ───────────
196
196
  # Creates an executable target from all sources in CMAKE_CURRENT_SOURCE_DIR,
197
197
  # with platform-specific HAL sources included automatically.
198
- # On Windows and Xbox the WIN32 subsystem flag is set automatically.
198
+ # On Windows and Xbox the WIN32 subsystem flag is set automatically; pass
199
+ # CONSOLE to opt out and produce a console-subsystem binary instead.
199
200
  #
200
201
  # Arguments:
201
202
  # target CMake target name
203
+ # CONSOLE (optional flag) Skip the WIN32 subsystem flag on Windows/Xbox
204
+ # so the binary is built as a console application. Equivalent
205
+ # to passing /SUBSYSTEM:CONSOLE under MSVC by hand. No-op
206
+ # outside Windows/Xbox.
202
207
  # EXCLUDE (optional) One or more regex patterns; matched files are
203
208
  # excluded from the auto-collected source list
204
209
  # extra_sources (optional) Additional source files to append (positional,
205
210
  # or via the SOURCES keyword)
206
211
  #
207
212
  # Example:
208
- # SETUP_EXE(ChromaApp)
213
+ # SETUP_EXE(ChromaApp) # GUI subsystem on Windows
214
+ # SETUP_EXE(ChromaTool CONSOLE) # console subsystem on Windows
209
215
  # SETUP_EXE(ChromaTool ${TOOL_GENERATED_SOURCES})
210
216
  # SETUP_EXE(ChromaTool EXCLUDE ".*test_stub.*" SOURCES ${EXTRA})
211
217
  function(SETUP_EXE target)
212
- cmake_parse_arguments(_SETUP "" "" "EXCLUDE;SOURCES" ${ARGN})
218
+ cmake_parse_arguments(_SETUP "CONSOLE" "" "EXCLUDE;SOURCES" ${ARGN})
213
219
  _wyvrn_collect_sources(_sources ${_SETUP_EXCLUDE})
214
220
  list(APPEND _sources ${_SETUP_SOURCES} ${_SETUP_UNPARSED_ARGUMENTS})
215
221
 
216
- if(WYVRN_PLATFORM_WINDOWS OR WYVRN_PLATFORM_XBOX)
222
+ if((WYVRN_PLATFORM_WINDOWS OR WYVRN_PLATFORM_XBOX) AND NOT _SETUP_CONSOLE)
217
223
  add_executable(${target} WIN32 ${_sources})
218
224
  else()
219
225
  add_executable(${target} ${_sources})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyvrnpm",
3
- "version": "2.13.11",
3
+ "version": "2.14.1",
4
4
  "description": "A simple, static-hosting-compatible C++ package manager",
5
5
  "keywords": [
6
6
  "c++",
package/src/config.js CHANGED
@@ -23,6 +23,11 @@ function getConfigPath() {
23
23
  return path.join(getConfigDir(), 'config.json');
24
24
  }
25
25
 
26
+ /** Full path to update-check.json (npm self-update notifier cache). */
27
+ function getUpdateCheckPath() {
28
+ return path.join(getConfigDir(), 'update-check.json');
29
+ }
30
+
26
31
  /**
27
32
  * Read and parse config.json.
28
33
  * Returns a default empty config if the file doesn't exist yet.
@@ -76,7 +81,7 @@ function writeConfig(config) {
76
81
  fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + '\n', 'utf8');
77
82
  }
78
83
 
79
- module.exports = { getConfigDir, getConfigPath, readConfig, writeConfig };
84
+ module.exports = { getConfigDir, getConfigPath, getUpdateCheckPath, readConfig, writeConfig };
80
85
 
81
86
  /**
82
87
  * @typedef {{ name: string, url: string, profile?: string, token?: string, signing?: SigningConfig, trustRootPub?: string }} SourceEntry
package/src/resolve.js CHANGED
@@ -298,6 +298,19 @@ async function resolveDependencies(
298
298
  return list;
299
299
  };
300
300
 
301
+ /**
302
+ * Cache `@latest` resolution per package — one round trip per name
303
+ * regardless of how often it appears in the graph as `"latest"`.
304
+ * @type {Map<string, string>}
305
+ */
306
+ const latestCache = new Map();
307
+ const fetchLatest = async (name) => {
308
+ if (latestCache.has(name)) return latestCache.get(name);
309
+ const v = await resolveLatestVersion(name, sourcesFor(name), platform, httpClient);
310
+ latestCache.set(name, v);
311
+ return v;
312
+ };
313
+
301
314
  /**
302
315
  * Pending work queue: each entry is { name, version, by? }.
303
316
  * `by` is the contributor label for conflict diagnostics.
@@ -319,8 +332,9 @@ async function resolveDependencies(
319
332
  // Skip this for linked packages which have "linked:" prefix.
320
333
  if (version === 'latest') {
321
334
  try {
322
- version = await resolveLatestVersion(name, sourcesFor(name), platform, httpClient);
323
- log.info(`Resolved "${name}@latest" ${version}`);
335
+ const cached = latestCache.has(name);
336
+ version = await fetchLatest(name);
337
+ if (!cached) log.info(`Resolved "${name}@latest" → ${version}`);
324
338
  } catch (err) {
325
339
  log.warn(err.message);
326
340
  continue;
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ // Detached background fetcher for the npm self-update notifier.
6
+ //
7
+ // Spawned by src/update-check/index.js when the update-check.json cache is
8
+ // stale or missing. Runs independently of the parent CLI process — the parent
9
+ // returns from `maybeNotify()` immediately; this script's exit timing is
10
+ // irrelevant to the user. Banner prints on the *next* invocation, after the
11
+ // cache file has been populated.
12
+ //
13
+ // Hard rules:
14
+ // - swallow every error; never exit non-zero (parent ignores us anyway, but
15
+ // a non-zero exit could surface in `ps` / job-control noise)
16
+ // - 10 s timeout — slow networks must not leave a zombie node process
17
+ // - atomic write (tmp + rename) so a half-written file never trips the
18
+ // parent's JSON.parse
19
+ //
20
+ // Test hooks (undocumented, internal-only):
21
+ // WYVRNPM_UPDATE_CHECK_URL — override the registry URL
22
+ // WYVRNPM_UPDATE_CHECK_PATH — override the cache file path
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ const { getUpdateCheckPath } = require('../config');
28
+ const { wyvrnFetch } = require('../http-fetch');
29
+
30
+ const DEFAULT_URL = 'https://registry.npmjs.org/wyvrnpm/latest';
31
+ const TIMEOUT_MS = 10_000;
32
+
33
+ async function main() {
34
+ const url = process.env.WYVRNPM_UPDATE_CHECK_URL || DEFAULT_URL;
35
+ const cachePath = process.env.WYVRNPM_UPDATE_CHECK_PATH || getUpdateCheckPath();
36
+
37
+ const installed = require('../../package.json').version;
38
+
39
+ const ac = new AbortController();
40
+ const timeout = setTimeout(() => ac.abort(), TIMEOUT_MS);
41
+ let body;
42
+ try {
43
+ const res = await wyvrnFetch(url, { signal: ac.signal });
44
+ if (!res.ok) return;
45
+ body = await res.json();
46
+ } finally {
47
+ clearTimeout(timeout);
48
+ }
49
+
50
+ const latestVersion = body && typeof body.version === 'string' ? body.version : null;
51
+ if (!latestVersion) return;
52
+
53
+ const payload = {
54
+ lastCheckedAt: Date.now(),
55
+ latestVersion,
56
+ installedAtCheck: installed,
57
+ };
58
+
59
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
60
+ const tmp = `${cachePath}.${process.pid}.tmp`;
61
+ fs.writeFileSync(tmp, JSON.stringify(payload, null, 2) + '\n', 'utf8');
62
+ fs.renameSync(tmp, cachePath);
63
+ }
64
+
65
+ // Swallow everything — a background fetch failure must never surface.
66
+ main().catch(() => {}).finally(() => process.exit(0));
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ // Lightweight npm self-update notifier for the wyvrnpm CLI.
4
+ //
5
+ // `maybeNotify()` is called once at the top of bin/wyvrnpm.js, before yargs
6
+ // parses argv. It runs synchronously and returns immediately — never blocks
7
+ // the user's command on a network call. Two paths:
8
+ //
9
+ // - cache fresh + newer version on npm → print one-line warning banner
10
+ // - cache stale or missing → spawn detached child to refresh,
11
+ // banner appears on the *next*
12
+ // invocation
13
+ //
14
+ // Suppression covers CI, non-TTY, --format=json, and explicit opt-out.
15
+ // Background failures (no network, npm rate-limit, corrupt cache) are
16
+ // silent — see fetcher.js. Hand-rolled to avoid the 10+ transitive deps
17
+ // the npm `update-notifier` package brings.
18
+ //
19
+ // Cache schema (under %LOCALAPPDATA%\wyvrnpm\update-check.json):
20
+ // { lastCheckedAt: <ms>, latestVersion: "<x.y.z>", installedAtCheck: "<x.y.z>" }
21
+ //
22
+ // `installedAtCheck` invalidates the cache when the user has manually
23
+ // upgraded since the last check — without it, a stale cache could keep
24
+ // nagging about a version the user already has.
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const childProc = require('child_process');
29
+
30
+ const log = require('../logger');
31
+ const { getUpdateCheckPath } = require('../config');
32
+ const { compareVersions } = require('../version-range');
33
+
34
+ const TTL_MS = 24 * 60 * 60 * 1000;
35
+ const FETCHER_SCRIPT = path.join(__dirname, 'fetcher.js');
36
+ const PKG_VERSION = require('../../package.json').version;
37
+
38
+ /**
39
+ * Decide whether to run the update check at all. Returns false in any
40
+ * non-interactive context — CI, redirected stdout, JSON output mode, or
41
+ * when the user has explicitly opted out.
42
+ */
43
+ function shouldRunUpdateCheck(env = process.env, argv = process.argv, stdout = process.stdout) {
44
+ if (env.WYVRNPM_NO_UPDATE_CHECK) return false;
45
+ if (env.CI) return false;
46
+ if (!stdout || !stdout.isTTY) return false;
47
+
48
+ if (argv.includes('--no-update-check')) return false;
49
+
50
+ // `--format json` (space-separated) and `--format=json` (equals) — any
51
+ // subcommand running in JSON mode would only inject stderr noise into
52
+ // CI scripts that capture stdout.
53
+ for (let i = 0; i < argv.length; i++) {
54
+ const a = argv[i];
55
+ if (a === '--format=json') return false;
56
+ if (a === '--format' && argv[i + 1] === 'json') return false;
57
+ }
58
+
59
+ // Don't recurse — the detached fetcher must not re-trigger maybeNotify.
60
+ if (argv[1] && argv[1].endsWith(path.join('update-check', 'fetcher.js'))) {
61
+ return false;
62
+ }
63
+
64
+ return true;
65
+ }
66
+
67
+ function readCache(cachePath) {
68
+ try {
69
+ const raw = fs.readFileSync(cachePath, 'utf8');
70
+ const parsed = JSON.parse(raw);
71
+ if (typeof parsed.lastCheckedAt !== 'number') return null;
72
+ if (typeof parsed.latestVersion !== 'string') return null;
73
+ if (typeof parsed.installedAtCheck !== 'string') return null;
74
+ return parsed;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function spawnFetcher() {
81
+ try {
82
+ const child = childProc.spawn(
83
+ process.execPath,
84
+ [FETCHER_SCRIPT],
85
+ { detached: true, stdio: 'ignore', windowsHide: true },
86
+ );
87
+ child.unref();
88
+ } catch {
89
+ // Spawn failures (sandbox, exec policy) — silent. Worst case the user
90
+ // never sees the banner; the CLI itself keeps working.
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Print the banner via log.warn — single line on stderr, [wyvrn] prefix
96
+ * applied by the logger. Stays out of stdout so `VER=$(wyvrnpm version)`
97
+ * stays clean.
98
+ */
99
+ function emitBanner(installed, latest) {
100
+ log.warn(`wyvrnpm ${installed} → ${latest} available. Run \`npm i -g wyvrnpm\` to update.`);
101
+ }
102
+
103
+ /**
104
+ * Main entry. Sync work (gate + cache read + banner) returns synchronously;
105
+ * background fetch runs in a detached child. Total foreground cost: one
106
+ * fs.readFileSync of a tiny JSON file in the happy path.
107
+ */
108
+ function maybeNotify(opts = {}) {
109
+ const env = opts.env || process.env;
110
+ const argv = opts.argv || process.argv;
111
+ const stdout = opts.stdout || process.stdout;
112
+ const cachePath = opts.cachePath || getUpdateCheckPath();
113
+ const installed = opts.installedVersion || PKG_VERSION;
114
+ const now = opts.now || Date.now();
115
+ const spawn = opts.spawn || spawnFetcher;
116
+ const banner = opts.banner || emitBanner;
117
+
118
+ if (!shouldRunUpdateCheck(env, argv, stdout)) return;
119
+
120
+ const cache = readCache(cachePath);
121
+
122
+ // Cache invalid if installedAtCheck disagrees with the live version
123
+ // (user upgraded since the last check) — treat as stale.
124
+ if (!cache || cache.installedAtCheck !== installed) {
125
+ spawn();
126
+ return;
127
+ }
128
+
129
+ if (now - cache.lastCheckedAt >= TTL_MS) {
130
+ spawn();
131
+ // Fall through — we may still have a useful latestVersion from the
132
+ // last fetch even though it's "stale" by TTL. Print the banner if
133
+ // applicable; the background fetch will refresh for next time.
134
+ }
135
+
136
+ if (compareVersions(cache.latestVersion, installed) > 0) {
137
+ banner(installed, cache.latestVersion);
138
+ }
139
+ }
140
+
141
+ module.exports = {
142
+ maybeNotify,
143
+ shouldRunUpdateCheck,
144
+ readCache,
145
+ };