beez-rp 0.1.1

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 ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ Todos los cambios relevantes de beez-rp se documentan en este archivo.
4
+
5
+ El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y el proyecto usa [Semantic Versioning](https://semver.org/lang/es/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.1.1] - 2026-09-26
10
+
11
+ ### Added
12
+
13
+ - Plantilla `.env.example` para configurar el token de npm.
14
+
15
+ ## [0.1.0] - 2026-09-26
16
+
17
+ ### Added
18
+
19
+ - Reglas de versión compartidas: solo versiones estables `X.Y.Z` y solo la siguiente patch, minor o major, sin saltear, repetir ni bajar versiones.
20
+ - Comando `beez-rp ignore-build` para el `ignoreCommand` de Vercel, que buildea solo cuando la versión de `package.json` es la siguiente versión estable válida.
21
+ - Módulos `changelog` y `changelog-ai` para liberar el bloque `[Unreleased]` de Keep a Changelog y completarlo con Codex.
22
+ - Módulo `terminal-ui` con cajas, spinner y selector interactivo para comandos de release.
23
+ - Fixtures `beez-rp/testing` con todas las variantes de versión rechazadas, para los tests de cada proyecto.
24
+
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Guido Modarelli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # beez-rp
2
+
3
+ Proceso de release compartido por los proyectos Beez (beez-ui, TuTribu, Control Mensual). No tiene dependencias de runtime: solo usa módulos `node:*`, así que `npx` lo descarga y ejecuta en segundos, incluso antes de instalar las dependencias del proyecto.
4
+
5
+ ## Regla de versiones
6
+
7
+ - Solo existen versiones estables `X.Y.Z`: nunca `-alpha.X`, `-beta.X`, `-rc.X`, metadata `+build`, prefijos (`v1.2.3`) ni ceros a la izquierda.
8
+ - Después de una versión solo se permite la siguiente patch, minor o major. De `1.2.3` valen `1.2.4`, `1.3.0` o `2.0.0`.
9
+ - No se puede repetir, bajar ni saltear versiones (`1.0.0` → `3.0.0`, `1.2.3` → `1.4.0`), ni subir la minor o la major sin reiniciar las partes menores (`1.2.3` → `1.3.3`).
10
+
11
+ ## Módulos
12
+
13
+ | Import | Contenido |
14
+ | --- | --- |
15
+ | `beez-rp/versions` | `isStableReleaseVersion`, `parseReleaseVersion`, `bumpReleaseVersion`, `listNextVersions`, `listAllowedVersionsAfter`, `resolveRequestedVersion` (`--bump` / `--set-version`), `toReleaseTag`, `isReleaseCommitSubject`, `suggestReleaseType`. |
16
+ | `beez-rp/build-gate` | `decideBuild(previousVersion, currentVersion)` y `decideBuildForCheckout(repositoryRoot)` para el `ignoreCommand` de Vercel. |
17
+ | `beez-rp/changelog` | Lectura y release del bloque `## [Unreleased]` de `CHANGELOG.md` (Keep a Changelog). |
18
+ | `beez-rp/changelog-ai` | Prompt e invocación de Codex para completar `[Unreleased]` vacío. |
19
+ | `beez-rp/terminal-ui` | Cajas, filas, banner, spinner y selector interactivo sin dependencias. |
20
+ | `beez-rp/testing` | Fixtures de versiones permitidas y rechazadas para los tests de cada proyecto. |
21
+ | `beez-rp/constants` | Todas las constantes, agrupadas por dominio. |
22
+
23
+ ## Gate de Vercel
24
+
25
+ `beez-rp ignore-build` compara la versión de `package.json` del commit desplegado con la de `HEAD^`. Imprime el motivo y, como última línea, `BUILD` o `SKIP`, y sale con `0`. Si no puede decidir, sale con `2`.
26
+
27
+ El `ignoreCommand` de Vercel corre antes de instalar dependencias. Por eso cada proyecto usa un wrapper que buildea solo cuando la última línea es `BUILD`: cualquier otra salida, incluido un fallo de `npx` o de red, saltea el build.
28
+
29
+ ```bash
30
+ #!/bin/bash
31
+ # scripts/ignore-build.sh — Vercel: exit 0 saltea el build, exit 1 buildea.
32
+ set -u
33
+ GATE_OUTPUT=$(npx --yes beez-rp@X.Y.Z ignore-build)
34
+ GATE_EXIT_CODE=$?
35
+ echo "$GATE_OUTPUT"
36
+ if [ "$GATE_EXIT_CODE" -eq 0 ] && [ "$(printf '%s\n' "$GATE_OUTPUT" | tail -n 1)" = "BUILD" ]; then
37
+ exit 1
38
+ fi
39
+ exit 0
40
+ ```
41
+
42
+ ```json
43
+ { "ignoreCommand": "bash scripts/ignore-build.sh" }
44
+ ```
45
+
46
+ ## Tests en los proyectos
47
+
48
+ ```ts
49
+ import { REJECTED_VERSION_BUMP_CASES, CURRENT_STABLE_VERSION } from "beez-rp/testing";
50
+ import { resolveRequestedVersion } from "beez-rp/versions";
51
+
52
+ it.each(REJECTED_VERSION_BUMP_CASES)("rechaza %s (%j)", (_reason, version) => {
53
+ expect(() => resolveRequestedVersion(CURRENT_STABLE_VERSION, { bump: null, setVersion: version })).toThrow();
54
+ });
55
+ ```
56
+
57
+ ## Publicar beez-rp
58
+
59
+ ```bash
60
+ pnpm create-version # o pnpm cv
61
+ pnpm cv --bump patch|minor|major
62
+ pnpm cv --set-version X.Y.Z # solo la siguiente patch, minor o major
63
+ pnpm cv --dry-run # diagnóstico y plan, sin cambiar nada
64
+ ```
65
+
66
+ El comando sale solo desde `main`, limpio y al día con origin (solo `CHANGELOG.md` puede quedar sin commitear).
67
+
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.
73
+
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.
75
+
76
+ ## Desarrollo
77
+
78
+ ```bash
79
+ pnpm install
80
+ pnpm check # typecheck (JSDoc con checkJs) + tests
81
+ pnpm build:types # genera types/*.d.ts (también corre en prepack)
82
+ ```
package/bin/beez-rp.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `beez-rp` command line.
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.
10
+ *
11
+ * @module beez-rp-cli
12
+ */
13
+
14
+ import { decideBuildForCheckout } from "../src/build-gate.js";
15
+ import { BUILD_DECISION, DECISION_EXIT_CODE, GATE_FAILURE_EXIT_CODE } from "../src/constants/build-gate.js";
16
+ import { CLI_COMMAND } from "../src/constants/cli.js";
17
+
18
+ /** Usage printed for unknown commands. */
19
+ const USAGE = `Usage: beez-rp ${CLI_COMMAND.ignoreBuild}`;
20
+
21
+ const [command] = process.argv.slice(2);
22
+
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
+ try {
28
+ const decision = decideBuildForCheckout(process.cwd());
29
+ console.log(decision.reason);
30
+ console.log(decision.shouldBuild ? BUILD_DECISION.build : BUILD_DECISION.skip);
31
+ process.exitCode = DECISION_EXIT_CODE;
32
+ } catch (error) {
33
+ console.error("beez-rp ignore-build: could not decide; the build must be skipped.", error);
34
+ process.exitCode = GATE_FAILURE_EXIT_CODE;
35
+ }
36
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "beez-rp",
3
+ "version": "0.1.1",
4
+ "description": "Dependency-free release process shared by the Beez projects: stable version rules, Vercel build gate, Keep a Changelog and terminal UI.",
5
+ "license": "MIT",
6
+ "author": "Guido Modarelli",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/guidomodarelli/beez-rp.git"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "engines": {
14
+ "node": ">=22"
15
+ },
16
+ "packageManager": "pnpm@12.6.0",
17
+ "bin": {
18
+ "beez-rp": "./bin/beez-rp.js"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./types/index.d.ts",
23
+ "default": "./src/index.js"
24
+ },
25
+ "./versions": {
26
+ "types": "./types/versions.d.ts",
27
+ "default": "./src/versions.js"
28
+ },
29
+ "./build-gate": {
30
+ "types": "./types/build-gate.d.ts",
31
+ "default": "./src/build-gate.js"
32
+ },
33
+ "./changelog": {
34
+ "types": "./types/changelog.d.ts",
35
+ "default": "./src/changelog.js"
36
+ },
37
+ "./changelog-ai": {
38
+ "types": "./types/changelog-ai.d.ts",
39
+ "default": "./src/changelog-ai.js"
40
+ },
41
+ "./terminal-ui": {
42
+ "types": "./types/terminal-ui.d.ts",
43
+ "default": "./src/terminal-ui.js"
44
+ },
45
+ "./testing": {
46
+ "types": "./types/testing.d.ts",
47
+ "default": "./src/testing.js"
48
+ },
49
+ "./constants": {
50
+ "types": "./types/constants/index.d.ts",
51
+ "default": "./src/constants/index.js"
52
+ },
53
+ "./package.json": "./package.json"
54
+ },
55
+ "files": [
56
+ "bin",
57
+ "src",
58
+ "types",
59
+ "CHANGELOG.md",
60
+ "README.md",
61
+ "LICENSE.md"
62
+ ],
63
+ "scripts": {
64
+ "build:types": "tsc -p tsconfig.types.json",
65
+ "typecheck": "tsc -p tsconfig.json",
66
+ "test": "vitest run",
67
+ "check": "pnpm typecheck && pnpm test",
68
+ "create-version": "node scripts/release.js",
69
+ "cv": "node scripts/release.js",
70
+ "prepack": "pnpm build:types"
71
+ },
72
+ "devDependencies": {
73
+ "@types/node": "^24.13.4",
74
+ "typescript": "^7.0.2",
75
+ "vitest": "^5.0.0"
76
+ }
77
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Decides whether Vercel builds a commit (`vercel.json` → `ignoreCommand`).
3
+ *
4
+ * Only a stable `X.Y.Z` version that is the next patch, minor or major of the
5
+ * previous commit's version ships, the same rule `create-version` applies. An
6
+ * unchanged, lower or skipped version (`1.0.0` → `3.0.0`) and any prerelease
7
+ * or build-metadata version skip the build. When the previous version cannot
8
+ * be read there is nothing to compare, so a stable version builds.
9
+ *
10
+ * @module build-gate
11
+ */
12
+
13
+ import { execFileSync } from "node:child_process";
14
+ import { readFileSync } from "node:fs";
15
+ import path from "node:path";
16
+
17
+ import { PACKAGE_MANIFEST_FILE, PREVIOUS_REVISION } from "./constants/build-gate.js";
18
+ import { isStableReleaseVersion, listAllowedVersionsAfter } from "./versions.js";
19
+
20
+ /**
21
+ * @typedef {{ shouldBuild: boolean, reason: string }} BuildDecision
22
+ */
23
+
24
+ /**
25
+ * Decides whether a commit is built.
26
+ *
27
+ * @param {string | null} previousVersion - Version of the previous commit, or `null` when unreadable.
28
+ * @param {string | null} currentVersion - Version of the commit being deployed, or `null` when unreadable.
29
+ * @returns {BuildDecision} Decision and a log line.
30
+ */
31
+ export function decideBuild(previousVersion, currentVersion) {
32
+ if (currentVersion === null) {
33
+ return { shouldBuild: false, reason: "Current package version could not be read. Skipping build." };
34
+ }
35
+
36
+ if (!isStableReleaseVersion(currentVersion)) {
37
+ return { shouldBuild: false, reason: `Version ${currentVersion} is not a stable X.Y.Z release. Skipping build.` };
38
+ }
39
+
40
+ const allowedVersions = previousVersion === null ? null : listAllowedVersionsAfter(previousVersion);
41
+
42
+ if (allowedVersions === null) {
43
+ return { shouldBuild: true, reason: `Previous package version could not be compared. Building stable ${currentVersion}.` };
44
+ }
45
+
46
+ if (currentVersion === previousVersion) {
47
+ return { shouldBuild: false, reason: "Version did not change. Skipping build." };
48
+ }
49
+
50
+ if (!allowedVersions.includes(currentVersion)) {
51
+ return {
52
+ shouldBuild: false,
53
+ reason: `Version ${previousVersion} -> ${currentVersion} is not the next patch, minor or major (${allowedVersions.join(", ")}). Skipping build.`,
54
+ };
55
+ }
56
+
57
+ return { shouldBuild: true, reason: `Version changed: ${previousVersion} -> ${currentVersion}. Building.` };
58
+ }
59
+
60
+ /**
61
+ * Reads the `version` field of a `package.json` text.
62
+ *
63
+ * @param {() => string} readManifest - Returns the manifest contents.
64
+ * @returns {string | null} Version, or `null` when unreadable.
65
+ */
66
+ function readVersion(readManifest) {
67
+ try {
68
+ const version = JSON.parse(readManifest()).version;
69
+ return typeof version === "string" ? version : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Reads the previous and the current `package.json` versions of a Git
77
+ * checkout and decides whether it is built.
78
+ *
79
+ * @param {string} repositoryRoot - Checkout whose `HEAD` is being deployed.
80
+ * @returns {BuildDecision} Decision and a log line.
81
+ */
82
+ export function decideBuildForCheckout(repositoryRoot) {
83
+ const previousVersion = readVersion(() =>
84
+ execFileSync("git", ["show", `${PREVIOUS_REVISION}:${PACKAGE_MANIFEST_FILE}`], {
85
+ cwd: repositoryRoot,
86
+ encoding: "utf8",
87
+ stdio: ["ignore", "pipe", "ignore"],
88
+ })
89
+ );
90
+ const currentVersion = readVersion(() => readFileSync(path.join(repositoryRoot, PACKAGE_MANIFEST_FILE), "utf8"));
91
+
92
+ return decideBuild(previousVersion, currentVersion);
93
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Asks Codex to fill the CHANGELOG `[Unreleased]` block when a release finds it empty.
3
+ *
4
+ * The prompt travels through stdin (`codex exec -`), so the shell command stays a
5
+ * fixed string on Windows, where `codex` is a `.cmd` shim that needs a shell.
6
+ *
7
+ * @module changelog-ai
8
+ */
9
+
10
+ import { spawn } from "node:child_process";
11
+
12
+ import { CHANGE_TYPES, UNRELEASED_HEADING } from "./constants/changelog.js";
13
+ import { CODEX_COMMAND, CODEX_NOT_FOUND_EXIT_CODE, PROMPT_SHORT_SHA_LENGTH } from "./constants/changelog-ai.js";
14
+
15
+ /**
16
+ * Builds the instructions for Codex from the commits that will ship.
17
+ *
18
+ * @param {{ sha: string, subject: string }[]} commits - Unreleased commits, newest first.
19
+ * @param {string} audience - Who reads the changelog, e.g. "quien consume el paquete".
20
+ * @returns {string} Prompt in Spanish.
21
+ */
22
+ export function buildChangelogPrompt(commits, audience) {
23
+ const commitList = commits.map((commit) => `- ${commit.sha.slice(0, PROMPT_SHORT_SHA_LENGTH)} ${commit.subject}`).join("\n");
24
+ return [
25
+ `Completá el bloque \`${UNRELEASED_HEADING}\` de CHANGELOG.md siguiendo Keep a Changelog.`,
26
+ `- Agrupá las entradas bajo \`### ${CHANGE_TYPES.join("`, `### ")}\`, en ese orden y solo las secciones que apliquen.`,
27
+ `- Una línea \`- \` por cambio, en español, clara para ${audience}; nada de detalles internos de implementación.`,
28
+ `- Si \`${UNRELEASED_HEADING}\` no existe, crealo justo debajo del título del documento.`,
29
+ "- Modificá únicamente CHANGELOG.md: no toques las versiones ya publicadas, ni otros archivos, ni hagas commits.",
30
+ "- Usá `git show <sha>` si necesitás ver el detalle de un commit.",
31
+ "",
32
+ "Commits sin publicar (del más nuevo al más viejo):",
33
+ commitList,
34
+ ].join("\n");
35
+ }
36
+
37
+ /**
38
+ * Runs Codex non-interactively with the prompt on stdin, showing its progress.
39
+ *
40
+ * @param {string} root - Repository directory where Codex edits CHANGELOG.md.
41
+ * @param {string} prompt - Instructions from {@link buildChangelogPrompt}.
42
+ * @returns {Promise<number>} Codex exit code; {@link CODEX_NOT_FOUND_EXIT_CODE} when the CLI is not installed.
43
+ */
44
+ export function runCodex(root, prompt) {
45
+ return new Promise((resolve) => {
46
+ // The command line is a constant; the prompt only travels through stdin.
47
+ const child = spawn(CODEX_COMMAND, { cwd: root, shell: true, stdio: ["pipe", "inherit", "inherit"] });
48
+ child.on("error", () => resolve(CODEX_NOT_FOUND_EXIT_CODE));
49
+ child.on("close", (status) => resolve(status ?? 1));
50
+ child.stdin.end(prompt);
51
+ });
52
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Reads and releases CHANGELOG.md in the Keep a Changelog format.
3
+ *
4
+ * Every change adds its entries under `## [Unreleased]`, grouped by change
5
+ * type (`### Added`, `### Changed`, `### Deprecated`, `### Removed`,
6
+ * `### Fixed`, `### Security`). Releasing moves that block under
7
+ * `## [X.Y.Z] - YYYY-MM-DD` and leaves an empty `## [Unreleased]` on top.
8
+ * Headings of older releases without brackets (`## 0.6.0 - 2026-09-23`)
9
+ * remain valid.
10
+ *
11
+ * @module changelog
12
+ */
13
+
14
+ import {
15
+ CHANGE_TYPES,
16
+ ENTRY_LINE_PATTERN,
17
+ LEADING_WHITESPACE_PATTERN,
18
+ LINE_BREAK_PATTERN,
19
+ RELEASE_HEADING_PATTERN,
20
+ SECTION_HEADING_PATTERN,
21
+ UNRELEASED_HEADING,
22
+ UNRELEASED_LINE_PATTERN,
23
+ } from "./constants/changelog.js";
24
+
25
+ /**
26
+ * @typedef {{ label: string, heading: string, start: number, bodyStart: number, end: number }} ChangelogBlock
27
+ * @typedef {{ exists: boolean, entryCount: number, unknownSections: string[], body: string }} UnreleasedState
28
+ */
29
+
30
+ /**
31
+ * Splits the changelog into release blocks in document order.
32
+ *
33
+ * @param {string} changelog - CHANGELOG.md contents.
34
+ * @returns {ChangelogBlock[]} Blocks.
35
+ */
36
+ function listBlocks(changelog) {
37
+ const headings = [...changelog.matchAll(RELEASE_HEADING_PATTERN)];
38
+ return headings.map((match, index) => {
39
+ const start = match.index ?? 0;
40
+ return {
41
+ label: match[1],
42
+ heading: match[0],
43
+ start,
44
+ bodyStart: start + match[0].length,
45
+ end: headings[index + 1]?.index ?? changelog.length,
46
+ };
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Counts the change entries of a block body.
52
+ *
53
+ * @param {string} body - Block text below its heading.
54
+ * @returns {number} Number of `- ` lines.
55
+ */
56
+ function countEntries(body) {
57
+ return body.split(LINE_BREAK_PATTERN).filter((line) => ENTRY_LINE_PATTERN.test(line)).length;
58
+ }
59
+
60
+ /**
61
+ * Describes the `## [Unreleased]` block.
62
+ *
63
+ * @param {string} changelog - CHANGELOG.md contents.
64
+ * @returns {UnreleasedState} Unreleased state.
65
+ */
66
+ export function readUnreleased(changelog) {
67
+ const block = listBlocks(changelog).find((candidate) => UNRELEASED_LINE_PATTERN.test(candidate.heading));
68
+ if (!block) return { exists: false, entryCount: 0, unknownSections: [], body: "" };
69
+ const body = changelog.slice(block.bodyStart, block.end);
70
+ const unknownSections = [...body.matchAll(SECTION_HEADING_PATTERN)]
71
+ .map((match) => match[1])
72
+ .filter((name) => !CHANGE_TYPES.includes(name));
73
+ return { exists: true, entryCount: countEntries(body), unknownSections, body: body.trim() };
74
+ }
75
+
76
+ /**
77
+ * Returns the newest released block (the first one that is not `[Unreleased]`).
78
+ *
79
+ * @param {string} changelog - CHANGELOG.md contents.
80
+ * @returns {{ version: string, entryCount: number } | null} Latest release entry.
81
+ */
82
+ export function readLatestRelease(changelog) {
83
+ const block = listBlocks(changelog).find((candidate) => !UNRELEASED_LINE_PATTERN.test(candidate.heading));
84
+ return block ? { version: block.label, entryCount: countEntries(changelog.slice(block.bodyStart, block.end)) } : null;
85
+ }
86
+
87
+ /**
88
+ * Moves the `[Unreleased]` changes under a new version heading and leaves an empty `[Unreleased]` block.
89
+ *
90
+ * @param {string} changelog - CHANGELOG.md contents.
91
+ * @param {string} version - Version being released.
92
+ * @param {string} releaseDate - Date in `YYYY-MM-DD` format.
93
+ * @returns {string} Released changelog.
94
+ * @throws {Error} When `[Unreleased]` is missing, empty or uses an unknown section.
95
+ */
96
+ export function releaseUnreleased(changelog, version, releaseDate) {
97
+ const unreleased = readUnreleased(changelog);
98
+ if (!unreleased.exists) throw new Error(`create-version: CHANGELOG.md needs a "${UNRELEASED_HEADING}" block`);
99
+ if (unreleased.unknownSections.length > 0) {
100
+ throw new Error(
101
+ `create-version: CHANGELOG.md [Unreleased] uses unknown sections (${unreleased.unknownSections.join(", ")}); use ${CHANGE_TYPES.join(", ")}`
102
+ );
103
+ }
104
+ if (unreleased.entryCount === 0) throw new Error("create-version: CHANGELOG.md [Unreleased] has no changes to release");
105
+ const block = listBlocks(changelog).find((candidate) => UNRELEASED_LINE_PATTERN.test(candidate.heading));
106
+ if (!block) throw new Error(`create-version: CHANGELOG.md needs a "${UNRELEASED_HEADING}" block`);
107
+ const released = `${UNRELEASED_HEADING}\n\n## [${version}] - ${releaseDate}\n\n${unreleased.body}\n\n`;
108
+ return `${changelog.slice(0, block.start)}${released}${changelog.slice(block.end).replace(LEADING_WHITESPACE_PATTERN, "")}`;
109
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Contract between the `beez-rp ignore-build` command and the Vercel
3
+ * `ignoreCommand` wrapper that reads its output.
4
+ *
5
+ * @module constants/build-gate
6
+ */
7
+
8
+ /** Decision printed as the last output line of `beez-rp ignore-build`. */
9
+ export const BUILD_DECISION = Object.freeze({
10
+ build: "BUILD",
11
+ skip: "SKIP",
12
+ });
13
+
14
+ /** Git revision whose `package.json` holds the previous version. */
15
+ export const PREVIOUS_REVISION = "HEAD^";
16
+
17
+ /** Manifest that holds the version. */
18
+ export const PACKAGE_MANIFEST_FILE = "package.json";
19
+
20
+ /** Exit code of `beez-rp ignore-build` when a decision was printed. */
21
+ export const DECISION_EXIT_CODE = 0;
22
+
23
+ /** Exit code of `beez-rp ignore-build` when it could not decide; wrappers must skip the build. */
24
+ export const GATE_FAILURE_EXIT_CODE = 2;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Codex invocation used to fill an empty `[Unreleased]` block.
3
+ *
4
+ * @module constants/changelog-ai
5
+ */
6
+
7
+ /** Exit code shells use when a command does not exist. */
8
+ export const CODEX_NOT_FOUND_EXIT_CODE = 127;
9
+
10
+ /** Fixed Codex command: non-interactive, allowed to edit the workspace, no saved session, prompt on stdin. */
11
+ export const CODEX_COMMAND = "codex exec --sandbox workspace-write --ephemeral --color never -";
12
+
13
+ /** Length of the abbreviated commit ids listed in the prompt. */
14
+ export const PROMPT_SHORT_SHA_LENGTH = 7;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Keep a Changelog contract used to read and release `CHANGELOG.md`.
3
+ *
4
+ * @module constants/changelog
5
+ */
6
+
7
+ /** Changelog file released together with `package.json`. */
8
+ export const CHANGELOG_FILE = "CHANGELOG.md";
9
+
10
+ /** Heading of the block that collects changes not yet released. */
11
+ export const UNRELEASED_HEADING = "## [Unreleased]";
12
+
13
+ /** Change types allowed as `###` sections, in Keep a Changelog order. */
14
+ export const CHANGE_TYPES = Object.freeze(["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]);
15
+
16
+ /** Any release-level heading: `## [Unreleased]`, `## [0.7.0] - 2026-09-26` or `## 0.6.0 - 2026-09-23`. */
17
+ export const RELEASE_HEADING_PATTERN = /^## +\[?([^\]\s]+)\]?[^\n]*$/gmu;
18
+
19
+ /** Unreleased heading, case-insensitive, as its own line. */
20
+ export const UNRELEASED_LINE_PATTERN = /^## +\[Unreleased\][ \t]*$/imu;
21
+
22
+ /** A change entry line. */
23
+ export const ENTRY_LINE_PATTERN = /^\s*- +\S/mu;
24
+
25
+ /** A change-type section heading inside a release block. */
26
+ export const SECTION_HEADING_PATTERN = /^### +(.+?)\s*$/gmu;
27
+
28
+ /** Leading whitespace left before the next block after moving `[Unreleased]`. */
29
+ export const LEADING_WHITESPACE_PATTERN = /^\s+/u;
30
+
31
+ /** Line break in a changelog written on any platform. */
32
+ export const LINE_BREAK_PATTERN = /\r?\n/u;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Commands understood by the `beez-rp` command line.
3
+ *
4
+ * @module constants/cli
5
+ */
6
+
7
+ /** Command names accepted as the first `beez-rp` argument. */
8
+ export const CLI_COMMAND = Object.freeze({
9
+ ignoreBuild: "ignore-build",
10
+ });
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Facade of every `beez-rp` constant, grouped by domain.
3
+ *
4
+ * @module constants
5
+ */
6
+
7
+ export * from "./build-gate.js";
8
+ export * from "./changelog.js";
9
+ export * from "./changelog-ai.js";
10
+ export * from "./cli.js";
11
+ export * from "./terminal-ui.js";
12
+ export * from "./versions.js";