wyvrnpm 2.10.0 → 2.11.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
@@ -235,6 +235,51 @@ wyvrnpm add zlib fmt@10.2.0.0 spdlog
235
235
 
236
236
  ---
237
237
 
238
+ ### `wyvrnpm version`
239
+
240
+ 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).
241
+
242
+ - **Bare** — prints the current version on stdout, naked (no `[wyvrn]` prefix), so `VER=$(wyvrnpm version)` works straight away.
243
+ - **Full set** — `wyvrnpm 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.
244
+ - **Component flags** — `--major / --minor / --patch / --build N` replace one component while leaving the others alone. Combine flags freely (`--minor 3 --build 0`).
245
+ - Positional and component flags are mutually exclusive — pick a mode.
246
+
247
+ ```bash
248
+ # Read the current version (CI capture)
249
+ VER=$(wyvrnpm version)
250
+
251
+ # Inject the build number from a pipeline variable
252
+ wyvrnpm version --build "$BUILD_ID"
253
+
254
+ # Cut a minor release — reset the build component to 0
255
+ wyvrnpm version --minor 3 --build 0
256
+
257
+ # Set a full version verbatim
258
+ wyvrnpm version 2.0.0.0
259
+
260
+ # CI-friendly capture
261
+ wyvrnpm version --build "$BUILD_ID" --format json
262
+ # {"command":"version","previous":"1.2.3.4","version":"1.2.3.99","written":true,...}
263
+
264
+ # Preview without writing
265
+ wyvrnpm version --build 99 --dry-run
266
+ ```
267
+
268
+ **Options**
269
+
270
+ | Option | Default | Description |
271
+ |---|---|---|
272
+ | `<newVersion>` positional | — | Full 4-part version (e.g. `1.2.3.4`). Mutex with `--major/--minor/--patch/--build`. |
273
+ | `--major` | — | Replace the 1st component. |
274
+ | `--minor` | — | Replace the 2nd component. |
275
+ | `--patch` | — | Replace the 3rd component. |
276
+ | `--build` | — | Replace the 4th component (typical CI use). |
277
+ | `--dry-run` | `false` | Compute and print the proposed version without writing. |
278
+ | `--format` | `text` | Output format: `text` (human) or `json` (CI — single object on stdout). |
279
+ | `--manifest` | `./wyvrn.json` | Path to the manifest file. |
280
+
281
+ ---
282
+
238
283
  ### `wyvrnpm install`
239
284
 
240
285
  Resolves the full dependency graph and downloads all packages.
@@ -431,6 +476,10 @@ wyvrnpm publish --path ./dist
431
476
 
432
477
  # Overwrite an existing build (same version + profile hash)
433
478
  wyvrnpm publish --force
479
+
480
+ # Dry-run — show the upload plan without touching the registry
481
+ wyvrnpm publish --dry-run
482
+ wyvrnpm publish --dry-run --format json | jq '.wouldOverwrite, .plan'
434
483
  ```
435
484
 
436
485
  **Options**
@@ -445,6 +494,7 @@ wyvrnpm publish --force
445
494
  | `--path` | `.` | Directory to zip and publish. |
446
495
  | `--manifest` | `./wyvrn.json` | Path to the manifest file. |
447
496
  | `--force` / `-f` | `false` | Overwrite an existing published version (same version + profile hash). |
497
+ | `--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`. |
448
498
 
449
499
  During publish the tool also:
450
500
  - reads `wyvrn.lock` to pin locked dependency versions into the uploaded manifest
package/bin/wyvrn.js CHANGED
@@ -5,6 +5,7 @@
5
5
  const yargs = require('yargs');
6
6
  const init = require('../src/commands/init');
7
7
  const add = require('../src/commands/add');
8
+ const versionCmd = require('../src/commands/version');
8
9
  const install = require('../src/commands/install');
9
10
  const clean = require('../src/commands/clean');
10
11
  const publish = require('../src/commands/publish');
@@ -143,6 +144,40 @@ yargs
143
144
  (argv) => add(argv),
144
145
  )
145
146
 
