beez-rp 0.1.1 → 0.2.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/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y e
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.0] - 2026-09-26
10
+
11
+ ### Added
12
+
13
+ - `beez-rp create-version` (y `beez-rp/create-version`): el comando de release compartido por todos los proyectos, configurable con `beez-rp.config.js` (checks, migraciones, preparación, publicación en npm o propia, textos y resumen).
14
+ - Retoma en un mismo flujo el push de un commit de versión local y la publicación de una versión que npm todavía no tiene.
15
+ - `buildChangelogPrompt` acepta el idioma de las entradas: español por defecto o inglés limitado a ASCII.
16
+
17
+ ### Changed
18
+
19
+ - El último release es el último commit de `origin/main` que cambió la versión de `package.json`, así que también reconoce releases con otros asuntos o versiones subidas a mano.
20
+ - beez-rp se publica con su propio `beez-rp create-version` en lugar de un script aparte.
21
+
9
22
  ## [0.1.1] - 2026-09-26
10
23
 
11
24
  ### Added
package/README.md CHANGED
@@ -17,6 +17,7 @@ Proceso de release compartido por los proyectos Beez (beez-ui, TuTribu, Control
17
17
  | `beez-rp/changelog` | Lectura y release del bloque `## [Unreleased]` de `CHANGELOG.md` (Keep a Changelog). |
18
18
  | `beez-rp/changelog-ai` | Prompt e invocación de Codex para completar `[Unreleased]` vacío. |
19
19
  | `beez-rp/terminal-ui` | Cajas, filas, banner, spinner y selector interactivo sin dependencias. |
20
+ | `beez-rp/create-version` | Comando compartido de release: `runCreateVersion`, el planificador puro `buildReleasePlan`, el lector de estado y los tipos de `beez-rp.config.js`. |
20
21
  | `beez-rp/testing` | Fixtures de versiones permitidas y rechazadas para los tests de cada proyecto. |
21
22
  | `beez-rp/constants` | Todas las constantes, agrupadas por dominio. |
22
23
 
@@ -54,7 +55,13 @@ it.each(REJECTED_VERSION_BUMP_CASES)("rechaza %s (%j)", (_reason, version) => {
54
55
  });
55
56
  ```
56
57
 
57
- ## Publicar beez-rp
58
+ ## create-version
59
+
60
+ Cada proyecto versiona con el mismo comando y describe sus diferencias en `beez-rp.config.js` (o `beez-rp.config.mjs`, que tiene prioridad y conviene en proyectos sin `"type": "module"`):
61
+
62
+ ```json
63
+ { "scripts": { "create-version": "beez-rp create-version", "cv": "beez-rp create-version" } }
64
+ ```
58
65
 
59
66
  ```bash
60
67
  pnpm create-version # o pnpm cv
@@ -63,15 +70,46 @@ pnpm cv --set-version X.Y.Z # solo la siguiente patch, minor o major
63
70
  pnpm cv --dry-run # diagnóstico y plan, sin cambiar nada
64
71
  ```
65
72
 
66
- El comando sale solo desde `main`, limpio y al día con origin (solo `CHANGELOG.md` puede quedar sin commitear).
73
+ El comando sale solo desde `main`, limpio y al día con origin (solo `CHANGELOG.md` puede quedar sin commitear). En una rama feature explica qué falta: pushear, abrir o mergear el PR (con `gh`).
74
+
75
+ 1. Actualiza `main` desde origin si está atrás.
76
+ 2. Aplica migraciones pendientes, si el proyecto tiene adaptador, después de pedir confirmación.
77
+ 3. Si `[Unreleased]` está vacío, lo completa Codex a partir de los commits sin publicar.
78
+ 4. Corre los `checks`.
79
+ 5. Pide la versión, pasa `[Unreleased]` a `## [X.Y.Z] - AAAA-MM-DD` y crea el commit `X.Y.Z` con el tag anotado `vX.Y.Z`.
80
+ 6. Corre `prepare`, sube `main` y el tag con `git push --atomic` y corre `publish`.
81
+
82
+ El último release es el último commit de `origin/main` que cambió el `version` de `package.json`, así que sirve con commits `X.Y.Z`, con otros asuntos de release y con versiones subidas a mano. Si algo falla después del commit, volver a correr el comando retoma solo lo que falta: el push de un commit de versión local o, con `registry: "npm"`, la preparación y publicación de una versión que npm todavía no tiene.
83
+
84
+ ### beez-rp.config.js
85
+
86
+ ```js
87
+ /** @type {import("beez-rp/create-version").CreateVersionConfig} */
88
+ export default {
89
+ projectName: "TuTribu", // banner; por defecto el name de package.json
90
+ changelog: { audience: "quien usa TuTribu", language: "es" }, // "en": entradas en inglés ASCII
91
+ releaseTypeDescriptions: { patch: "…", minor: "…", major: "…" },
92
+ publishedLabel: "en producción", // banner: vX.Y.Z en producción
93
+ registry: "npm", // retoma y banner según las versiones en npm
94
+ checks: ["pnpm check"], // antes de tocar la versión
95
+ migrations: { check, apply, targetHint }, // adaptador de base de datos
96
+ prepare: ["pnpm release:prepare"], // comandos o función, sobre el commit de versión
97
+ publish: "npm", // npm publish con NPM_TOKEN, o una función
98
+ summary: ["Vercel buildea {version}."], // líneas extra del resumen final
99
+ };
100
+ ```
101
+
102
+ Solo `changelog.audience` es obligatorio. Los hooks (`migrations.check`, `migrations.apply`, `prepare`, `publish`) reciben `{ repositoryRoot, version, git, run, print, fail }`: `git` lee Git, `run("pnpm x")` corre un comando visible y devuelve su exit code, y `fail(mensaje, qué hacer)` corta el paso con una explicación. El config no necesita importar `beez-rp`.
103
+
104
+ `migrations.check` devuelve `{ status: "up-to-date" | "pending" | "unknown", pending, target, reason }`; después de `apply`, el comando vuelve a llamar a `check` y falla si siguen pendientes. `publish: "npm"` toma `NPM_TOKEN` del entorno o de un `.env` ignorado por Git, referenciado por el `.npmrc` del repo.
105
+
106
+ ## Publicar beez-rp
67
107
 
68
- 1. Si `[Unreleased]` está vacío, lo completa Codex a partir de los commits sin publicar.
69
- 2. Corre `pnpm check`.
70
- 3. Pide la versión, pasa `[Unreleased]` a `## [X.Y.Z] - AAAA-MM-DD` y crea el commit `X.Y.Z` con el tag `vX.Y.Z`.
71
- 4. Sube `main` y el tag con `git push --atomic`.
72
- 5. Publica en npm. El `.npmrc` del repo referencia `${NPM_TOKEN}`, que se toma del entorno o de un `.env` ignorado por Git.
108
+ beez-rp se publica con su propio comando (`beez-rp.config.js`: `checks: ["pnpm check"]`, `publish: "npm"`):
73
109
 
74
- Si algo falla después del commit, volver a correr el comando retoma solo el push o la publicación, sin generar otra versión.
110
+ ```bash
111
+ pnpm cv
112
+ ```
75
113
 
76
114
  ## Desarrollo
77
115
 
package/bin/beez-rp.js CHANGED
@@ -2,11 +2,14 @@
2
2
  /**
3
3
  * `beez-rp` command line.
4
4
  *
5
- * `beez-rp ignore-build` prints why the current checkout is built or skipped
6
- * and, as its last line, the decision (`BUILD` or `SKIP`), exiting with `0`.
7
- * It exits with `2` when it cannot decide. Vercel wrappers must build only
8
- * when the last line is `BUILD`, so any failure (including `npx` itself)
9
- * skips the build.
5
+ * - `beez-rp ignore-build` prints why the current checkout is built or skipped
6
+ * and, as its last line, the decision (`BUILD` or `SKIP`), exiting with `0`.
7
+ * It exits with `2` when it cannot decide. Vercel wrappers must build only
8
+ * when the last line is `BUILD`, so any failure (including `npx` itself)
9
+ * skips the build.
10
+ * - `beez-rp create-version [--bump patch|minor|major | --set-version X.Y.Z] [--dry-run]`
11
+ * diagnoses the repository in the current directory and ships its release
12
+ * from `main`, following its `beez-rp.config.js`.
10
13
  *
11
14
  * @module beez-rp-cli
12
15
  */
@@ -14,16 +17,16 @@
14
17
  import { decideBuildForCheckout } from "../src/build-gate.js";
15
18
  import { BUILD_DECISION, DECISION_EXIT_CODE, GATE_FAILURE_EXIT_CODE } from "../src/constants/build-gate.js";
16
19
  import { CLI_COMMAND } from "../src/constants/cli.js";
20
+ import { runCreateVersion } from "../src/create-version/run.js";
17
21
 
18
22
  /** Usage printed for unknown commands. */
19
- const USAGE = `Usage: beez-rp ${CLI_COMMAND.ignoreBuild}`;
23
+ const USAGE = `Usage: beez-rp ${CLI_COMMAND.ignoreBuild} | beez-rp ${CLI_COMMAND.createVersion} [options]`;
20
24
 
21
- const [command] = process.argv.slice(2);
25
+ const [command, ...commandArguments] = process.argv.slice(2);
22
26
 
23
- if (command !== CLI_COMMAND.ignoreBuild) {
24
- console.error(`beez-rp: unknown command "${command ?? ""}". ${USAGE}`);
25
- process.exitCode = GATE_FAILURE_EXIT_CODE;
26
- } else {
27
+ if (command === CLI_COMMAND.createVersion) {
28
+ process.exitCode = await runCreateVersion({ repositoryRoot: process.cwd(), argv: commandArguments });
29
+ } else if (command === CLI_COMMAND.ignoreBuild) {
27
30
  try {
28
31
  const decision = decideBuildForCheckout(process.cwd());
29
32
  console.log(decision.reason);
@@ -33,4 +36,7 @@ if (command !== CLI_COMMAND.ignoreBuild) {
33
36
  console.error("beez-rp ignore-build: could not decide; the build must be skipped.", error);
34
37
  process.exitCode = GATE_FAILURE_EXIT_CODE;
35
38
  }
39
+ } else {
40
+ console.error(`beez-rp: unknown command "${command ?? ""}". ${USAGE}`);
41
+ process.exitCode = GATE_FAILURE_EXIT_CODE;
36
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beez-rp",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Dependency-free release process shared by the Beez projects: stable version rules, Vercel build gate, Keep a Changelog and terminal UI.",
5
5
  "license": "MIT",
6
6
  "author": "Guido Modarelli",
@@ -46,6 +46,10 @@
46
46
  "types": "./types/testing.d.ts",
47
47
  "default": "./src/testing.js"
48
48
  },
49
+ "./create-version": {
50
+ "types": "./types/create-version/index.d.ts",
51
+ "default": "./src/create-version/index.js"
52
+ },
49
53
  "./constants": {
50
54
  "types": "./types/constants/index.d.ts",
51
55
  "default": "./src/constants/index.js"
@@ -65,8 +69,8 @@
65
69
  "typecheck": "tsc -p tsconfig.json",
66
70
  "test": "vitest run",
67
71
  "check": "pnpm typecheck && pnpm test",
68
- "create-version": "node scripts/release.js",
69
- "cv": "node scripts/release.js",
72
+ "create-version": "node bin/beez-rp.js create-version",
73
+ "cv": "node bin/beez-rp.js create-version",
70
74
  "prepack": "pnpm build:types"
71
75
  },
72
76
  "devDependencies": {
@@ -11,16 +11,33 @@ import { spawn } from "node:child_process";
11
11
 
12
12
  import { CHANGE_TYPES, UNRELEASED_HEADING } from "./constants/changelog.js";
13
13
  import { CODEX_COMMAND, CODEX_NOT_FOUND_EXIT_CODE, PROMPT_SHORT_SHA_LENGTH } from "./constants/changelog-ai.js";
14
+ import { CHANGELOG_LANGUAGE } from "./constants/create-version.js";
14
15
 
15
16
  /**
16
17
  * Builds the instructions for Codex from the commits that will ship.
17
18
  *
18
19
  * @param {{ sha: string, subject: string }[]} commits - Unreleased commits, newest first.
19
20
  * @param {string} audience - Who reads the changelog, e.g. "quien consume el paquete".
20
- * @returns {string} Prompt in Spanish.
21
+ * @param {"es" | "en"} [language] - Language of the entries: Spanish by default, or English limited to ASCII.
22
+ * @returns {string} Prompt written in the requested language.
21
23
  */
22
- export function buildChangelogPrompt(commits, audience) {
24
+ export function buildChangelogPrompt(commits, audience, language = CHANGELOG_LANGUAGE.spanish) {
23
25
  const commitList = commits.map((commit) => `- ${commit.sha.slice(0, PROMPT_SHORT_SHA_LENGTH)} ${commit.subject}`).join("\n");
26
+
27
+ if (language === CHANGELOG_LANGUAGE.english) {
28
+ return [
29
+ `Fill the \`${UNRELEASED_HEADING}\` block of CHANGELOG.md following Keep a Changelog.`,
30
+ `- Group entries under \`### ${CHANGE_TYPES.join("`, `### ")}\`, in that order and only the sections that apply.`,
31
+ `- One \`- \` line per change, in English and ASCII only, clear for ${audience}; no internal implementation details.`,
32
+ `- If \`${UNRELEASED_HEADING}\` does not exist, create it right below the document title.`,
33
+ "- Modify only CHANGELOG.md: do not touch released versions or other files, and do not commit.",
34
+ "- Use `git show <sha>` when you need the details of a commit.",
35
+ "",
36
+ "Unreleased commits (newest first):",
37
+ commitList,
38
+ ].join("\n");
39
+ }
40
+
24
41
  return [
25
42
  `Completá el bloque \`${UNRELEASED_HEADING}\` de CHANGELOG.md siguiendo Keep a Changelog.`,
26
43
  `- Agrupá las entradas bajo \`### ${CHANGE_TYPES.join("`, `### ")}\`, en ese orden y solo las secciones que apliquen.`,
@@ -7,4 +7,5 @@
7
7
  /** Command names accepted as the first `beez-rp` argument. */
8
8
  export const CLI_COMMAND = Object.freeze({
9
9
  ignoreBuild: "ignore-build",
10
+ createVersion: "create-version",
10
11
  });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Git, npm, configuration and display values of the shared `beez-rp create-version` command.
3
+ *
4
+ * @module constants/create-version
5
+ */
6
+
7
+ /** Branch that receives releases. */
8
+ export const MAIN_BRANCH = "main";
9
+
10
+ /** Remote that receives releases. */
11
+ export const RELEASE_REMOTE = "origin";
12
+
13
+ /** Remote-tracking ref of the release branch. */
14
+ export const REMOTE_MAIN_REF = `${RELEASE_REMOTE}/${MAIN_BRANCH}`;
15
+
16
+ /** Configuration module read from the repository root. */
17
+ export const CREATE_VERSION_CONFIG_FILE = "beez-rp.config.js";
18
+
19
+ /**
20
+ * Configuration candidates in lookup order: `.mjs` is always ESM, so projects
21
+ * without `"type": "module"` load it without Node warnings.
22
+ */
23
+ export const CREATE_VERSION_CONFIG_FILES = Object.freeze(["beez-rp.config.mjs", CREATE_VERSION_CONFIG_FILE]);
24
+
25
+ /** Manifest whose `version` is released; shared with the build gate. */
26
+ export { PACKAGE_MANIFEST_FILE } from "./build-gate.js";
27
+
28
+ /** Pinned Node.js version compared with the running one when present; a full version or only its major. */
29
+ export const PINNED_NODE_VERSION_FILE = ".nvmrc";
30
+
31
+ /** Environment file that may hold `NPM_TOKEN` locally; ignored by Git. */
32
+ export const LOCAL_ENVIRONMENT_FILE = ".env";
33
+
34
+ /** Stable identifiers of every step the command knows how to run, in execution order. */
35
+ export const RELEASE_STEP = Object.freeze({
36
+ syncMain: "sync-main",
37
+ applyMigrations: "apply-migrations",
38
+ generateChangelog: "generate-changelog",
39
+ runChecks: "run-checks",
40
+ bumpVersion: "bump-version",
41
+ prepareRelease: "prepare-release",
42
+ pushRelease: "push-release",
43
+ publishRelease: "publish-release",
44
+ });
45
+
46
+ /** What the command does with the current state. */
47
+ export const RELEASE_MODE = Object.freeze({
48
+ newRelease: "new-release",
49
+ resume: "resume",
50
+ upToDate: "up-to-date",
51
+ blocked: "blocked",
52
+ });
53
+
54
+ /** Result of a project migrations adapter. */
55
+ export const MIGRATION_STATUS = Object.freeze({
56
+ upToDate: "up-to-date",
57
+ pending: "pending",
58
+ unknown: "unknown",
59
+ });
60
+
61
+ /** Pull request states reported by `gh pr view --json state`. */
62
+ export const PULL_REQUEST_STATE = Object.freeze({
63
+ open: "OPEN",
64
+ merged: "MERGED",
65
+ closed: "CLOSED",
66
+ });
67
+
68
+ /** Fields requested from `gh pr view`. */
69
+ export const PULL_REQUEST_JSON_FIELDS = "number,url,title,state,isDraft,headRefOid";
70
+
71
+ /** Message `gh pr view` prints when the branch has no pull request. */
72
+ export const NO_PULL_REQUEST_MESSAGE_PATTERN = /no pull requests found/iu;
73
+
74
+ /** Registries whose published versions drive resumes and the banner. */
75
+ export const RELEASE_REGISTRY = Object.freeze({
76
+ npm: "npm",
77
+ });
78
+
79
+ /** Built-in publisher selected with `publish: "npm"`. */
80
+ export const NPM_PUBLISHER = "npm";
81
+
82
+ /** Result of asking npm which versions of the package exist. */
83
+ export const NPM_LOOKUP_STATUS = Object.freeze({
84
+ ok: "ok",
85
+ failed: "failed",
86
+ });
87
+
88
+ /** Valid npm package name (optionally scoped), checked before it reaches a shell command line. */
89
+ export const NPM_PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/u;
90
+
91
+ /** npm error code of a package that was never published. */
92
+ export const NPM_NOT_FOUND_CODE = "E404";
93
+
94
+ /** Environment variable that holds the npm token; the repository `.npmrc` references it as `${NPM_TOKEN}`. */
95
+ export const NPM_TOKEN_VARIABLE = "NPM_TOKEN";
96
+
97
+ /** npm dist-tag every stable release is published under. */
98
+ export const NPM_DIST_TAG = "latest";
99
+
100
+ /** Languages the Codex changelog prompt can request. */
101
+ export const CHANGELOG_LANGUAGE = Object.freeze({
102
+ spanish: "es",
103
+ english: "en",
104
+ });
105
+
106
+ /** Command-line flags of `beez-rp create-version`. */
107
+ export const CREATE_VERSION_FLAG = Object.freeze({
108
+ bump: "bump",
109
+ setVersion: "set-version",
110
+ dryRun: "dry-run",
111
+ help: "help",
112
+ helpShort: "h",
113
+ endOfOptions: "--",
114
+ });
115
+
116
+ /** Optional `v` prefix accepted in hand-typed versions. */
117
+ export const VERSION_PREFIX_PATTERN = /^v/u;
118
+
119
+ /** `version` field of `package.json`, replaced in place to keep formatting. */
120
+ export const PACKAGE_VERSION_FIELD_PATTERN = /("version"\s*:\s*")[^"]+(")/u;
121
+
122
+ /** `git log -G` pattern of a changed top-level `version` field; the last such commit is the last release. */
123
+ export const VERSION_FIELD_CHANGE_PATTERN = `^[[:space:]]*"version"[[:space:]]*:`;
124
+
125
+ /** GitHub `owner/repo` inside an SSH or HTTPS remote URL. */
126
+ export const GITHUB_REPOSITORY_PATTERN = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/u;
127
+
128
+ /** Separates fields inside one `git log --format` record. */
129
+ export const FIELD_SEPARATOR = "\x1f";
130
+
131
+ /** Separates records in `git log --format` output. */
132
+ export const RECORD_SEPARATOR = "\x1e";
133
+
134
+ /** Width of the `git status --porcelain` state columns before each path. */
135
+ export const PORCELAIN_STATUS_WIDTH = 3;
136
+
137
+ /** Maximum commits listed in a box. */
138
+ export const MAX_LISTED_COMMITS = 12;
139
+
140
+ /** Maximum uncommitted files, foreign commits or pending migrations listed before summarizing. */
141
+ export const MAX_LISTED_ITEMS = 5;
142
+
143
+ /** Length of the abbreviated commit ids shown to the user. */
144
+ export const SHORT_SHA_LENGTH = 7;
145
+
146
+ /** Placeholder replaced by the released version in `summary` lines. */
147
+ export const SUMMARY_VERSION_PLACEHOLDER = "{version}";
148
+
149
+ /** Exit code of a release stopped by a failed step, invalid arguments or configuration. */
150
+ export const FAILURE_EXIT_CODE = 1;
@@ -10,3 +10,4 @@ export * from "./changelog-ai.js";
10
10
  export * from "./cli.js";
11
11
  export * from "./terminal-ui.js";
12
12
  export * from "./versions.js";
13
+ export * from "./create-version.js";
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Loads and validates `beez-rp.config.js`, the per-project part of
3
+ * `beez-rp create-version`: changelog audience and language, version
4
+ * descriptions, registry, checks, migrations, preparation and publication.
5
+ *
6
+ * The configuration never needs to import `beez-rp`: hooks receive every
7
+ * helper through their context, so a project can type it with
8
+ * `@type {import("beez-rp/create-version").CreateVersionConfig}` only.
9
+ *
10
+ * @module create-version/config
11
+ */
12
+
13
+ import { existsSync } from "node:fs";
14
+ import path from "node:path";
15
+ import { pathToFileURL } from "node:url";
16
+
17
+ import {
18
+ CHANGELOG_LANGUAGE,
19
+ CREATE_VERSION_CONFIG_FILE,
20
+ CREATE_VERSION_CONFIG_FILES,
21
+ NPM_PUBLISHER,
22
+ RELEASE_REGISTRY,
23
+ } from "../constants/create-version.js";
24
+ import { RELEASE_TYPE_ORDER } from "../constants/versions.js";
25
+
26
+ /**
27
+ * @typedef {import("./process.js").GitReader} GitReader
28
+ * @typedef {"patch" | "minor" | "major"} ReleaseType
29
+ * @typedef {{ status: "up-to-date" | "pending" | "unknown", pending: string[], target: string | null, reason: string | null }} MigrationCheck
30
+ * @typedef {{
31
+ * repositoryRoot: string,
32
+ * version: string | null,
33
+ * git: GitReader,
34
+ * run: (commandLine: string) => Promise<number>,
35
+ * print: (text?: string) => void,
36
+ * fail: (message: string, hint: string) => never,
37
+ * }} HookContext
38
+ * @typedef {(context: HookContext) => Promise<void> | void} ReleaseHook
39
+ * @typedef {{
40
+ * check: (context: HookContext) => Promise<MigrationCheck> | MigrationCheck,
41
+ * apply: (context: HookContext) => Promise<void> | void,
42
+ * targetHint?: string,
43
+ * }} MigrationsAdapter
44
+ * @typedef {{
45
+ * projectName?: string,
46
+ * changelog: { audience: string, language?: "es" | "en" },
47
+ * releaseTypeDescriptions?: Partial<Record<ReleaseType, string>>,
48
+ * registry?: "npm" | null,
49
+ * publishedLabel?: string,
50
+ * checks?: string[],
51
+ * migrations?: MigrationsAdapter | null,
52
+ * prepare?: string[] | ReleaseHook | null,
53
+ * publish?: "npm" | ReleaseHook | null,
54
+ * summary?: string[],
55
+ * }} CreateVersionConfig
56
+ * `summary` lines replace `{version}` with the released version.
57
+ * @typedef {{
58
+ * projectName: string | null,
59
+ * changelog: { audience: string, language: "es" | "en" },
60
+ * releaseTypeDescriptions: Record<ReleaseType, string>,
61
+ * registry: "npm" | null,
62
+ * publishedLabel: string,
63
+ * checks: string[],
64
+ * migrations: MigrationsAdapter | null,
65
+ * prepare: string[] | ReleaseHook | null,
66
+ * publish: "npm" | ReleaseHook | null,
67
+ * summary: string[],
68
+ * }} ResolvedCreateVersionConfig
69
+ */
70
+
71
+ /** What each release type means when a project does not describe it. */
72
+ const DEFAULT_RELEASE_TYPE_DESCRIPTIONS = Object.freeze({
73
+ patch: "Solo arreglos o cambios internos; nada nuevo para quien lo usa.",
74
+ minor: "Funcionalidades nuevas compatibles; lo existente sigue funcionando igual.",
75
+ major: "Cambio incompatible: quien lo usa tiene que adaptarse.",
76
+ });
77
+
78
+ /** Banner suffix of the version on `origin/main` when a project does not name its environment. */
79
+ const DEFAULT_PUBLISHED_LABEL = "publicada";
80
+
81
+ /**
82
+ * Types a configuration in editors; returns it unchanged.
83
+ *
84
+ * @param {CreateVersionConfig} config - Project configuration.
85
+ * @returns {CreateVersionConfig} The same configuration.
86
+ */
87
+ export function defineCreateVersionConfig(config) {
88
+ return config;
89
+ }
90
+
91
+ /**
92
+ * Builds a configuration error that names the file and the invalid field.
93
+ *
94
+ * @param {string} field - Offending field path.
95
+ * @param {string} expectation - What the field must be.
96
+ * @returns {Error} Error to throw.
97
+ */
98
+ function invalidField(field, expectation) {
99
+ return new Error(`beez-rp create-version: ${CREATE_VERSION_CONFIG_FILE}: ${field} must be ${expectation}`);
100
+ }
101
+
102
+ /**
103
+ * Checks that a value is an array of non-empty strings.
104
+ *
105
+ * @param {unknown} value - Candidate.
106
+ * @returns {value is string[]} Whether it is a string list.
107
+ */
108
+ function isStringList(value) {
109
+ return Array.isArray(value) && value.every((item) => typeof item === "string" && item.trim().length > 0);
110
+ }
111
+
112
+ /**
113
+ * Validates a raw configuration and fills its defaults.
114
+ *
115
+ * @param {unknown} rawConfig - Default export of `beez-rp.config.js`.
116
+ * @returns {ResolvedCreateVersionConfig} Configuration ready for the command.
117
+ * @throws {Error} When a field has an unsupported type or value.
118
+ */
119
+ export function resolveCreateVersionConfig(rawConfig) {
120
+ if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
121
+ throw invalidField("the default export", "an object");
122
+ }
123
+
124
+ const config = /** @type {Record<string, unknown>} */ (rawConfig);
125
+ const changelog = /** @type {Record<string, unknown> | undefined} */ (config.changelog);
126
+
127
+ if (!changelog || typeof changelog.audience !== "string" || changelog.audience.trim() === "") {
128
+ throw invalidField("changelog.audience", "a non-empty string describing who reads the changelog");
129
+ }
130
+
131
+ const language = changelog.language ?? CHANGELOG_LANGUAGE.spanish;
132
+ if (!(/** @type {readonly unknown[]} */ (Object.values(CHANGELOG_LANGUAGE))).includes(language)) {
133
+ throw invalidField("changelog.language", `one of ${Object.values(CHANGELOG_LANGUAGE).join(", ")}`);
134
+ }
135
+
136
+ if (config.projectName !== undefined && (typeof config.projectName !== "string" || config.projectName.trim() === "")) {
137
+ throw invalidField("projectName", "a non-empty string");
138
+ }
139
+
140
+ const descriptions = config.releaseTypeDescriptions ?? {};
141
+ if (typeof descriptions !== "object" || descriptions === null || Array.isArray(descriptions)) {
142
+ throw invalidField("releaseTypeDescriptions", "an object keyed by patch, minor and major");
143
+ }
144
+ for (const [releaseType, description] of Object.entries(descriptions)) {
145
+ if (!(/** @type {readonly string[]} */ (RELEASE_TYPE_ORDER)).includes(releaseType) || typeof description !== "string") {
146
+ throw invalidField(`releaseTypeDescriptions.${releaseType}`, "a string for patch, minor or major");
147
+ }
148
+ }
149
+
150
+ const publish = config.publish ?? null;
151
+ if (publish !== null && publish !== NPM_PUBLISHER && typeof publish !== "function") {
152
+ throw invalidField("publish", `"${NPM_PUBLISHER}", a function or null`);
153
+ }
154
+
155
+ const registry = config.registry ?? (publish === NPM_PUBLISHER ? RELEASE_REGISTRY.npm : null);
156
+ if (registry !== null && !(/** @type {readonly unknown[]} */ (Object.values(RELEASE_REGISTRY))).includes(registry)) {
157
+ throw invalidField("registry", `one of ${Object.values(RELEASE_REGISTRY).join(", ")} or null`);
158
+ }
159
+
160
+ const checks = config.checks ?? [];
161
+ if (!isStringList(checks)) {
162
+ throw invalidField("checks", "a list of command lines");
163
+ }
164
+
165
+ const prepare = config.prepare ?? null;
166
+ if (prepare !== null && typeof prepare !== "function" && !isStringList(prepare)) {
167
+ throw invalidField("prepare", "a list of command lines, a function or null");
168
+ }
169
+
170
+ const migrations = /** @type {MigrationsAdapter | null} */ (config.migrations ?? null);
171
+ if (migrations !== null && (typeof migrations.check !== "function" || typeof migrations.apply !== "function")) {
172
+ throw invalidField("migrations", "an object with check and apply functions, or null");
173
+ }
174
+ if (migrations?.targetHint !== undefined && typeof migrations.targetHint !== "string") {
175
+ throw invalidField("migrations.targetHint", "a string");
176
+ }
177
+
178
+ const summary = config.summary ?? [];
179
+ if (!isStringList(summary)) {
180
+ throw invalidField("summary", "a list of lines");
181
+ }
182
+
183
+ if (config.publishedLabel !== undefined && typeof config.publishedLabel !== "string") {
184
+ throw invalidField("publishedLabel", "a string");
185
+ }
186
+
187
+ return {
188
+ projectName: /** @type {string | undefined} */ (config.projectName) ?? null,
189
+ changelog: { audience: changelog.audience, language: /** @type {"es" | "en"} */ (language) },
190
+ releaseTypeDescriptions: { ...DEFAULT_RELEASE_TYPE_DESCRIPTIONS, .../** @type {Partial<Record<ReleaseType, string>>} */ (descriptions) },
191
+ registry: /** @type {"npm" | null} */ (registry),
192
+ publishedLabel: /** @type {string | undefined} */ (config.publishedLabel) ?? DEFAULT_PUBLISHED_LABEL,
193
+ checks,
194
+ migrations,
195
+ prepare: /** @type {string[] | ReleaseHook | null} */ (prepare),
196
+ publish: /** @type {"npm" | ReleaseHook | null} */ (publish),
197
+ summary,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Imports `beez-rp.config.mjs` or `beez-rp.config.js` from the repository root and validates it.
203
+ *
204
+ * @param {string} repositoryRoot - Repository root.
205
+ * @returns {Promise<ResolvedCreateVersionConfig>} Resolved configuration.
206
+ * @throws {Error} When the file is missing, fails to load or is invalid.
207
+ */
208
+ export async function loadCreateVersionConfig(repositoryRoot) {
209
+ const configPath = CREATE_VERSION_CONFIG_FILES.map((fileName) => path.join(repositoryRoot, fileName)).find((candidate) => existsSync(candidate));
210
+
211
+ if (!configPath) {
212
+ throw new Error(
213
+ `beez-rp create-version: ${CREATE_VERSION_CONFIG_FILES.join(" or ")} not found in ${repositoryRoot}; create it with at least changelog.audience`
214
+ );
215
+ }
216
+
217
+ let module;
218
+ try {
219
+ module = await import(pathToFileURL(configPath).href);
220
+ } catch (error) {
221
+ throw new Error(`beez-rp create-version: could not load ${configPath}`, { cause: error });
222
+ }
223
+
224
+ return resolveCreateVersionConfig(module.default);
225
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Errors of `beez-rp create-version`.
3
+ *
4
+ * @module create-version/errors
5
+ */
6
+
7
+ /** Failure of a release step or hook, with a Spanish explanation and the next action to take. */
8
+ export class ReleaseStepError extends Error {
9
+ /**
10
+ * @param {string} message - What failed, in Spanish.
11
+ * @param {string} hint - What to do next, in Spanish.
12
+ * @param {{ cause?: unknown }} [options] - Original error, when wrapping one.
13
+ */
14
+ constructor(message, hint, options) {
15
+ super(message, options);
16
+ this.name = "ReleaseStepError";
17
+ this.hint = hint;
18
+ }
19
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `beez-rp/create-version`: shared release command of the Beez projects.
3
+ *
4
+ * Projects run it as `beez-rp create-version` and describe their differences
5
+ * in `beez-rp.config.js` (changelog audience and language, checks, migrations,
6
+ * preparation, publication). The pure planner and the state reader are
7
+ * exported for project tests and custom tooling.
8
+ *
9
+ * @module create-version
10
+ */
11
+
12
+ export { defineCreateVersionConfig, loadCreateVersionConfig, resolveCreateVersionConfig } from "./config.js";
13
+ export { ReleaseStepError } from "./errors.js";
14
+ export { lookupPublishedVersions, publishToNpm } from "./npm.js";
15
+ export { DEFAULT_CAPABILITIES, RELEASE_USAGE, buildReleasePlan, describeFeatureBranchGaps, parseReleaseArguments } from "./plan.js";
16
+ export { createGitReader, listCommits, parseCommitLog, readPackageVersionAt, runCaptured, runCommandLine, runInherited } from "./process.js";
17
+ export { createHookContext, runCreateVersion } from "./run.js";
18
+ export { collectReleaseState, findLastRelease, readChangelogState } from "./state.js";