beez-rp 0.2.0 → 0.3.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.
@@ -27,6 +27,7 @@ import {
27
27
  FAILURE_EXIT_CODE,
28
28
  GITHUB_REPOSITORY_PATTERN,
29
29
  MAIN_BRANCH,
30
+ MAIN_SYNCED_RESTART_MESSAGE,
30
31
  MAX_LISTED_COMMITS,
31
32
  MAX_LISTED_ITEMS,
32
33
  MIGRATION_STATUS,
@@ -61,9 +62,17 @@ import {
61
62
  startSpinner,
62
63
  } from "../terminal-ui.js";
63
64
  import { listNextVersions, resolveRequestedVersion, suggestReleaseType, toReleaseTag } from "../versions.js";
65
+ import {
66
+ expandArtifactPattern,
67
+ findPnpmPackRewrites,
68
+ findPreparedArtifact,
69
+ isSafeArtifactPath,
70
+ verifyPreparedArtifact,
71
+ withArtifactOutsidePackageRoot,
72
+ } from "./artifact.js";
64
73
  import { loadCreateVersionConfig } from "./config.js";
65
74
  import { ReleaseStepError } from "./errors.js";
66
- import { lookupPublishedVersions, publishToNpm } from "./npm.js";
75
+ import { buildNpmAuthConfigLine, describePublishedRelease, lookupPublishedVersions, publishToNpm, readNpmPackIntegrity, resolvePublishRegistry } from "./npm.js";
67
76
  import { RELEASE_USAGE, buildReleasePlan, parseReleaseArguments } from "./plan.js";
68
77
  import { createGitReader, listCommits, runCommandLine, runInherited } from "./process.js";
69
78
  import { collectReleaseState } from "./state.js";
@@ -83,12 +92,21 @@ import { collectReleaseState } from "./state.js";
83
92
  * pushed: boolean,
84
93
  * published: boolean,
85
94
  * commitCount: number | null,
95
+ * packageName: string,
96
+ * registryUrl: string | null,
86
97
  * }} ReleaseContext
87
98
  */
88
99
 
89
100
  /** Raised when the user cancels on purpose; ends the run without an error box. */
90
101
  class ReleaseCancelledError extends Error {}
91
102
 
103
+ /**
104
+ * Raised after syncing `main` brought new commits: the diagnosis, the plan and the configuration
105
+ * (with every module it imports) belong to the previous `main`, so the run ends without an error
106
+ * and asks to run the command again in a new process.
107
+ */
108
+ class MainSyncedRestartError extends Error {}
109
+
92
110
  /** Answers of the pending-migrations prompt. */
93
111
  const MIGRATION_CHOICE = Object.freeze({ apply: "apply", skip: "skip", cancel: "cancel" });
94
112
 
@@ -304,6 +322,16 @@ function readWorkingUnreleased(repositoryRoot) {
304
322
  return existsSync(changelogPath) ? readUnreleased(readFileSync(changelogPath, "utf8")) : { exists: false, entryCount: 0, unknownSections: [], body: "" };
305
323
  }
306
324
 
325
+ /**
326
+ * Reads the working tree `package.json`, which syncing `main` may have changed after the diagnosis.
327
+ *
328
+ * @param {string} repositoryRoot - Repository root.
329
+ * @returns {import("./artifact.js").PackageManifest} Current manifest.
330
+ */
331
+ function readWorkingManifest(repositoryRoot) {
332
+ return JSON.parse(readFileSync(path.join(repositoryRoot, PACKAGE_MANIFEST_FILE), "utf8"));
333
+ }
334
+
307
335
  /**
308
336
  * Returns the version being released: the one just bumped or the pending one.
309
337
  *
@@ -321,12 +349,33 @@ function requireReleaseVersion(context) {
321
349
  }
322
350
 
323
351
  /**
324
- * Fast-forwards local `main` to `origin/main`.
352
+ * Steps a configuration adds to the release plan.
353
+ *
354
+ * @param {ResolvedCreateVersionConfig} config - Resolved configuration.
355
+ * @returns {import("./plan.js").ReleaseCapabilities} Capabilities for `buildReleasePlan`.
356
+ */
357
+ function describeReleaseCapabilities(config) {
358
+ return {
359
+ checks: config.checks.length > 0,
360
+ prepare: config.prepare !== null,
361
+ publish: config.publish !== null,
362
+ publishTitle: config.publish === NPM_PUBLISHER ? "Publicar en npm" : "Publicar el release",
363
+ };
364
+ }
365
+
366
+ /**
367
+ * Fast-forwards local `main` to `origin/main`. When that brings new commits the release stops
368
+ * before touching the version: the diagnosis, the plan and `beez-rp.config.(m)js` (imported with
369
+ * its modules at startup) come from the previous `main`, so the command must run again in a new
370
+ * process to diagnose with the new code and configuration.
325
371
  *
326
372
  * @param {ReleaseContext} context - Release context.
327
373
  * @returns {Promise<void>}
374
+ * @throws {ReleaseStepError} When the fast-forward fails.
375
+ * @throws {MainSyncedRestartError} When `main` moved and the command has to run again.
328
376
  */