147
+ // ── version ───────────────────────────────────────────────────────────────
148
+ .command(
149
+ 'version [newVersion]',
150
+ 'Read, set, or partially update the top-level `version` field in wyvrn.json. ' +
151
+ 'Bare `wyvrnpm version` prints the current value on stdout (for `VER=$(...)` ' +
152
+ 'capture). Pass a full 4-part value (1.2.3.4) or component flags (--major / ' +
153
+ '--minor / --patch / --build) to tweak individual parts. Designed for CI/CD — ' +
154
+ 'pair with --format json to capture { previous, version } on stdout.',
155
+ (y) => {
156
+ y
157
+ .positional('newVersion', {
158
+ type: 'string',
159
+ describe: 'Full 4-part version to write verbatim (e.g. 1.2.3.4). ' +
160
+ 'Mutually exclusive with --major/--minor/--patch/--build.',
161
+ })
162
+ .option('major', { type: 'number', description: 'Replace the 1st component (major).' })
163
+ .option('minor', { type: 'number', description: 'Replace the 2nd component (minor).' })
164
+ .option('patch', { type: 'number', description: 'Replace the 3rd component (patch).' })
165
+ .option('build', { type: 'number', description: 'Replace the 4th component (build) — typical CI use.' })
166
+ .option('dry-run', {
167
+ type: 'boolean',
168
+ default: false,
169
+ description: 'Compute and print the proposed version without writing wyvrn.json.',
170
+ })
171
+ .option('format', {
172
+ type: 'string',
173
+ choices: ['text', 'json'],
174
+ default: 'text',
175
+ description: 'Output format. "json" emits { command, previous, version, ... } on stdout.',
176
+ });
177
+ },
178
+ (argv) => versionCmd({ ...argv, dryRun: argv['dry-run'] }),
179
+ )
180
+
146
181
  // ── install ───────────────────────────────────────────────────────────────
147
182
  .command(
148
183
  'install',
@@ -529,6 +564,17 @@ yargs
529
564
  description: 'Overwrite an existing published version',
530
565
  default: false,
531
566
  })
567
+ .option('dry-run', {
568
+ type: 'boolean',
569
+ default: false,
570
+ description:
571
+ 'Mirror of `npm publish --dry-run` — run every step (profile, ' +
572
+ 'options, zip, SHA256, v2Exists check) and print the upload plan, ' +
573
+ 'but skip the provider write. An existing published version is ' +
574
+ 'reported as a warning instead of failing. Pair with --format json ' +
575
+ 'to script bulk publish validation (plan + wouldOverwrite flag ' +
576
+ 'land in stdout).',
577
+ })
532
578
  .option('option', {
533
579
  alias: 'o',
534
580
  type: 'string',
@@ -554,7 +600,12 @@ yargs
554
600
  })
555
601
  ;
556
602
  },
557
- (argv) => publish({ ...argv, awsProfile: argv['aws-profile'], tokenEnv: argv['token-env'] }),
603
+ (argv) => publish({
604
+ ...argv,
605
+ awsProfile: argv['aws-profile'],
606
+ tokenEnv: argv['token-env'],
607
+ dryRun: argv['dry-run'],
608
+ }),
558
609
  )
559
610
 
