Release & npm packaging
How the beetl package (spec §12.1) is built, packed, and — once user-gated
actions unlock it — published.
What ships
Section titled “What ships”Supported runtime: Node ≥ 22.12.0 (ADR-0001 — commander 15’s own floor;
engines declared in every workspace manifest, CI runs Node 22 + 24).
One unscoped package, beetl, from packages/cli, with two entry points
(ADR-0010). The tarball contains seven files:
| File | Role |
|---|---|
dist/index.mjs |
the CLI — shebang’d, mode 755, bin target |
dist/api.mjs |
the library — exports["."] |
dist/<chunk>.mjs |
shared core bundle, imported by both entries |
dist/api.d.mts |
public type declarations (zod inlined, no zod import) |
package.json |
manifest (bin + exports) |
README.md |
npm-facing copy |
LICENSE |
copied from the repo root |
Two entries in one tsdown config means core is emitted once into a shared
chunk rather than duplicated (the chunk’s basename is content-hashed, so it
changes with the bundle). The split is why dist/index.mjs now opens with an
import of that chunk — the shebang and 755 mode must survive it, which
pack-smoke verifies across 3 OS × Node 22/24.
bin is a plain path and is not resolved through exports, so adding
exports does not affect the CLI. It does close deep imports
(beetl/dist/...) — free to do before the first publish, expensive after.
@beetl/coreis never published. tsdown bundles it — and its runtime deps (zod,smol-toml,minisearch) — into the shared chunk, so core lives in the CLI’sdevDependencies. Listing it underdependencieswould breaknpm installfor consumers (unresolvable private package). This holds for the library entry too: zod is inlined intodist/api.d.mtsrather than imported, so the public types carry no zod dependency and no version-skew failure mode.- Only real runtime deps stay external:
@clack/prompts,commander,picocolors. --versionreads the version frompackage.jsonat runtime viacreateRequire(works from bothsrc/in dev anddist/in the tarball); nothing is hardcoded.prepackrunstsdown, so anynpm pack/pnpm publishalways ships a fresh build.
Release notes (L-06)
Section titled “Release notes (L-06)”Changesets is wired at the workspace root (.changeset/, package beetl
only — @beetl/core and beetl-docs are ignored as unpublished). User-
visible changes add a changeset (pnpm exec changeset); maintainers fold
them with changeset version before tagging. CONTRIBUTING.md documents
the contributor workflow.
Pre-commit / pre-push gates (M-14)
Section titled “Pre-commit / pre-push gates (M-14)”lefthook runs staged checks by tool capability: oxlint (TS/JS), oxfmt
--check on every type it supports (TS/JS, JSON/JSONC, Markdown/MDX,
YAML, TOML — a parse failure fails the check, so this doubles as validity),
bash -n for shell scripts, and astro check (via beetl-docs typecheck)
when anything under apps/docs/ is staged. pre-push runs the full test
suite.
Verifying a release locally (no npm interaction)
Section titled “Verifying a release locally (no npm interaction)”cd packages/clinpm pack --dry-run # inspect tarball contentsnpm pack --pack-destination /tmp # then npm i the .tgz in a scratch dirThe packed binary should run beetl --version and print the package version.
Both surfaces must work from one install — that is the acceptance criterion:
node --input-type=module -e ' import { SessionStore, searchSessions } from "beetl"; const s = SessionStore.open(process.cwd()); console.log(s.list().length, searchSessions(s, "test", 3));'npx publint <tarball>npx @arethetypeswrong/cli --profile esm-only <tarball>attw runs under --profile esm-only: the package is type: module and
Node ≥ 22.12, so node10 resolution and require()-of-ESM are expected
misses rather than defects. CI’s pack-smoke job runs all three.
Publishing (user-gated — do not automate)
Section titled “Publishing (user-gated — do not automate)”.github/workflows/release.yml publishes on a v* tag push: pnpm check,
tag-vs-package-version guard, a publish --dry-run exercise, then
pnpm --filter beetl publish with npm provenance. Auth is npm trusted
publishing (OIDC via id-token: write — M-13): no long-lived NPM_TOKEN
secret exists or is needed. All workflow actions (CI + release) are pinned
by immutable commit SHA with the tag in a comment. It is inert until
two user-only actions happen:
- Flip the GitHub repo public (spec: happens alongside first publish).
- Configure the
beetlpackage’s Trusted Publisher on npmjs.com to this repo’s release workflow.
Release procedure once unlocked: bump packages/cli/package.json version,
commit, git tag v<version>, push the tag.