329
377
  async function syncMainStep(context) {
378
+ const headBeforeSync = await context.reader.git(["rev-parse", "HEAD"]);
330
379
  await runGitStep(
331
380
  context,
332
381
  ["merge", "--ff-only", "--quiet", REMOTE_MAIN_REF],
@@ -334,6 +383,10 @@ async function syncMainStep(context) {
334
383
  `Revisá git status y git log ${REMOTE_MAIN_REF}..${MAIN_BRANCH}.`
335
384
  );
336
385
  print(`${ICON.success} ${MAIN_BRANCH} quedó igual a ${REMOTE_MAIN_REF}.`);
386
+
387
+ if ((await context.reader.git(["rev-parse", "HEAD"])) !== headBeforeSync) {
388
+ throw new MainSyncedRestartError();
389
+ }
337
390
  }
338
391
 
339
392
  /**
@@ -547,6 +600,128 @@ async function pushReleaseStep(context) {
547
600
  context.pushed = true;
548
601
  }
549
602
 
603
+ /**
604
+ * Lists the tracked files that differ from `HEAD`. `prepare` may create untracked or ignored
605
+ * output (`dist/`, `releases/`), but a modified tracked file (such as `package.json`) means
606
+ * `npm pack --dry-run` would no longer read the release commit.
607
+ *
608
+ * @param {ReleaseContext} context - Release context.
609
+ * @returns {Promise<string[]>} `git status --porcelain` lines of modified tracked files.
610
+ * @throws {ReleaseStepError} When Git cannot report the working tree state.
611
+ */
612
+ async function listTrackedChanges(context) {
613
+ const output = await context.reader.tryGit(["status", "--porcelain", "--untracked-files=no"]);
614
+
615
+ if (output === null) {
616
+ throw new ReleaseStepError("No se pudo leer el estado del working tree antes de publicar.", "No se publicó nada. Revisá git status y volvé a correr pnpm create-version.");
617
+ }
618
+
619
+ return output.split("\n").filter((line) => line.trim() !== "");
620
+ }
621
+
622
+ /**
623
+ * Finds and verifies the archive prepared for the release when the project configured `artifact`.
624
+ * The working tree must still be the release commit (no tracked file modified by `prepare`), the
625
+ * manifest must not rely on rewrites only `pnpm pack` applies, and the archive must have the same
626
+ * SHA-512 integrity `npm pack --dry-run --ignore-scripts` reports for that commit: `npm pack` is
627
+ * reproducible, so equal hashes mean the archive is byte for byte what npm packs.
628
+ *
629
+ * @param {ReleaseContext} context - Release context.
630
+ * @param {string} version - Version being published.
631
+ * @param {Record<string, unknown>} workingManifest - Working tree `package.json`, after `prepare`.
632
+ * @returns {Promise<string | null>} Verified archive path relative to the root, or `null` to publish the working tree.
633
+ */
634
+ async function resolvePublishedArtifact(context, version, workingManifest) {
635
+ const { artifact } = context.config;
636
+
637
+ if (!artifact) {
638
+ return null;
639
+ }
640
+
641
+ const trackedChanges = await listTrackedChanges(context);
642
+
643
+ if (trackedChanges.length > 0) {
644
+ throw new ReleaseStepError(
645
+ `El paso de preparación modificó archivos versionados: ${trackedChanges.slice(0, MAX_LISTED_ITEMS).join("; ")}.`,
646
+ "No se publicó nada. prepare puede generar archivos ignorados (dist/, releases/) pero no cambiar archivos versionados como package.json: revertí esos cambios y volvé a correr pnpm create-version."
647
+ );
648
+ }
649
+
650
+ const pnpmRewrites = findPnpmPackRewrites(workingManifest);
651
+
652
+ if (pnpmRewrites.length > 0) {
653
+ throw new ReleaseStepError(
654
+ `Este paquete depende de reescrituras de pnpm al empaquetar y beez-rp publica con npm: ${pnpmRewrites.slice(0, MAX_LISTED_ITEMS).join("; ")}.`,
655
+ "No se publicó nada. Reemplazá los especificadores workspace:/catalog:/jsr: por rangos de versión y sacá de publishConfig los campos del manifest que solo pnpm aplica al empaquetar (exports, main, bin, types...): declaralos en la raíz de package.json."
656
+ );
657
+ }
658
+
659
+ const release = { version, packageName: String(workingManifest.name) };
660
+ const prepared = findPreparedArtifact(context.repositoryRoot, artifact, release);
661
+
662
+ if (!prepared) {
663
+ throw new ReleaseStepError(
664
+ `No hay un artefacto preparado de ${version} que coincida con ${expandArtifactPattern(artifact, release)}.`,
665
+ "Revisá la salida del paso de preparación y volvé a correr pnpm create-version: retoma la preparación y la publicación."
666
+ );
667
+ }
668
+
669
+ if (!isSafeArtifactPath(prepared.path)) {
670
+ throw new ReleaseStepError(`La ruta del artefacto ${prepared.path} tiene caracteres no permitidos.`, "Usá rutas con letras, números, ., -, _, @, +, ~ y /.");
671
+ }
672
+
673
+ // Without a `files` allowlist or an ignore rule, npm would pack the archive into the package it
674
+ // describes; it is moved out of the root during the dry run and published later from its path.
675
+ const npmPack = await withArtifactOutsidePackageRoot(context.repositoryRoot, prepared.path, () => readNpmPackIntegrity(context.repositoryRoot));
676
+
677
+ if (!npmPack.pack) {
678
+ throw new ReleaseStepError(
679
+ `No se pudo verificar ${prepared.path}: ${npmPack.problem}.`,
680
+ "No se publicó nada. Corré npm pack --dry-run --json --ignore-scripts en la raíz para ver el error y volvé a correr pnpm create-version."
681
+ );
682
+ }
683
+
684
+ if (npmPack.pack.version !== version) {
685
+ throw new ReleaseStepError(
686
+ `npm pack --dry-run describe ${npmPack.pack.name}@${npmPack.pack.version} y se está publicando ${version}.`,
687
+ "No se publicó nada. Revisá que HEAD sea el commit de release y volvé a correr pnpm create-version."
688
+ );
689
+ }
690
+
691
+ const problems = verifyPreparedArtifact(context.repositoryRoot, prepared, npmPack.pack.integrity);
692
+
693
+ if (problems.length > 0) {
694
+ throw new ReleaseStepError(
695
+ `El artefacto ${prepared.path} no se puede publicar: ${problems.join("; ")}.`,
696
+ "No se publicó nada. Hacé que prepare empaquete con npm pack --ignore-scripts después de construir, borrá ese tarball y volvé a correr pnpm create-version."
697
+ );
698
+ }
699
+
700
+ print(`${ICON.success} ${prepared.path} verificado${prepared.expectedSha256 ? " (SHA-256 de la ruta e integrity de npm pack)" : " (integrity de npm pack)"}.`);
701
+ return prepared.path;
702
+ }
703
+
704
+ /**
705
+ * Resolves the registry the release is published to: `publishConfig["@scope:registry"]` for a
706
+ * scoped package, else `publishConfig.registry`, else the registry npm's config resolves in the
707
+ * repository (project `.npmrc`, environment, global config).
708
+ *
709
+ * @param {Record<string, unknown>} manifest - Working tree `package.json`.
710
+ * @param {string} repositoryRoot - Repository root.
711
+ * @returns {Promise<string>} Registry URL, already checked to be a plain http(s) URL.
712
+ * @throws {ReleaseStepError} When the registry is not a valid http(s) URL or npm cannot report it.
713
+ */
714
+ async function resolveReleaseRegistry(manifest, repositoryRoot) {
715
+ try {
716
+ return await resolvePublishRegistry(manifest, repositoryRoot);
717
+ } catch (error) {
718
+ throw new ReleaseStepError(
719
+ `No se puede publicar: ${error instanceof Error ? error.message : String(error)}.`,
720
+ "No se publicó nada. Corregí el registry (publishConfig.registry o publishConfig[\"@scope:registry\"] en package.json, o registry/@scope:registry en .npmrc) con una URL http(s) sin credenciales y volvé a correr pnpm create-version."
721
+ );
722
+ }
723
+ }
724
+
550
725
  /**
551
726
  * Publishes the release with npm or the project hook.
552
727
  *
@@ -558,7 +733,18 @@ async function publishReleaseStep(context) {
558
733
  const { publish } = context.config;
559
734
 
560
735
  if (publish === NPM_PUBLISHER) {
561
- const result = await publishToNpm(context.repositoryRoot);
736
+ // Syncing main may have renamed the package after the diagnosis: publish under the current name.
737
+ const manifest = readWorkingManifest(context.repositoryRoot);
738
+ const packageName = String(manifest.name);
739
+ context.packageName = packageName;
740
+ const registryUrl = await resolveReleaseRegistry(manifest, context.repositoryRoot);
741
+ context.registryUrl = registryUrl;
742
+ const authConfigLine = buildNpmAuthConfigLine(registryUrl);
743
+ const artifactPath = await resolvePublishedArtifact(context, version, manifest);
744
+ if (artifactPath) {
745
+ print(paint("gray", `Publicando ${artifactPath}; npm puede pedir la confirmación 2FA en el navegador o un código.`));
746
+ }
747
+ const result = await publishToNpm(context.repositoryRoot, { authConfigLine, artifactPath });
562
748
 
563
749
  if (result.missingToken) {
564
750
  throw new ReleaseStepError(
@@ -574,7 +760,8 @@ async function publishReleaseStep(context) {
574
760
  );
575
761
  }
576
762
 
577
- const npm = await lookupPublishedVersions(context.state.packageName, context.repositoryRoot);
763
+ // npm view ignores publishConfig, so the registry the release went to is queried explicitly.
764
+ const npm = await lookupPublishedVersions(packageName, context.repositoryRoot, registryUrl);
578
765
  if (!npm.publishedVersions.includes(version)) {
579
766
  print(`${ICON.warning} ${paint("yellow", `npm todavía no muestra ${version}; puede tardar unos segundos en propagarse.`)}`);
580
767
  }
@@ -623,8 +810,8 @@ function renderReleaseSummary(context, remoteUrl, startedAt) {
623
810
  lines.push(`${ICON.success} ${paint("bold", "Git")} ${MAIN_BRANCH} + ${tag} en ${RELEASE_REMOTE}`);
624
811
  }
625
812
 
626
- if (context.published && context.config.registry === RELEASE_REGISTRY.npm) {
627
- lines.push(`${ICON.success} ${paint("bold", "npm")} https://www.npmjs.com/package/${context.state.packageName}/v/${version}`);
813
+ if (context.published && context.registryUrl) {
814
+ lines.push(`${ICON.success} ${paint("bold", "npm")} ${describePublishedRelease({ registryUrl: context.registryUrl, packageName: context.packageName, version })}`);
628
815
  }
629
816
 
630
817
  const githubRepository = GITHUB_REPOSITORY_PATTERN.exec(remoteUrl)?.[1];
@@ -701,12 +888,7 @@ export async function runCreateVersion({ repositoryRoot, argv }) {
701
888
  print(renderBanner({ projectName: config.projectName ?? state.packageName, publishedLabel }));
702
889
  print(renderDiagnosis(state, repositoryRoot));
703
890
 
704
- const plan = buildReleasePlan(state, {
705
- checks: config.checks.length > 0,
706
- prepare: config.prepare !== null,
707
- publish: config.publish !== null,
708
- publishTitle: config.publish === NPM_PUBLISHER ? "Publicar en npm" : "Publicar el release",
709
- });
891
+ const plan = buildReleasePlan(state, describeReleaseCapabilities(config));
710
892
 
711
893
  if (plan.mode === RELEASE_MODE.upToDate) {
712
894
  const since = state.lastRelease?.version ? toReleaseTag(state.lastRelease.version) : "el inicio";
@@ -742,7 +924,7 @@ export async function runCreateVersion({ repositoryRoot, argv }) {
742
924
  }
743
925
 
744
926
  /** @type {ReleaseContext} */
745
- const context = { repositoryRoot, config, state, options, reader, version: plan.pendingVersion, pushed: false, published: false, commitCount: null };
927
+ const context = { repositoryRoot, config, state, options, reader, version: plan.pendingVersion, pushed: false, published: false, commitCount: null, packageName: state.packageName, registryUrl: null };
746
928
 
747
929
  for (const [index, planStep] of plan.steps.entries()) {
748
930
  print(renderStepHeader(index + 1, plan.steps.length, planStep.title));
@@ -754,6 +936,18 @@ export async function runCreateVersion({ repositoryRoot, argv }) {
754
936
  return 0;
755
937
  }
756
938
 
939
+ // Not a failure: the run ends on purpose so the next one diagnoses the updated main.
940
+ if (error instanceof MainSyncedRestartError) {
941
+ print(
942
+ renderBox({
943
+ title: `${MAIN_BRANCH} actualizado`,
944
+ lines: [`${ICON.info} ${MAIN_SYNCED_RESTART_MESSAGE}`, "", paint("gray", "No se tocó la versión ni los tags.")],
945
+ tone: BOX_TONE.info,
946
+ })
947
+ );
948
+ return 0;
949
+ }
950
+
757
951
  const lines = [`${ICON.failure} ${error instanceof Error ? error.message : String(error)}`];
758
952
  if (error instanceof ReleaseStepError) {
759
953
  lines.push("", `${paint("bold", "Qué hacer:")} ${error.hint}`);
@@ -18,6 +18,7 @@ import { CHANGELOG_FILE } from "../constants/changelog.js";
18
18
  import {
19
19
  MAIN_BRANCH,
20
20
  MIGRATION_STATUS,
21
+ NPM_LOOKUP_STATUS,
21
22
  NO_PULL_REQUEST_MESSAGE_PATTERN,
22
23
  PACKAGE_MANIFEST_FILE,
23
24
  PULL_REQUEST_JSON_FIELDS,
@@ -25,7 +26,7 @@ import {
25
26
  REMOTE_MAIN_REF,
26
27
  VERSION_FIELD_CHANGE_PATTERN,
27
28
  } from "../constants/create-version.js";
28
- import { lookupPublishedVersions } from "./npm.js";
29
+ import { lookupPublishedVersions, resolvePublishRegistry } from "./npm.js";
29
30
  import { createGitReader, listCommits, readPackageVersionAt, runCaptured } from "./process.js";
30
31
 
31
32
  /**
@@ -132,6 +133,28 @@ async function readMigrations(checkMigrations) {
132
133
  }
133
134
  }
134
135
 
136
+ /**
137
+ * Lists the published versions on the registry the working-tree manifest publishes to, so the
138
+ * diagnosis sees the same versions `npm publish` would conflict with.
139
+ *
140
+ * @param {typeof lookupPublishedVersions} lookupNpm - npm lookup adapter.
141
+ * @param {Record<string, unknown>} manifest - Working-tree `package.json`.
142
+ * @param {string} repositoryRoot - Repository root.
143
+ * @returns {Promise<import("./npm.js").NpmLookup>} Published versions, or a failed lookup when the registry is invalid
144
+ * or npm cannot report it.
145
+ */
146
+ async function lookupNpmOnPublishRegistry(lookupNpm, manifest, repositoryRoot) {
147
+ /** @type {string} */
148
+ let registryUrl;
149
+ try {
150
+ registryUrl = await resolvePublishRegistry(manifest, repositoryRoot);
151
+ } catch (error) {
152
+ return { status: NPM_LOOKUP_STATUS.failed, publishedVersions: [], reason: error instanceof Error ? error.message : String(error) };
153
+ }
154
+
155
+ return lookupNpm(String(manifest.name), repositoryRoot, registryUrl);
156
+ }
157
+
135
158
  /**
136
159
  * Gathers the complete release snapshot.
137
160
  *
@@ -188,7 +211,7 @@ export async function collectReleaseState({
188
211
 
189
212
  if (trackNpm) {
190
213
  onProgress("Consultando npm");
191
- npm = await lookupNpm(manifest.name, repositoryRoot);
214
+ npm = await lookupNpmOnPublishRegistry(lookupNpm, manifest, repositoryRoot);
192
215
  }
193
216
 
194
217
  if (checkMigrations) {
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `prepublishOnly` guard of the Beez packages: they are published with npm by
3
+ * `pnpm create-version`, which verifies the tarball against
4
+ * `npm pack --dry-run`, so a `pnpm publish` (or yarn/bun) is blocked.
5
+ *
6
+ * The guard only blocks package managers it recognizes. Without a user agent,
7
+ * or with an unknown one, the publisher cannot be determined and it allows the
8
+ * publication. It can be bypassed on purpose with `--ignore-scripts`.
9
+ *
10
+ * @module guard-publish
11
+ */
12
+
13
+ import {
14
+ ALLOWED_PUBLISH_USER_AGENT_PREFIX,
15
+ BLOCKED_PUBLISH_PACKAGE_MANAGERS,
16
+ GUARD_PUBLISH_EXIT_CODE,
17
+ PACKAGE_MANAGER_USER_AGENT_VARIABLE,
18
+ } from "./constants/guard-publish.js";
19
+
20
+ /**
21
+ * @typedef {{ allowed: boolean, packageManager: string | null, message: string | null, exitCode: number }} PublishGuardDecision
22
+ */
23
+
24
+ /**
25
+ * Finds the blocked package manager that runs the publication.
26
+ *
27
+ * @param {string | undefined} userAgent - Value of `npm_config_user_agent`.
28
+ * @returns {string | null} Blocked package manager name, or `null` for npm, unknown or missing user agents.
29
+ */
30
+ export function findBlockedPublishPackageManager(userAgent) {
31
+ if (!userAgent || userAgent.startsWith(ALLOWED_PUBLISH_USER_AGENT_PREFIX)) {
32
+ return null;
33
+ }
34
+
35
+ const blockedEntry = Object.entries(BLOCKED_PUBLISH_PACKAGE_MANAGERS).find(([userAgentPrefix]) => userAgent.startsWith(userAgentPrefix));
36
+ return blockedEntry ? blockedEntry[1] : null;
37
+ }
38
+
39
+ /**
40
+ * Builds the explanation printed when a publication is blocked.
41
+ *
42
+ * @param {string} packageManager - Blocked package manager name.
43
+ * @returns {string} Message in Spanish with what to run instead.
44
+ */
45
+ export function buildBlockedPublishMessage(packageManager) {
46
+ return (
47
+ `beez-rp guard-publish: no publiques con ${packageManager}. Los paquetes Beez se publican con \`pnpm create-version\`, ` +
48
+ "que publica con npm y verifica el tarball contra `npm pack --dry-run`."
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Decides whether the publication may continue.
54
+ *
55
+ * @param {string | undefined} userAgent - Value of `npm_config_user_agent`.
56
+ * @returns {PublishGuardDecision} Decision, message to print on stderr and exit code.
57
+ */
58
+ export function decidePublishGuard(userAgent) {
59
+ const packageManager = findBlockedPublishPackageManager(userAgent);
60
+
61
+ return packageManager === null
62
+ ? { allowed: true, packageManager: null, message: null, exitCode: GUARD_PUBLISH_EXIT_CODE.allowed }
63
+ : { allowed: false, packageManager, message: buildBlockedPublishMessage(packageManager), exitCode: GUARD_PUBLISH_EXIT_CODE.blocked };
64
+ }
65
+
66
+ /**
67
+ * Decides with the user agent of an environment (the current process by default).
68
+ *
69
+ * @param {NodeJS.ProcessEnv} [environment] - Environment of the lifecycle script.
70
+ * @returns {PublishGuardDecision} Decision for its `npm_config_user_agent`.
71
+ */
72
+ export function decidePublishGuardForEnvironment(environment = process.env) {
73
+ return decidePublishGuard(environment[PACKAGE_MANAGER_USER_AGENT_VARIABLE]);
74
+ }
package/src/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * - `beez-rp/versions`: stable `X.Y.Z` rules; only the next patch, minor or major is allowed.
5
5
  * - `beez-rp/build-gate`: Vercel `ignoreCommand` decision built on those rules.
6
6
  * - `beez-rp/changelog` and `beez-rp/changelog-ai`: Keep a Changelog release and Codex fill-in.
7
+ * - `beez-rp/guard-publish`: `prepublishOnly` guard that blocks pnpm, yarn and bun publications.
7
8
  * - `beez-rp/terminal-ui`: boxes, spinners and prompts for release commands.
8
9
  * - `beez-rp/testing`: shared version bump fixtures for consumer test suites.
9
10
  * - `beez-rp/constants`: every constant above, grouped by domain.
@@ -14,6 +15,7 @@
14
15
  export * from "./build-gate.js";
15
16
  export * from "./changelog.js";
16
17
  export * from "./changelog-ai.js";
18
+ export * from "./guard-publish.js";
17
19
  export * from "./versions.js";
18
20
  export {
19
21
  ICON,
@@ -7,4 +7,5 @@
7
7
  export declare const CLI_COMMAND: Readonly<{
8
8
  ignoreBuild: "ignore-build";
9
9
  createVersion: "create-version";
10
+ guardPublish: "guard-publish";
10
11
  }>;
@@ -109,7 +109,125 @@ export declare const MAX_LISTED_COMMITS = 12;
109
109
  export declare const MAX_LISTED_ITEMS = 5;
110
110
  /** Length of the abbreviated commit ids shown to the user. */
111
111
  export declare const SHORT_SHA_LENGTH = 7;
112
+ /** Placeholders of the `artifact` pattern: the released version and the npm package name. */
113
+ export declare const ARTIFACT_VERSION_PLACEHOLDER = "{version}";
114
+ export declare const ARTIFACT_NAME_PLACEHOLDER = "{name}";
115
+ /**
116
+ * Scope of a package name as `npm pack` and `pnpm pack` name the tarball: the
117
+ * leading `@` is dropped and the `/` becomes `-` (`@scope/pkg` → `scope-pkg`).
118
+ * The `scope` group keeps the scope without `@`.
119
+ */
120
+ export declare const PACKAGE_SCOPE_PATTERN: RegExp;
121
+ /** Replacement of {@link PACKAGE_SCOPE_PATTERN} that yields the tarball base name prefix. */
122
+ export declare const PACKED_SCOPE_REPLACEMENT = "$<scope>-";
123
+ /** Wildcard of an `artifact` pattern segment, such as the checksum in `releases/{version}-*`. */
124
+ export declare const ARTIFACT_SEGMENT_WILDCARD = "*";
125
+ /**
126
+ * Checksum placeholder: the SHA-256 of the archive, verified before publishing. It may repeat,
127
+ * in one segment or several, and every occurrence must declare the same digest.
128
+ */
129
+ export declare const ARTIFACT_SHA256_PLACEHOLDER = "{sha256}";
130
+ /** Lowercase hexadecimal SHA-256 digest matched by {@link ARTIFACT_SHA256_PLACEHOLDER}. */
131
+ export declare const SHA256_HEX_PATTERN_SOURCE = "[0-9a-f]{64}";
132
+ /**
133
+ * Characters allowed in an artifact path passed to `npm publish` through the Windows shell;
134
+ * `~` is allowed because npm package names (and so tarball names) may contain it.
135
+ */
136
+ export declare const SAFE_ARTIFACT_PATH_PATTERN: RegExp;
137
+ /**
138
+ * Prefix that makes npm read an artifact path as a local file: a bare
139
+ * `releases/x/pkg.tgz` operand is parsed as a package spec (a GitHub shorthand).
140
+ */
141
+ export declare const LOCAL_PATH_PREFIX = "./";
142
+ /**
143
+ * Arguments of the `npm pack` run that reports the integrity npm would pack from the release
144
+ * checkout: nothing is written and no lifecycle script runs, so it only reads what `prepare` left.
145
+ */
146
+ export declare const NPM_PACK_DRY_RUN_ARGUMENTS: readonly string[];
147
+ /**
148
+ * Prefix of the temporary directory, outside the package root, that holds the prepared archive
149
+ * while `npm pack --dry-run` runs, so npm never packs the archive into the package it describes.
150
+ */
151
+ export declare const ARTIFACT_HOLDING_DIRECTORY_PREFIX = "beez-rp-artifact-";
152
+ /** Node.js error code of a `rename` across file systems, retried as copy and delete. */
153
+ export declare const CROSS_DEVICE_RENAME_ERROR_CODE = "EXDEV";
154
+ /** Hash algorithm of the npm `integrity` string (`sha512-<base64>`). */
155
+ export declare const NPM_INTEGRITY_ALGORITHM = "sha512";
156
+ /** npm `integrity` string as `npm pack --json` reports it. */
157
+ export declare const NPM_INTEGRITY_PATTERN: RegExp;
158
+ /** Manifest field whose keys npm applies as configuration when it publishes. */
159
+ export declare const PUBLISH_CONFIG_FIELD = "publishConfig";
160
+ /**
161
+ * `publishConfig` keys pnpm hoists onto the packed manifest root. npm treats every
162
+ * `publishConfig` key as npm configuration and never rewrites manifest fields with it, so a
163
+ * package that declares any of these would be published differently by npm than by `pnpm pack`.
164
+ * Any other key (`registry`, `access`, `tag`, `provenance`, `@scope:registry`, `otp`...) is npm
165
+ * configuration and is accepted.
166
+ *
167
+ * Source: `PUBLISH_CONFIG_WHITELIST` in pnpm v12.6.0
168
+ * (`pnpm/crates/exportable-manifest/src/create.rs`; same list as
169
+ * `releasing/exportable-manifest/src/overridePublishConfig.ts` in pnpm 11).
170
+ *
171
+ * @see https://github.com/pnpm/pnpm/blob/v12.6.0/pnpm/crates/exportable-manifest/src/create.rs
172
+ * @see https://docs.npmjs.com/cli/v11/configuring-npm/package-json#publishconfig
173
+ */
174
+ export declare const PNPM_HOISTED_PUBLISH_CONFIG_KEYS: readonly string[];
175
+ /** Dependency maps npm publishes as they are written in `package.json`. */
176
+ export declare const PUBLISHED_DEPENDENCY_FIELDS: readonly string[];
177
+ /**
178
+ * Dependency specifiers only `pnpm pack` rewrites and npm publishes verbatim, which npm then
179
+ * cannot install (`EUNSUPPORTEDPROTOCOL`):
180
+ * - `catalog:` (dereferenced to the catalog entry);
181
+ * - `workspace:` (resolved to the linked package version), as the whole specifier or, in
182
+ * `peerDependencies`, as a segment of a compound range (`^1.0.0 || workspace:>=1.0.0`);
183
+ * - `jsr:` (turned into an `npm:@jsr/<scope>__<name>` alias).
184
+ *
185
+ * Source: `convert_dependency_for_publish` (catalog → workspace → jsr replacers) in pnpm v12.6.0
186
+ * `pnpm/crates/exportable-manifest/src/create.rs`, and `replace_workspace_protocol_peer_dependency`
187
+ * in `pnpm/crates/exportable-manifest/src/replace.rs`. No other protocol is rewritten there.
188
+ *
189
+ * @see https://github.com/pnpm/pnpm/blob/v12.6.0/pnpm/crates/exportable-manifest/src/create.rs
190
+ * @see https://github.com/pnpm/pnpm/blob/v12.6.0/pnpm/crates/exportable-manifest/src/replace.rs
191
+ */
192
+ export declare const PNPM_PACK_REWRITTEN_SPECIFIER_PATTERN: RegExp;
193
+ /** Public npm registry: npm's default, used when no config sets `@scope:registry` nor `registry`. */
194
+ export declare const DEFAULT_NPM_REGISTRY_URL = "https://registry.npmjs.org/";
195
+ /** Prefix of a package page on npmjs.com, linked in the summary of a release published to {@link DEFAULT_NPM_REGISTRY_URL}. */
196
+ export declare const NPMJS_PACKAGE_PAGE_URL = "https://www.npmjs.com/package/";
197
+ /** `publishConfig` key of the registry for every package, used when no scope-specific registry applies. */
198
+ export declare const PUBLISH_CONFIG_REGISTRY_KEY = "registry";
199
+ /** Suffix of the scope-specific `publishConfig` registry key, as in `@scope:registry`. */
200
+ export declare const SCOPED_REGISTRY_KEY_SUFFIX = ":registry";
201
+ /**
202
+ * Registry URL allowed on the `npm view` command line built for the Windows shell: only characters
203
+ * without shell meaning (no `%`, `&`, `|`, `^`, quotes or spaces), after URL normalization.
204
+ */
205
+ export declare const SHELL_SAFE_REGISTRY_URL_PATTERN: RegExp;
206
+ /** npm option that selects the registry `npm view` queries. */
207
+ export declare const NPM_REGISTRY_OPTION = "--registry";
208
+ /** npm option that replaces the user config (`~/.npmrc`) with the temporary one. */
209
+ export declare const NPM_USER_CONFIG_OPTION = "--userconfig";
210
+ /** npm subcommand that prints the effective value of a config key. */
211
+ export declare const NPM_CONFIG_GET_ARGUMENTS: readonly string[];
212
+ /** What `npm config get` prints for a key no config source sets, such as an unused `@scope:registry`. */
213
+ export declare const NPM_UNSET_CONFIG_VALUE = "undefined";
214
+ /** Protocols a publish registry URL may use. */
215
+ export declare const NPM_REGISTRY_PROTOCOLS: readonly string[];
216
+ /**
217
+ * Value of the registry credential in the temporary npm config: npm expands `${NPM_TOKEN}` from
218
+ * the environment, so the token never reaches the disk or a command line.
219
+ */
220
+ export declare const NPM_AUTH_TOKEN_REFERENCE = "${NPM_TOKEN}";
221
+ /** Prefix of the temporary directory holding the publish-only npm user config. */
222
+ export declare const NPM_AUTH_DIRECTORY_PREFIX = "beez-rp-npm-auth-";
223
+ /** Characters that could break out of a quoted path on the Windows shell. */
224
+ export declare const UNSAFE_QUOTED_PATH_PATTERN: RegExp;
112
225
  /** Placeholder replaced by the released version in `summary` lines. */
113
226
  export declare const SUMMARY_VERSION_PLACEHOLDER = "{version}";
227
+ /**
228
+ * Shown when syncing `main` brought new commits: the run ends (exit code 0, it is not a failure)
229
+ * so the next one loads the updated configuration and diagnoses again.
230
+ */
231
+ export declare const MAIN_SYNCED_RESTART_MESSAGE = "main se actualiz\u00F3 desde origin: volv\u00E9 a correr pnpm create-version para diagnosticar con el c\u00F3digo y la configuraci\u00F3n nuevos.";
114
232
  /** Exit code of a release stopped by a failed step, invalid arguments or configuration. */
115
233
  export declare const FAILURE_EXIT_CODE = 1;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Contract of `beez-rp guard-publish`, the `prepublishOnly` guard that keeps
3
+ * Beez packages from being published by a package manager other than npm.
4
+ *
5
+ * @module constants/guard-publish
6
+ */
7
+ /**
8
+ * Environment variable package managers set for lifecycle scripts, for
9
+ * example `pnpm/12.6.0 npm/? node/v24.21.0 win32 x64`. npm also reads it as its
10
+ * `user-agent` config, so an inherited value survives `npm publish`.
11
+ */
12
+ export declare const PACKAGE_MANAGER_USER_AGENT_VARIABLE = "npm_config_user_agent";
13
+ /** User agent prefix of npm, the only package manager allowed to publish. */
14
+ export declare const ALLOWED_PUBLISH_USER_AGENT_PREFIX = "npm/";
15
+ /** Package managers whose `publish` is blocked, keyed by their user agent prefix. */
16
+ export declare const BLOCKED_PUBLISH_PACKAGE_MANAGERS: Readonly<{
17
+ "pnpm/": "pnpm";
18
+ "yarn/": "yarn";
19
+ "bun/": "bun";
20
+ }>;
21
+ /** Exit codes of `beez-rp guard-publish`. */
22
+ export declare const GUARD_PUBLISH_EXIT_CODE: Readonly<{
23
+ allowed: 0;
24
+ blocked: 1;
25
+ }>;
@@ -7,6 +7,7 @@ export * from "./build-gate.js";
7
7
  export * from "./changelog.js";
8
8
  export * from "./changelog-ai.js";
9
9
  export * from "./cli.js";
10
+ export * from "./guard-publish.js";
10
11
  export * from "./terminal-ui.js";
11
12
  export * from "./versions.js";
12
13
  export * from "./create-version.js";