@theholocron/cli 5.0.0-alpha.5 → 5.0.0-alpha.7

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/dist/cli.mjs CHANGED
@@ -3324,6 +3324,170 @@ async function runPublish(input = {}) {
3324
3324
  packageNames
3325
3325
  };
3326
3326
  }
3327
+ /**
3328
+ * `holocron publish` (no `--initial`) — the steady-state publish
3329
+ * `exec.publishCmd` in `release.config.ts` should call, replacing each
3330
+ * repo's hand-typed `pnpm -r --filter=./packages/* publish ...` shell (which
3331
+ * differs, today, only in monorepo-vs-single-package, provenance on/off, and
3332
+ * whether a repo needs to skip already-published packages) with one uniform
3333
+ * invocation. Part of the config-resolution workstream, #676 — see
3334
+ * `.notes/tech-config-resolution.spec.md`.
3335
+ *
3336
+ * Deliberately does not do the `--initial` bootstrap dance (npm
3337
+ * login-if-needed, Trusted Publisher next-steps printout) — a steady-state
3338
+ * publish runs in CI, authenticated via OIDC automatically, no interactive
3339
+ * step involved. Reuses `hasPackagesDir()`'s monorepo-vs-single-package
3340
+ * detection rather than duplicating it.
3341
+ */
3342
+ async function runSteadyPublish(input = {}) {
3343
+ const print = input.print ?? ((line) => console.log(line));
3344
+ const logger = input.logger ?? getLogger();
3345
+ const cwd = input.cwd ?? process.cwd();
3346
+ const tag = input.tag ?? "latest";
3347
+ const dryRun = input.dryRun ?? false;
3348
+ const otp = input.otp;
3349
+ const exec = input.exec ?? defaultExec$1;
3350
+ const provenance = input.provenance ?? true;
3351
+ const skipAlreadyPublished = input.skipAlreadyPublished ?? false;
3352
+ const entries = input.packages ?? discoverPublicPackageEntries(cwd);
3353
+ const packageNames = entries.map((e) => e.name);
3354
+ print(`Holocron publish${dryRun ? " (dry-run)" : ""}`);
3355
+ print(` cwd: ${cwd}`);
3356
+ print(` tag: ${tag}`);
3357
+ print(` provenance: ${provenance}`);
3358
+ if (skipAlreadyPublished) print(" skip-already-published: true");
3359
+ logger.info({
3360
+ tag,
3361
+ provenance,
3362
+ skipAlreadyPublished,
3363
+ dryRun: dryRun || void 0
3364
+ }, "publish: start");
3365
+ if (otp) print(` otp: <${otp.length} chars>`);
3366
+ print("");
3367
+ if (entries.length === 0) {
3368
+ const message = "nothing to publish (no packages/* and root package.json is private or unnamed)";
3369
+ print(` ✗ ${message}`);
3370
+ logger.warn({ reason: message }, "publish: done");
3371
+ return {
3372
+ status: "fail",
3373
+ message,
3374
+ packageNames
3375
+ };
3376
+ }
3377
+ const baseFlags = [
3378
+ "--access",
3379
+ "public",
3380
+ "--no-git-checks",
3381
+ "--tag",
3382
+ tag,
3383
+ ...provenance ? ["--provenance"] : []
3384
+ ];
3385
+ const otpFlags = otp ? ["--otp", otp] : [];
3386
+ if (dryRun) {
3387
+ print("");
3388
+ if (skipAlreadyPublished) for (const entry of entries) print(` … would check ${entry.name}@${entry.version}, publish if not already on npm`);
3389
+ else {
3390
+ const args = [
3391
+ ...hasPackagesDir(cwd) ? ["-r", "--filter=./packages/*"] : [],
3392
+ "publish",
3393
+ ...baseFlags,
3394
+ ...otpFlags
3395
+ ];
3396
+ print(` … (dry-run) skipping actual publish`);
3397
+ print(` would run: pnpm ${args.join(" ")}`);
3398
+ }
3399
+ logger.info({
3400
+ tag,
3401
+ status: "dry-run",
3402
+ packages: entries.length
3403
+ }, "publish: done");
3404
+ return {
3405
+ status: "dry-run",
3406
+ message: "dry-run — no publish executed",
3407
+ packageNames
3408
+ };
3409
+ }
3410
+ if (skipAlreadyPublished) {
3411
+ print(" → publishing packages not already on npm…");
3412
+ for (const entry of entries) {
3413
+ const view = await exec("npm", [
3414
+ "view",
3415
+ `${entry.name}@${entry.version}`,
3416
+ "version"
3417
+ ], { cwd });
3418
+ if (view.exitCode === 0 && view.stdout.trim()) {
3419
+ print(` skip ${entry.name}@${entry.version} (already published)`);
3420
+ continue;
3421
+ }
3422
+ const publish = await exec("pnpm", [
3423
+ "--filter",
3424
+ entry.dir,
3425
+ "publish",
3426
+ ...baseFlags,
3427
+ ...otpFlags
3428
+ ], { cwd });
3429
+ if (publish.exitCode !== 0) {
3430
+ const message = `publish failed for ${entry.name}@${entry.version} (exit ${publish.exitCode}): ${publish.stderr.trim() || publish.stdout.trim() || "no output"}`;
3431
+ print(` ✗ ${message}`);
3432
+ logger.warn({
3433
+ tag,
3434
+ package: entry.name,
3435
+ reason: message
3436
+ }, "publish: done");
3437
+ return {
3438
+ status: "fail",
3439
+ message,
3440
+ packageNames
3441
+ };
3442
+ }
3443
+ print(` ✓ ${entry.name}@${entry.version}`);
3444
+ }
3445
+ logger.info({
3446
+ tag,
3447
+ status: "ok",
3448
+ packages: entries.length
3449
+ }, "publish: done");
3450
+ return {
3451
+ status: "ok",
3452
+ packageNames
3453
+ };
3454
+ }
3455
+ print(" → publishing all public @theholocron/* packages…");
3456
+ const publish = await exec("pnpm", [
3457
+ ...hasPackagesDir(cwd) ? ["-r", "--filter=./packages/*"] : [],
3458
+ "publish",
3459
+ ...baseFlags,
3460
+ ...otpFlags
3461
+ ], { cwd });
3462
+ if (publish.exitCode !== 0) {
3463
+ const message = `publish failed (exit ${publish.exitCode}): ${publish.stderr.trim() || publish.stdout.trim() || "no output"}`;
3464
+ print(` ✗ ${message}`);
3465
+ if (publish.stdout.includes("EOTP") || publish.stderr.includes("EOTP")) {
3466
+ print("");
3467
+ print(" → hint: your npm account requires 2FA for writes. Re-run with `--otp <code>`:");
3468
+ print(` pnpm exec holocron publish --otp <6-digit-code>`);
3469
+ }
3470
+ logger.warn({
3471
+ tag,
3472
+ reason: message
3473
+ }, "publish: done");
3474
+ return {
3475
+ status: "fail",
3476
+ message,
3477
+ packageNames
3478
+ };
3479
+ }
3480
+ print(" ✓ publish complete");
3481
+ logger.info({
3482
+ tag,
3483
+ status: "ok",
3484
+ packages: entries.length
3485
+ }, "publish: done");
3486
+ return {
3487
+ status: "ok",
3488
+ packageNames
3489
+ };
3490
+ }
3327
3491
  function hasPackagesDir(cwd) {
3328
3492
  return existsSync(join(cwd, "packages"));
3329
3493
  }
