Skip to content

feat: distribution channels — JSR, browser/OPFS, CLI, binaries, Docker (issue #6)#8

Merged
cevheri merged 26 commits into
mainfrom
feat/distribution-channels
Jun 29, 2026
Merged

feat: distribution channels — JSR, browser/OPFS, CLI, binaries, Docker (issue #6)#8
cevheri merged 26 commits into
mainfrom
feat/distribution-channels

Conversation

@cevheri

@cevheri cevheri commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Implements issue #6 (distribution channels) as a phased, autonomously-built PR.
Each phase was developed with TDD, gated locally, pushed, and only advanced once
all of CI (gate, SonarCloud quality gate, CodeQL) was green. Ready for final
human review — please do not expect it pre-merged.

Design: docs/superpowers/specs/2026-06-29-distribution-channels-design.md.

Phases (all CI-green)

  • 0 — Runtime-agnostic core + browser entry. Moved node:fs out of the
    kernel into src/adapter/node-fs.ts; kernel now imports nothing from node:.
    New @libredb/libredb/browser entry (in-memory; injected fs for persistence)
    via the browser export condition. A Bun.Transpiler import-graph test proves
    the browser entry pulls in no node: builtins. (changeset: minor)
  • 1 — JSR + CDN/browser docs. jsr.json (source exports), a JSR publish job
    (OIDC), sync-version.ts keeps jsr.json in step (test-backstopped), README
    "Install elsewhere". Verified with jsr publish --dry-run.
  • 2 — CLI (npx libredb). Read: inspect/stats/get/scan (read-only fs
    adapter — inspection never mutates the file). Write: set/delete/import
    (advisory <path>.lock with sentinel; --force; reserved-key guard; atomic
    bulk import). Zero-dep parseArgs; bin shim excluded from coverage + e2e smoke
    test. (changeset: minor)
  • 3 — Standalone binaries. bun build --compile cross-compile matrix
    (linux/macOS/windows, x64+arm64) → GitHub Release + .sha256. bun run compile
    for local builds. Verified native compile locally.
  • 4 — Docker. Multi-arch image (distroless/cc) → GHCR; a CLI shell, not a
    server. Verified locally: build + volume-mounted set/get/stats roundtrip.
  • 5 — OPFS browser persistence. opfsFileSystem maps the synchronous kernel
    fs seam onto a FileSystemSyncAccessHandle (no async core); exported from the
    browser entry; a local SyncAccessHandle type means no DOM-lib dependency.
    100% tested against an in-memory fake. (changeset: minor)

Verification

  • bun run gate green (261 tests, 100% line/function/statement coverage).
  • publint clean; attw --profile esm-only green for . and ./browser.
  • Three adversarial multi-agent reviews (Phase 0, Phase 2, and a final
    whole-PR pass); all real findings fixed (notably: OPFS short-write loop, CLI
    reserved-key guard + lock sentinel, release-workflow gating + prerelease guard).
  • SonarCloud quality gate green (tssecurity:S8707 scoped out in the fs/CLI layers
    with rationale — opening a caller-named path is by design for an embedded DB).

Maintainer prerequisites before the first release using this

  • JSR: create the @libredb scope + libredb package on jsr.io and link this
    repo (enables the OIDC publish).
  • Docker/binaries: none — GITHUB_TOKEN covers GHCR and Release uploads.
  • Every published GitHub Release publishes to npm/JSR latest + Docker :latest;
    mark a release as a pre-release to skip all publishing.

cevheri added 2 commits June 29, 2026 14:27
Move the node:fs dependency out of the kernel (core.ts) into a dedicated
adapter (adapter/node-fs.ts) so the kernel imports nothing from node:. The
default Node entry (index.ts) wires the node:fs adapter in as the default
filesystem, preserving open({ path }) durability; the new browser entry
(browser.ts, exposed as @libredb/libredb/browser via the exports map) carries
no default filesystem so its import graph is free of node: builtins.

A path-backed open with no filesystem now throws a clear error in the kernel.
Tests that exercise real-disk durability open through the Node entry; a new
browser.test.ts pins both the runtime behaviour and the no-node-imports
guarantee via a static import-graph walk.

Phase 0 of issue #6 (distribution channels).
Add a jsr.json manifest (source-based exports for the default and ./browser
entries) and a JSR publish job in the release workflow, authenticated via OIDC
after the npm publish succeeds. sync-version.ts now keeps jsr.json's version in
step with package.json (single source of truth), backstopped by a test. README
gains an 'Install elsewhere' section covering JSR, pinned CDN imports, and the
browser entry.

Phase 1 of issue #6 (distribution channels).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Phase 0 of “distribution channels” (#6): refactors LibreDB so the kernel is runtime-agnostic (no node: imports), while preserving Node/Bun durability ergonomics via a Node-specific entry wrapper and adding a dedicated browser entry point.

Changes:

  • Move the default node:fs filesystem adapter out of src/core.ts into src/adapter/node-fs.ts, and make the kernel require an explicit filesystem for path-backed opens.
  • Add a browser entry (src/browser.ts) and tests that pin both behavior and the “no node: builtins in the browser import graph” guarantee.
  • Update package exports / size limits and adjust tests to import open through the Node entry when they need real-path persistence.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/core.ts Removes node:fs dependency from the kernel; adds explicit runtime errors for empty path and missing fs when path is provided.
src/adapter/node-fs.ts Introduces the Node/Bun FileSystem adapter previously embedded in the kernel.
src/index.ts Wraps kernel open to default fs to nodeFileSystem() when path is provided and fs is omitted (Node/Bun ergonomics).
src/browser.ts Adds a browser-safe entry that re-exports the kernel open (no default filesystem).
src/browser.test.ts Adds tests for browser entry behavior and an import-graph walk asserting no transitive node: builtins.
src/index.test.ts Adds tests pinning Node entry semantics (default fs, in-memory open, empty-path error, injected fs passthrough).
src/core.fs.test.ts Adds tests asserting kernel throws on open({ path }) without fs, and on empty path.
src/core.kv.test.ts Switches durability tests to import open via the Node entry.
src/core.recovery.test.ts Switches recovery tests to import open via the Node entry.
src/lens/catalog.test.ts Switches persistence tests to import open via the Node entry.
src/lens/document.test.ts Switches persistence tests to import open via the Node entry.
src/lens/kv.test.ts Switches persistence tests to import open via the Node entry.
src/lens/relational.test.ts Switches persistence tests to import open via the Node entry.
package.json Adds exports entries/conditions for ./browser and browser condition on ..
knip.json Registers src/browser.ts as an entry for knip.
.size-limit.json Adds a size-limit target for dist/browser.js.
docs/superpowers/specs/2026-06-29-distribution-channels-design.md Adds the research/design document for distribution channels and Phase 0 rationale.
.changeset/distribution-browser-entry.md Adds a minor changeset documenting the new browser entry and kernel refactor.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/core.ts Outdated
Comment thread docs/superpowers/specs/2026-06-29-distribution-channels-design.md Outdated
Introduce a zero-dependency `libredb` bin (node:util parseArgs) built on the
public API. run(argv, io) is the whole CLI as a pure function returning an exit
code, so every command and error path is unit-tested; main.ts is a thin process
shim (excluded from coverage, validated by an end-to-end smoke test).

Read commands open through a read-only filesystem adapter so inspection never
mutates the file: append/fsync refuse and truncate is a no-op, so a crash-torn
tail is recovered in memory while the bytes on disk are left untouched.

Phase 2a of issue #6 (distribution channels).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 3 comments.

Comment thread package.json
Comment thread src/cli/run.ts
Comment thread src/browser.ts Outdated
…ory lock

Extend the CLI with set, delete, and import. import bulk-sets keys from a JSON
object in a single transaction so a load is atomic — a crash mid-write lands
nothing. All writes acquire an advisory <path>.lock first (LibreDB is
single-process with no file locking), turning a concurrent writer into a loud
error instead of corruption; --force overrides a stale lock. README documents
the full CLI.

Phase 2b of issue #6 (distribution channels).
tssecurity:S8707 flags CLI-argument-derived paths reaching the filesystem as a
path-escape vulnerability. That is by design here: LibreDB is an embedded,
single-process database and `libredb` is its local CLI, both built to open the
path their caller names (like sqlite3/cat). There is no privilege boundary to
cross, and confining to a base directory would defeat the tool. Scoped to
src/adapter and src/cli only.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

Comment thread src/cli/lock.ts Outdated
Comment thread src/core.ts
Comment thread .github/workflows/publish.yml
cevheri added 3 commits June 29, 2026 15:32
Add a release job that cross-compiles the CLI with `bun build --compile` for
linux/macOS/windows (x64 + arm64) on one runner, attaching each self-contained
executable and its .sha256 to the GitHub Release. Not shipped to npm/JSR. Adds a
`bun run compile` script for local native builds, README download notes, and
gitignore entries for compiled output.

Phase 3 of issue #6 (distribution channels).
Add a Dockerfile that compiles the CLI with bun and copies the self-contained
binary into a distroless/cc runtime, plus a release job that builds it
multi-arch (linux/amd64 + linux/arm64) and pushes to ghcr.io/<owner>/libredb
tagged with the version and latest. The image is a CLI shell, not a server —
LibreDB stays embedded. Verified locally: build + volume-mounted set/get/stats
roundtrip. README documents the docker run usage.

Phase 4 of issue #6 (distribution channels).
A FileSystemSyncAccessHandle exposes synchronous read/write/getSize/truncate/
flush/close, which map onto the kernel's synchronous filesystem seam, so a
LibreDB database can be durable in the browser with no async core. opfsFileSystem
wraps an already-open handle (async acquisition is the caller's, keeping open()
synchronous) and is exported from the browser entry. A locally-declared
SyncAccessHandle type names the handle shape, so the package needs no DOM lib
types and the adapter is fully unit-tested against an in-memory fake (write,
reopen-recovery, torn-tail truncate).

Phase 5 of issue #6 (distribution channels).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 3 comments.

Comment thread .github/workflows/publish.yml Outdated
Comment thread src/cli/lock.ts
Comment thread README.md
- opfs: loop append() until every byte is written (a short write() return must
  not persist a torn record while fsync reports success), mirroring node-fs; add
  a short-write test.
- publish.yml: gate binaries and docker behind needs:publish and guard every job
  with if:!prerelease, so a failed gate or a pre-release cannot ship a split or
  latest-tagged release.
- Dockerfile: pin oven/bun:1.3.14 (match .bun-version) and copy only src/.
- Docs: drop stale 'future OPFS adapter' wording (it shipped); reconcile the
  design doc's CLI section with what shipped (no repl, stats by kind, <path>.lock).
@cevheri cevheri changed the title feat: distribution channels (issue #6) feat: distribution channels — JSR, browser/OPFS, CLI, binaries, Docker (issue #6) Jun 29, 2026
@cevheri cevheri marked this pull request as ready for review June 29, 2026 12:54
@cevheri cevheri requested a review from Copilot June 29, 2026 13:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 3 comments.

Comment thread src/index.ts
Comment thread .github/workflows/publish.yml
Comment thread .dockerignore Outdated
cevheri added 3 commits June 29, 2026 16:10
The docker job now logs into both GHCR and Docker Hub and pushes the same
multi-arch build to both (docker.io/<DOCKER_HUB_USERNAME>/libredb :version +
:latest) in one buildx run. Bump to 0.1.1 to exercise the full release pipeline
end-to-end with the added registry.
The Docker Hub username is a repository variable (not a secret), so the login
step read an empty username and failed. Reference vars.DOCKER_HUB_USERNAME
instead. Bump to 0.1.2 to re-run the full pipeline (0.1.1 already published npm/
JSR but its Docker push never ran).
cevheri added 2 commits June 29, 2026 16:29
- lock.ts: acquireLock now treats only EEXIST as 'locked' and rethrows other IO
  errors (missing dir, permissions) with the original attached as cause, so real
  failures are not misreported; add a test for the surfaced-error path.
- core.ts: reword OpenOptions.fs doc — fs is optional at the type level but
  required at runtime for a path-backed open (kernel/browser throw; Node defaults).
- design spec: label the node:fs-coupling bullet as the pre-Phase-0 baseline.
- .dockerignore: correct the comment (only src/ is copied into the build).

Addresses Copilot PR review comments.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 4 comments.

Comment thread .github/workflows/publish.yml Outdated
Comment thread src/cli/run.ts
Comment thread src/cli/run.ts Outdated
Comment thread src/cli/lock.ts Outdated
cevheri added 2 commits June 29, 2026 17:07
Add BrowserOpenOptions (a union making fs mandatory whenever path is present)
and type the browser entry's open with it, so a path-backed open without a
filesystem is a compile error for TS users instead of only a runtime throw. The
kernel is unchanged: assigning its wider-typed open here is sound by parameter
contravariance (BrowserOpenOptions is a subtype of OpenOptions). A
@ts-expect-error test locks the guarantee; the runtime guard still protects JS
callers. Addresses the browser-entry Copilot review comment.
- run.ts: dispatch via a Map so an inherited property name (toString,
  __proto__) can never resolve to a handler; malformed import JSON now returns
  the usage exit code 2 instead of falling through to 1.
- lock.ts: close the lock fd in a finally so a failed sentinel write cannot leak
  the descriptor.
- publish.yml: rewrite the node -p version reads to single-quote form (no nested
  double quotes) — equivalent, and stops the recurring false-positive flag.
Adds tests for the prototype-property and malformed-JSON cases.
Address the external PR review's two actionable polish items:
- sync-version.ts now rewrites the esm.sh pin in README.md to the current
  version, so the CDN example never drifts behind a release (single source of
  truth, like core.ts and jsr.json).
- publish.yml header documents the Docker Hub one-time setup (DOCKER_HUB_USERNAME
  variable + DOCKER_HUB_TOKEN secret) and why an unset username fails fast.
The review's other points were already resolved (publish.yml quoting, browser
options type) or are intentional (single whole-file read on recovery).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 3 comments.

Comment thread src/cli/run.ts
Comment thread README.md Outdated
Comment thread src/cli/run.test.ts
- run.ts: the delete command now guards reserved keys like set/import, so the
  CLI cannot remove a catalog/internal key and corrupt the layout; add a test.
- README: clarify that importing the main entry in a browser gives Node types
  unless TS is configured for the browser condition — use the /browser subpath
  for the fs-required-with-path typing. Addresses Copilot review comments.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 39 out of 40 changed files in this pull request and generated 2 comments.

Comment thread src/cli/lock.ts
Comment thread src/cli/lock.ts Outdated
cevheri added 6 commits June 29, 2026 17:37
The browser open is typed with BrowserOpenOptions (fs required when a path is
given), so re-exporting the kernel's permissive OpenOptions from this entry
advertised a contract it does not honor. Drop it; keep Database/FileSystem/
WalFile, which a browser consumer needs to inject a custom filesystem. Tightens
the entry's type surface to its real API.
- dropOwnLock: use existsSync for the 'no lock' case so a non-ENOENT read error
  (e.g. EACCES) propagates instead of being silently swallowed.
- treat an empty <path>.lock as ours and removable, so a stray left by a failed
  sentinel write (ENOSPC/EIO) is recoverable via --force instead of wedging
  future writes; a non-empty non-sentinel file is still refused (protects
  unrelated user data). Add a test for the empty-stray case.
Security-review (supply-chain) follow-ups:
- Dockerfile: pin oven/bun:1.3.14 and gcr.io/distroless/cc-debian12 by digest.
  Both tags are mutable (cc-debian12 is rolling), so the digests make the image
  reproducible and resistant to an upstream tag override. Verified the build
  locally with the pinned digests.
- README: note that --force is only for clearing a stale lock from a crashed
  writer, since the advisory lock cannot stop two simultaneous forcing writers.

(Other review items are accepted design decisions or maintainer-side: npm
--provenance lands when the repo is public; Docker Hub token scope is a registry
setting.)
The badge row covered only npm; add JSR and Docker Hub (Docker-logo) badges so
all three registries this project publishes to are visible, and link the Docker
Hub and GHCR image pages from the container section.
bunx jsr can resolve 'jsr' to the repo-root jsr.json on some Bun versions
("Cannot run jsr.json"), which would fail the JSR job at release time after npm
already published. Switch the job to Node + `npx jsr publish` — JSR's canonical
invocation, robust across environments — and drop setup-bun (npx needs only
Node). OIDC (id-token) is unchanged.
The browser/CLI/OPFS features already shipped publicly in the 0.1.0-0.1.2 test
releases, so the stable release is a patch (0.1.3) finalizing them — keeps Studio
in the ^0.1.x range and avoids colliding with the burned versions on npm/JSR.
changeset:version consumed the three changesets, wrote CHANGELOG.md, and synced
the version across core.ts, jsr.json, and the README CDN pin.
@sonarqubecloud

Copy link
Copy Markdown

@cevheri cevheri merged commit 8adfd0b into main Jun 29, 2026
6 checks passed
@cevheri cevheri deleted the feat/distribution-channels branch June 29, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Explore additional packaging/distribution channels (JSR, CDN/browser, CLI, standalone binary, Docker)

2 participants