wyvrnpm 2.10.0 → 2.10.2

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
@@ -431,6 +431,10 @@ wyvrnpm publish --path ./dist
431
431
 
432
432
  # Overwrite an existing build (same version + profile hash)
433
433
  wyvrnpm publish --force
434
+
435
+ # Dry-run — show the upload plan without touching the registry
436
+ wyvrnpm publish --dry-run
437
+ wyvrnpm publish --dry-run --format json | jq '.wouldOverwrite, .plan'
434
438
  ```
435
439
 
436
440
  **Options**
@@ -445,6 +449,7 @@ wyvrnpm publish --force
445
449
  | `--path` | `.` | Directory to zip and publish. |
446
450
  | `--manifest` | `./wyvrn.json` | Path to the manifest file. |
447
451
  | `--force` / `-f` | `false` | Overwrite an existing published version (same version + profile hash). |
452
+ | `--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
453
 
449
454
  During publish the tool also:
450
455
  - reads `wyvrn.lock` to pin locked dependency versions into the uploaded manifest
package/bin/wyvrn.js CHANGED
@@ -529,6 +529,17 @@ yargs
529
529
  description: 'Overwrite an existing published version',
530
530
  default: false,
531
531
  })
532
+ .option('dry-run', {
533
+ type: 'boolean',
534
+ default: false,
535
+ description:
536
+ 'Mirror of `npm publish --dry-run` — run every step (profile, ' +
537
+ 'options, zip, SHA256, v2Exists check) and print the upload plan, ' +
538
+ 'but skip the provider write. An existing published version is ' +
539
+ 'reported as a warning instead of failing. Pair with --format json ' +
540
+ 'to script bulk publish validation (plan + wouldOverwrite flag ' +
541
+ 'land in stdout).',
542
+ })
532
543
  .option('option', {
533
544
  alias: 'o',
534
545
  type: 'string',
@@ -554,7 +565,12 @@ yargs
554
565
  })
555
566
  ;
556
567
  },
557
- (argv) => publish({ ...argv, awsProfile: argv['aws-profile'], tokenEnv: argv['token-env'] }),
568
+ (argv) => publish({
569
+ ...argv,
570
+ awsProfile: argv['aws-profile'],
571
+ tokenEnv: argv['token-env'],
572
+ dryRun: argv['dry-run'],
573
+ }),
558
574
  )
559
575
 
560
576
  // ── 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.10.2",
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;