@@ -3337,6 +3501,37 @@ function publicPackageName(pkgPath) {
3337
3501
  return;
3338
3502
  }
3339
3503
  }
3504
+ /** Read a `package.json`'s `name` + `version`, when it's public (`!private && name`). */
3505
+ function publicPackageEntry(pkgPath, dir) {
3506
+ if (!existsSync(pkgPath)) return void 0;
3507
+ try {
3508
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
3509
+ return !pkg.private && pkg.name && pkg.version ? {
3510
+ name: pkg.name,
3511
+ version: pkg.version,
3512
+ dir
3513
+ } : void 0;
3514
+ } catch {
3515
+ return;
3516
+ }
3517
+ }
3518
+ /**
3519
+ * Same repo-layout detection as {@link discoverPublicPackages}, but carrying
3520
+ * version + a `pnpm --filter`-ready path — {@link runSteadyPublish}'s
3521
+ * `skipAlreadyPublished` mode needs both to check `npm view <pkg>@<version>`
3522
+ * and publish one workspace at a time.
3523
+ */
3524
+ function discoverPublicPackageEntries(cwd) {
3525
+ if (!hasPackagesDir(cwd)) {
3526
+ const entry = publicPackageEntry(join(cwd, "package.json"), ".");
3527
+ return entry ? [entry] : [];
3528
+ }
3529
+ const packagesDir = join(cwd, "packages");
3530
+ return readdirSync(packagesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).flatMap((e) => {
3531
+ const entry = publicPackageEntry(join(packagesDir, e.name, "package.json"), `./packages/${e.name}`);
3532
+ return entry ? [entry] : [];
3533
+ });
3534
+ }
3340
3535
  /**
3341
3536
  * Monorepo (`packages/` present): every public `packages/*` workspace.
3342
3537
  * Single package (no `packages/`): the root `package.json`, if it's public —
@@ -7976,11 +8171,10 @@ try {
7976
8171
  }).command("publish", "Publish @theholocron/* packages to npm", (y) => y.option("initial", {
7977
8172
  type: "boolean",
7978
8173
  default: false,
7979
- describe: "One-shot bootstrap publish for trusted-publishing-eligible packages (npm needs the package to exist before Trusted Publishing can be configured for it). Required today — a non-initial `publish` isn't implemented yet."
8174
+ describe: "One-shot bootstrap publish for trusted-publishing-eligible packages (npm needs the package to exist before Trusted Publishing can be configured for it). Skips the steady-state OIDC path below — use only for a package's very first publish."
7980
8175
  }).option("tag", {
7981
8176
  type: "string",
7982
- default: "alpha",
7983
- describe: "npm distribution tag (defaults to alpha)"
8177
+ describe: "npm distribution tag. Defaults to alpha for --initial, latest for a steady-state publish."
7984
8178
  }).option("otp", {
7985
8179
  type: "string",
7986
8180
  describe: "One-time password from your authenticator (required if npm needs 2FA for writes)"
@@ -7990,6 +8184,14 @@ try {
7990
8184
  }).option("repo", {
7991
8185
  type: "string",
7992
8186
  describe: "owner/repo for Trusted Publisher config (--sync-trust). Defaults to theholocron/holocron."
8187
+ }).option("provenance", {
8188
+ type: "boolean",
8189
+ default: true,
8190
+ describe: "Publish with --provenance (steady-state only). Default on — free under CI/OIDC."
8191
+ }).option("skip-already-published", {
8192
+ type: "boolean",
8193
+ default: false,
8194
+ describe: "Steady-state only: check `npm view <pkg>@<version>` before publishing each package and skip it if that exact version already exists, instead of one bulk publish. For repos that ship many independently-versioned packages where a release doesn't bump every one."
7993
8195
  }), async (argv) => {
7994
8196
  if (argv.syncTrust) {
7995
8197
  const [oldFile, newFile] = argv.syncTrust;
@@ -8007,15 +8209,21 @@ try {
8007
8209
  })).status === "fail") process.exitCode = 1;
8008
8210
  return;
8009
8211
  }
8010
- if (!argv.initial) {
8011
- getLogger().error("publish: only `--initial` and `--sync-trust` are supported today. Run `holocron publish --initial`.");
8012
- process.exitCode = 1;
8212
+ if (argv.initial) {
8213
+ if ((await runPublish({
8214
+ cwd: argv.cwd,
8215
+ dryRun: argv.dryRun,
8216
+ ...argv.tag ? { tag: argv.tag } : {},
8217
+ ...argv.otp ? { otp: argv.otp } : {}
8218
+ })).status === "fail") process.exitCode = 1;
8013
8219
  return;
8014
8220
  }
8015
- if ((await runPublish({
8221
+ if ((await runSteadyPublish({
8016
8222
  cwd: argv.cwd,
8017
- tag: argv.tag,
8018
8223
  dryRun: argv.dryRun,
8224
+ provenance: argv.provenance,
8225
+ skipAlreadyPublished: argv.skipAlreadyPublished,
8226
+ ...argv.tag ? { tag: argv.tag } : {},
8019
8227
  ...argv.otp ? { otp: argv.otp } : {}
8020
8228
  })).status === "fail") process.exitCode = 1;
8021
8229
  }).command("sync [steps..]", "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)", (y) => y.positional("steps", {