Skip to content

Release & npm packaging

How the beetl package (spec §12.1) is built, packed, and — once user-gated actions unlock it — published.

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/core is never published. tsdown bundles it — and its runtime deps (zod, smol-toml, minisearch) — into the shared chunk, so core lives in the CLI’s devDependencies. Listing it under dependencies would break npm install for consumers (unresolvable private package). This holds for the library entry too: zod is inlined into dist/api.d.mts rather 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.
  • --version reads the version from package.json at runtime via createRequire (works from both src/ in dev and dist/ in the tarball); nothing is hardcoded.
  • prepack runs tsdown, so any npm pack / pnpm publish always ships a fresh build.

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.

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)”
Terminal window
cd packages/cli
npm pack --dry-run # inspect tarball contents
npm pack --pack-destination /tmp # then npm i the .tgz in a scratch dir

The packed binary should run beetl --version and print the package version. Both surfaces must work from one install — that is the acceptance criterion:

Terminal window
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:

  1. Flip the GitHub repo public (spec: happens alongside first publish).
  2. Configure the beetl package’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.