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 +5 -0
- package/bin/wyvrn.js +17 -1
- package/claude/skills/wyvrnpm.skill +0 -0
- package/package.json +1 -1
- package/src/commands/publish.js +120 -0
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({
|
|
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
package/src/commands/publish.js
CHANGED
|
@@ -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;
|