560
611
  // ── configure ─────────────────────────────────────────────────────────────
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyvrnpm",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "A simple, static-hosting-compatible C++ package manager",
5
5
  "keywords": [
6
6
  "c++",
@@ -22,6 +22,59 @@ const { loadIgnorePatterns, isIgnored } = require('../ignore'
22
22
  const { hasBuildRecipe, normalizeRecipe } = require('../build/recipe');
23
23
  const log = require('../logger');
24
24
 
25
+ /**
26
+ * Human-friendly byte formatter for the dry-run summary.
27
+ *
28
+ * @param {number} n
29
+ * @returns {string}
30
+ */
31
+ function formatBytes(n) {
32
+ if (!Number.isFinite(n) || n < 0) return `${n}`;
33
+ if (n < 1024) return `${n} B`;
34
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
35
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(2)} MB`;
36
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
37
+ }
38
+
39
+ /**
40
+ * Compute the registry URLs/paths that `v2Publish` would write to. Pure —
41
+ * no network, no filesystem. Mirrors the keys produced by S3/HTTP/File
42
+ * providers (§4.5 of CLAUDE.md), so dry-run can surface the exact shape
43
+ * of the upload without actually performing it.
44
+ *
45
+ * @param {{
46
+ * source: string,
47
+ * name: string,
48
+ * version: string,
49
+ * profileHash: string,
50
+ * gitSha?: string | null,
51
+ * gitRepo?: string | null,
52
+ * updateLatest?: boolean,
53
+ * }} opts
54
+ * @returns {Array<{ path: string, kind: 'wyvrn.zip'|'wyvrn.json'|'source.json'|'versions.json'|'latest.json' }>}
55
+ */
56
+ function planPublishUrls({
57
+ source, name, version, profileHash,
58
+ gitSha = null, gitRepo = null, updateLatest = true,
59
+ }) {
60
+ const base = source.replace(/\/$/, '');
61
+ const v2root = `${base}/v2/${name}`;
62
+ const buildRoot = `${v2root}/${version}/${profileHash}`;
63
+
64
+ const plan = [
65
+ { path: `${buildRoot}/wyvrn.zip`, kind: 'wyvrn.zip' },
66
+ { path: `${buildRoot}/wyvrn.json`, kind: 'wyvrn.json' },
67
+ ];
68
+ if (gitSha || gitRepo) {
69
+ plan.push({ path: `${v2root}/${version}/source.json`, kind: 'source.json' });
70
+ }
71
+ plan.push({ path: `${v2root}/versions.json`, kind: 'versions.json' });
72
+ if (updateLatest) {
73
+ plan.push({ path: `${v2root}/latest.json`, kind: 'latest.json' });
74
+ }
75
+ return plan;
76
+ }
77
+
25
78
  /**
26
79
  * Walk `dir` recursively and add every file to `zip`, skipping anything that
27
80
  * matches `.wyvrnignore` patterns OR whose zip-relative path is already in
@@ -146,7 +199,11 @@ async function publish(argv) {
146
199
 
147
200
  const { source, awsProfile, token } = resolveSource(argv, ctx);
148
201
  const { path: publishPath, force } = argv;
202
+ const dryRun = Boolean(argv.dryRun);
149
203
  const jsonOut = ctx.flags.format === 'json';
204
+ if (dryRun) {
205
+ log.info('DRY RUN — no upload will occur. Previewing publish plan only.');
206
+ }
150
207
 
151
208
  // ── Load manifest ─────────────────────────────────────────────────────────
152
209
  // ctx.manifest is a lazy getter — throws on first access if the file is
@@ -221,10 +278,18 @@ async function publish(argv) {
221
278
  );
222
279
 
223
280
  // ── Check v2 existence ────────────────────────────────────────────────────
281
+ // Dry-run softens the overwrite-without-force error to a warning so the
282
+ // full plan still prints — CI callers should parse `--format json` and
283
+ // gate on `wouldOverwrite` rather than on the exit code.
224
284
  const v2AlreadyExists = await provider.v2Exists({ source, name, version, profileHash, awsProfile, token });
225
285
  if (v2AlreadyExists) {
226
286
  if (force) {
227
287
  log.warn(`${name}@${version} [${profileHash}] already exists — overwriting (--force)`);
288
+ } else if (dryRun) {
289
+ log.warn(
290
+ `${name}@${version} [${profileHash}] already exists on the registry. ` +
291
+ `A real publish would refuse without --force.`,
292
+ );
228
293
  } else {
229
294
  log.error(
230
295
  `${name}@${version} [${profileHash}] already exists.\n` +
@@ -380,7 +445,60 @@ async function publish(argv) {
380
445
  zip.writeZip(tmpZipPath);
381
446
 
382
447
  const contentSha256 = sha256Of(tmpZipPath);
448
+ const zipBytes = fs.statSync(tmpZipPath).size;
449
+ const manifestBytes = fs.statSync(tmpManifestPath).size;
383
450
  log.info(`Content SHA256 : ${contentSha256}`);
451
+ log.info(`Zip size : ${formatBytes(zipBytes)} (${zipBytes} bytes)`);
452
+
453
+ // ── Dry run: print the plan, skip the actual upload ──────────────────
454
+ // Mirrors `npm publish --dry-run` — every step above (profile, options,
455
+ // zip, hash, v2Exists) runs; only the provider write is skipped.
456
+ if (dryRun) {
457
+ const plan = planPublishUrls({
458
+ source, name, version, profileHash, gitSha, gitRepo, updateLatest: true,
459
+ });
460
+
461
+ if (!jsonOut) {
462
+ console.log();
463
+ console.log(`Would publish ${name}@${version} [${profileHash}] to ${source}`);
464
+ console.log(` contentSha256 : ${contentSha256}`);
465
+ console.log(` zip size : ${formatBytes(zipBytes)}`);
466
+ console.log(` manifest size : ${formatBytes(manifestBytes)}`);
467
+ if (v2AlreadyExists) {
468
+ console.log(` overwrite : YES — ${force ? '--force set, would proceed' : 'NOT SET, real publish would refuse'}`);
469
+ }
470
+ console.log();
471
+ console.log('Registry writes:');
472
+ for (const { path: p, kind } of plan) {
473
+ console.log(` → ${p} (${kind})`);
474
+ }
475
+ console.log();
476
+ log.success('Dry run OK — nothing uploaded.');
477
+ } else {
478
+ const payload = {
479
+ command: 'publish',
480
+ dryRun: true,
481
+ wyvrnpmVersion: require('../../package.json').version,
482
+ name,
483
+ version,
484
+ profileHash,
485
+ source,
486
+ contentSha256,
487
+ zipBytes,
488
+ manifestBytes,
489
+ options: effectiveOptions ?? null,
490
+ gitSha: gitSha ?? null,
491
+ gitRepo: gitRepo ?? null,
492
+ wouldOverwrite: Boolean(v2AlreadyExists),
493
+ forceSet: Boolean(force),
494
+ publishedLock,
495
+ publishedConf: Object.keys(publishedConf).length > 0 ? unflattenConf(publishedConf) : null,
496
+ plan,
497
+ };
498
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
499
+ }
500
+ return;
501
+ }
384
502
 
385
503
  // ── v2 publish ────────────────────────────────────────────────────────
386
504
  log.info('Publishing (v2) ...');
@@ -433,3 +551,5 @@ module.exports = publish;
433
551
  module.exports.addFolderFiltered = addFolderFiltered;
434
552
  module.exports.addInstallTree = addInstallTree;
435
553
  module.exports.collectInstallTreeFiles = collectInstallTreeFiles;
554
+ module.exports.planPublishUrls = planPublishUrls;
555
+ module.exports.formatBytes = formatBytes;
@@ -0,0 +1,187 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const { readManifest, writeManifest } = require('../manifest');
6
+ const log = require('../logger');
7
+
8
+ /** Strict 4-part wyvrnpm version: `x.y.z.b`, all non-negative integers, no leading zeros. */
9
+ const VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
10
+
11
+ /**
12
+ * Parse a 4-part version string into `[major, minor, patch, build]`.
13
+ * Throws with a clear message when the string fails the wyvrnpm format.
14
+ *
15
+ * @param {string} v
16
+ * @returns {[number, number, number, number]}
17
+ */
18
+ function parseVersion(v) {
19
+ if (typeof v !== 'string' || v.length === 0) {
20
+ throw new Error('version is empty');
21
+ }
22
+ const m = VERSION_RE.exec(v);
23
+ if (!m) {
24
+ throw new Error(
25
+ `"${v}" is not a valid wyvrnpm version. ` +
26
+ 'Expected 4-part x.y.z.b with non-negative integers (e.g. 1.2.3.4).',
27
+ );
28
+ }
29
+ return [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
30
+ }
31
+
32
+ const COMPONENT_INDEX = { major: 0, minor: 1, patch: 2, build: 3 };
33
+
34
+ function isNonNegInt(n) {
35
+ return Number.isInteger(n) && n >= 0;
36
+ }
37
+
38
+ /**
39
+ * `wyvrnpm version [<new-version>]` — read, set, or partially update the
40
+ * top-level `version` field in wyvrn.json. Designed for CI/CD (build-number
41
+ * injection + tag-the-build readback).
42
+ *
43
+ * wyvrnpm version → print the current version
44
+ * wyvrnpm version 1.2.3.4 → set verbatim
45
+ * wyvrnpm version --build 99 → only the 4th component
46
+ * wyvrnpm version --minor 3 --build 0 → multiple components, others left alone
47
+ *
48
+ * Positional and component flags are mutually exclusive — pick one mode.
49
+ * Bare invocation is read-only (never writes, never warns about an unset
50
+ * version on disk — it prints `(unset)` to stderr and exits 1 in that
51
+ * case so scripts notice). CI scripts use --format json to capture the
52
+ * `{ previous, version }` payload on stdout.
53
+ *
54
+ * @param {object} argv
55
+ */
56
+ function versionCmd(argv) {
57
+ const manifestPath = path.resolve(argv.manifest);
58
+ const jsonOut = argv.format === 'json';
59
+ if (jsonOut) log.setJsonMode(true);
60
+
61
+ const positional = argv.newVersion;
62
+
63
+ // Component flags arrive on argv as `argv.major` / `argv.minor` / etc.
64
+ // because yargs lowercases option names. Keep insertion order so error
65
+ // messages list flags in the same order as the CLI surface.
66
+ const partial = {};
67
+ for (const key of Object.keys(COMPONENT_INDEX)) {
68
+ if (argv[key] !== undefined) partial[key] = argv[key];
69
+ }
70
+ const partialKeys = Object.keys(partial);
71
+
72
+ if (positional && partialKeys.length > 0) {
73
+ log.error(
74
+ 'cannot combine a positional version with --major/--minor/--patch/--build. ' +
75
+ 'Use one or the other.',
76
+ );
77
+ process.exit(1);
78
+ }
79
+
80
+ // Read-only mode: no positional + no component flags. Prints the current
81
+ // version and returns without touching disk. Matches `npm version` behaviour
82
+ // when called bare.
83
+ const readOnly = !positional && partialKeys.length === 0;
84
+
85
+ let manifest;
86
+ try {
87
+ manifest = readManifest(manifestPath);
88
+ } catch (err) {
89
+ log.error(err.message);
90
+ process.exit(1);
91
+ }
92
+
93
+ const previous =
94
+ typeof manifest.version === 'string' && manifest.version.length > 0
95
+ ? manifest.version
96
+ : null;
97
+
98
+ if (readOnly) {
99
+ const wyvrnpmVersion = require('../../package.json').version;
100
+ if (jsonOut) {
101
+ process.stdout.write(JSON.stringify({
102
+ command: 'version',
103
+ wyvrnpmVersion,
104
+ path: manifestPath,
105
+ version: previous,
106
+ written: false,
107
+ }) + '\n');
108
+ } else if (previous) {
109
+ // Bare stdout for `VER=$(wyvrnpm version)` — no `[wyvrn]` prefix here.
110
+ process.stdout.write(previous + '\n');
111
+ } else {
112
+ log.error(`no \`version\` field in ${manifestPath}`);
113
+ process.exit(1);
114
+ }
115
+ return;
116
+ }
117
+
118
+ let newVersion;
119
+ if (positional) {
120
+ try {
121
+ parseVersion(positional);
122
+ } catch (err) {
123
+ log.error(err.message);
124
+ process.exit(1);
125
+ }
126
+ newVersion = positional;
127
+ } else {
128
+ let parts;
129
+ try {
130
+ parts = parseVersion(previous ?? '');
131
+ } catch (err) {
132
+ log.error(
133
+ `cannot tweak components — current manifest version is invalid or missing: ${err.message}\n` +
134
+ ' Set a full version first: wyvrnpm version <x.y.z.b>',
135
+ );
136
+ process.exit(1);
137
+ }
138
+ for (const key of partialKeys) {
139
+ const n = partial[key];
140
+ if (!isNonNegInt(n)) {
141
+ log.error(`--${key} must be a non-negative integer (got ${JSON.stringify(n)})`);
142
+ process.exit(1);
143
+ }
144
+ parts[COMPONENT_INDEX[key]] = n;
145
+ }
146
+ newVersion = parts.join('.');
147
+ }
148
+
149
+ const wyvrnpmVersion = require('../../package.json').version;
150
+
151
+ if (argv.dryRun) {
152
+ if (jsonOut) {
153
+ process.stdout.write(JSON.stringify({
154
+ command: 'version',
155
+ wyvrnpmVersion,
156
+ path: manifestPath,
157
+ previous,
158
+ version: newVersion,
159
+ dryRun: true,
160
+ written: false,
161
+ }) + '\n');
162
+ } else {
163
+ log.info(`would set version: ${previous ?? '(unset)'} → ${newVersion} (dry-run)`);
164
+ }
165
+ return;
166
+ }
167
+
168
+ manifest.version = newVersion;
169
+ writeManifest(manifestPath, manifest);
170
+
171
+ if (jsonOut) {
172
+ process.stdout.write(JSON.stringify({
173
+ command: 'version',
174
+ wyvrnpmVersion,
175
+ path: manifestPath,
176
+ previous,
177
+ version: newVersion,
178
+ dryRun: false,
179
+ written: true,
180
+ }) + '\n');
181
+ } else {
182
+ log.success(`${manifestPath}: ${previous ?? '(unset)'} → ${newVersion}`);
183
+ }
184
+ }
185
+
186
+ module.exports = versionCmd;
187
+ module.exports.parseVersion = parseVersion;