Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2a934c7
docs: add distribution channels research and design (issue #6)
cevheri Jun 29, 2026
8a1bd79
feat: make kernel runtime-agnostic and add browser entry point
cevheri Jun 29, 2026
ffaa1d9
feat: publish to JSR and document CDN/browser usage
cevheri Jun 29, 2026
78f3547
feat: add libredb CLI with read commands (inspect, stats, get, scan)
cevheri Jun 29, 2026
25e725d
feat: add libredb CLI write commands (set, delete, import) with advis…
cevheri Jun 29, 2026
57da743
ci: scope out SonarCloud S8707 in the filesystem and CLI layers
cevheri Jun 29, 2026
8d1bd97
feat: distribute standalone CLI binaries via GitHub Releases
cevheri Jun 29, 2026
f002b8e
feat: ship a multi-arch Docker image of the CLI to GHCR
cevheri Jun 29, 2026
1f823de
feat: add OPFS-backed browser persistence (opfsFileSystem)
cevheri Jun 29, 2026
e30ab5b
fix: harden OPFS adapter and release workflow per final review
cevheri Jun 29, 2026
7f67fa4
chore: bump version to 0.1.0
cevheri Jun 29, 2026
7c575da
ci: also publish the Docker image to Docker Hub; bump to 0.1.1
cevheri Jun 29, 2026
45fc278
ci: use vars.DOCKER_HUB_USERNAME for Docker Hub login; bump to 0.1.2
cevheri Jun 29, 2026
6010e46
docs: note the Docker image is published to Docker Hub too
cevheri Jun 29, 2026
b330ccf
fix: lock reports only EEXIST as locked; clarify fs/docs per review
cevheri Jun 29, 2026
6f040af
chore: add .qoder/ to .gitignore
cevheri Jun 29, 2026
a197813
feat: type browser open so fs is required when a path is given
cevheri Jun 29, 2026
37e4705
fix: harden CLI dispatch, import, and lock per Copilot review
cevheri Jun 29, 2026
0aef094
docs: sync-version manages the README CDN pin; document Docker Hub setup
cevheri Jun 29, 2026
028297c
fix: delete refuses reserved keys; clarify browser typing in README
cevheri Jun 29, 2026
dd36030
refactor: stop re-exporting OpenOptions from the browser entry
cevheri Jun 29, 2026
29b1865
fix: make the advisory lock robust to IO faults (per Copilot review)
cevheri Jun 29, 2026
25b51a5
build: pin base image digests; clarify --force is for stale locks
cevheri Jun 29, 2026
584c94e
docs: add JSR and Docker Hub badges and a Docker Hub link
cevheri Jun 29, 2026
498d47b
ci: publish to JSR via npx, not bunx
cevheri Jun 29, 2026
5c316b2
chore: release 0.1.3 (consume changesets, write CHANGELOG)
cevheri Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Keep the build context tiny: the image only needs src/ to compile the CLI
# (bun build --compile bundles src directly). Everything else is excluded.
node_modules
dist
coverage
.git
.github
.attw
*.tgz
*.log
docs
*.md
.changeset
.remember
.ralph
loop
.vscode
.idea
libredb
libredb-*
*.sha256
154 changes: 151 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
name: Publish

# Publishes to npm ONLY when a GitHub Release is published. It never runs on
# push or pull_request, so creating the tag + release is the single trigger.
# Publishes to npm, JSR, GitHub Releases, GHCR and Docker Hub ONLY when a GitHub
# Release is published. It never runs on push or pull_request, so creating the
# tag + release is the single trigger.
#
# JSR (one-time setup by the maintainer before the first JSR release): create the
# `@libredb` scope on jsr.io, create the `libredb` package, and link this GitHub
# repository to it. That linkage is what lets the `jsr` job below authenticate via
# OIDC (`id-token: write`) with no token or secret.
#
# Docker Hub (one-time setup): set the repository VARIABLE `DOCKER_HUB_USERNAME`
# (the Docker Hub namespace, e.g. `libredb`) and the repository SECRET
# `DOCKER_HUB_TOKEN`. Both are required by the `docker` job; if the username
# variable is unset the Docker Hub login step fails fast (a clear guard) rather
# than pushing to a malformed `docker.io//libredb` path. GHCR needs no setup (it
# uses the built-in `GITHUB_TOKEN`).
on:
release:
types: [published]
Expand All @@ -12,6 +25,9 @@ permissions:
jobs:
publish:
name: npm publish
# Full releases only: a GitHub pre-release must not publish to the npm/JSR
# `latest` channel or Docker `:latest`. Every job carries the same guard.
if: ${{ !github.event.release.prerelease }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand All @@ -38,10 +54,142 @@ jobs:
- name: Job summary
if: success()
run: |
V=$(node -p "require('./package.json').version")
V=$(node -p 'require("./package.json").version')
{
echo "## Published to npm"
echo ""
echo "- Package: \`@libredb/libredb@${V}\`"
echo "- [View on npm](https://www.npmjs.com/package/@libredb/libredb/v/${V})"
} >> "$GITHUB_STEP_SUMMARY"

jsr:
name: JSR publish
# Runs only after npm publish succeeds (npm is the primary registry and has
# already run the full gate), so JSR mirrors exactly what npm shipped.
needs: publish
Comment thread
cevheri marked this conversation as resolved.
if: ${{ !github.event.release.prerelease }}
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # OIDC: token-less JSR publish via the repo-package linkage
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# Use Node + `npx jsr publish` (JSR's canonical invocation) rather than
# `bunx jsr`, which can resolve `jsr` to the repo-root jsr.json on some Bun
# versions ("Cannot run jsr.json"). JSR publishes from source and runs its
# own slow-types check; no build or dependency install is needed.
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "22"
- run: npx --yes jsr publish
- name: Job summary
if: success()
run: |
V=$(node -p 'require("./package.json").version')
{
echo "## Published to JSR"
echo ""
echo "- Package: \`@libredb/libredb@${V}\`"
echo "- [View on JSR](https://jsr.io/@libredb/libredb@${V})"
} >> "$GITHUB_STEP_SUMMARY"

binaries:
name: Standalone binaries
# A self-contained executable of the CLI attached to the GitHub Release (not
# shipped to npm/JSR). Gated behind the publish job so binaries never ship
# from a commit that failed the gate. Bun cross-compiles every target from
# one Linux runner.
needs: publish
if: ${{ !github.event.release.prerelease }}
runs-on: ubuntu-latest
permissions:
contents: write # upload assets to the GitHub Release
strategy:
fail-fast: false
matrix:
include:
- target: bun-linux-x64
out: libredb-linux-x64
- target: bun-linux-arm64
out: libredb-linux-arm64
- target: bun-darwin-x64
out: libredb-darwin-x64
- target: bun-darwin-arm64
out: libredb-darwin-arm64
- target: bun-windows-x64
out: libredb-windows-x64.exe
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .bun-version
- run: bun install --frozen-lockfile
- name: Compile
env:
TARGET: ${{ matrix.target }}
OUT: ${{ matrix.out }}
run: bun build --compile --target="$TARGET" src/cli/main.ts --outfile "$OUT"
- name: Checksum
env:
OUT: ${{ matrix.out }}
run: sha256sum "$OUT" > "$OUT.sha256"
- name: Upload to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.event.release.tag_name }}
OUT: ${{ matrix.out }}
run: gh release upload "$TAG" "$OUT" "$OUT.sha256" --clobber

docker:
name: Docker image
# A portable shell for the CLI (not a server), built multi-arch once and
# pushed to both GHCR and Docker Hub. Gated behind the publish job so the
# image (and :latest) never ships from a commit that failed the gate.
needs: publish
if: ${{ !github.event.release.prerelease }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Resolve version
id: meta
run: echo "version=$(node -p 'require("./package.json").version')" >> "$GITHUB_OUTPUT"
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Log in to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ vars.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
# One buildx build, pushed to both registries via the tag list.
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/libredb:${{ steps.meta.outputs.version }}
ghcr.io/${{ github.repository_owner }}/libredb:latest
docker.io/${{ vars.DOCKER_HUB_USERNAME }}/libredb:${{ steps.meta.outputs.version }}
docker.io/${{ vars.DOCKER_HUB_USERNAME }}/libredb:latest
- name: Job summary
if: success()
env:
OWNER: ${{ github.repository_owner }}
DOCKERHUB: ${{ vars.DOCKER_HUB_USERNAME }}
VERSION: ${{ steps.meta.outputs.version }}
run: |
{
echo "## Published Docker image"
echo ""
echo "- \`ghcr.io/${OWNER}/libredb:${VERSION}\` (and \`:latest\`)"
echo "- \`docker.io/${DOCKERHUB}/libredb:${VERSION}\` (and \`:latest\`)"
} >> "$GITHUB_STEP_SUMMARY"
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ dist/
build/
*.tsbuildinfo

# Compiled standalone binaries (bun build --compile / `bun run compile`)
/libredb
/libredb-*
*.sha256

# Packaging-check scratch (attw runs against a packed tarball)
.attw/
*.tgz
Expand Down Expand Up @@ -39,3 +44,4 @@ loop/

.npmrc

.qoder/
5 changes: 5 additions & 0 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@
"path": "dist/index.js",
"ignore": ["node:fs", "node:os", "node:path"],
"limit": "4 kB"
},
{
"name": "browser entry (min+brotli)",
"path": "dist/browser.js",
"limit": "4 kB"
}
]
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# @libredb/libredb

## 0.1.3

### Patch Changes

- 78f3547: Add a `libredb` CLI for inspecting and editing `.libredb` files (`npx libredb`).

Read commands: `inspect` (list each namespace, its kind, and table schemas),
`stats` (file size and namespace counts by kind), `get <key>`, and
`scan <prefix>`. They open through a read-only filesystem adapter, so inspecting
a file never mutates it — even a crash-torn tail is recovered in memory only,
leaving the bytes on disk untouched.

Write commands: `set <key> <value>`, `delete <key>`, and `import <file.json>`
(bulk-set from a JSON object in a single atomic commit). Writes take an advisory
`<path>.lock` so a second concurrent writer fails loudly instead of corrupting
the file; `--force` overrides a stale lock.

The CLI is built on the public API with zero dependencies (Node/Bun `parseArgs`).

- 8a1bd79: Add a browser entry point (`@libredb/libredb/browser`) and make the kernel
runtime-agnostic.

The `node:fs` dependency moved out of the kernel (`core.ts`) into a dedicated
adapter, so importing LibreDB no longer drags `node:fs` into the module graph.
The default Node entry (`@libredb/libredb`) is unchanged: `open({ path })` still
defaults to the real filesystem and is durable out of the box. The new browser
entry exposes the same lens surface with an `open` that has no default
filesystem — in-memory databases work anywhere, and a path-backed open accepts
an injected filesystem. A bundler targeting the browser now resolves a build
free of Node built-ins via the `browser` export condition.

- 1f823de: Add OPFS-backed browser persistence via `opfsFileSystem` (exported from
`@libredb/libredb/browser`).

A browser `FileSystemSyncAccessHandle` exposes synchronous read/write/getSize/
truncate/flush/close, which map directly onto the kernel's synchronous filesystem
seam — so a LibreDB database can be durable in the browser with no async core.
Inside a Web Worker, obtain a sync access handle and pass it to `open`:

```ts
const root = await navigator.storage.getDirectory();
const file = await root.getFileHandle("app.libredb", { create: true });
const db = open({
path: "app.libredb",
fs: opfsFileSystem(await file.createSyncAccessHandle()),
});
```

The adapter takes an already-open handle (acquisition is async and the caller's),
keeping `open` synchronous. The new `SyncAccessHandle` type names the handle
shape the adapter needs, so the package depends on no DOM lib types.
33 changes: 33 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# syntax=docker/dockerfile:1
#
# A portable shell for the `libredb` CLI — NOT a server. LibreDB is an embedded,
# in-process database; this image just carries the inspection/edit CLI so it can
# run anywhere with a volume-mounted .libredb file. Mount your data and run a
# command, e.g.:
#
# docker run --rm -v "$PWD:/data" ghcr.io/libredb/libredb inspect /data/app.libredb
#
# The CLI has zero runtime dependencies, so the build needs no `bun install`:
# `bun build --compile` bundles only src/ into one self-contained executable.

# Stage 1: compile the CLI for the build platform (buildx sets it per --platform,
# so the same Dockerfile cross-builds amd64 and arm64).
# Pinned by digest (the tag is mutable) for a reproducible, supply-chain-safe
# build; the tag stays for readability and tracks .bun-version (1.3.14, the same
# Bun the binaries job pins via setup-bun). Bump both the tag and the digest
# together when .bun-version changes.
FROM oven/bun:1.3.14@sha256:e10577f0db68676a7024391c6e5cb4b879ebd17188ab750cf10024a6d700e5c4 AS build
WORKDIR /src
# bun build --compile bundles only the source it reaches; the CLI imports nothing
# outside src/, so no package.json/tsconfig and no `bun install` are needed.
COPY src ./src
RUN bun build --compile src/cli/main.ts --outfile /libredb

# Stage 2: a minimal runtime carrying only the binary and the glibc/libstdc++ a
# bun-compiled executable links against. distroless/cc has exactly that. Pinned
# by digest because `cc-debian12` is a rolling tag (no version), so the digest is
# what makes the runtime reproducible; refresh it periodically for base updates.
FROM gcr.io/distroless/cc-debian12@sha256:d703b626ba455c4e6c6fbe5f36e6f427c85d51445598d564652a2f334179f96e
COPY --from=build /libredb /usr/local/bin/libredb
WORKDIR /data
ENTRYPOINT ["/usr/local/bin/libredb"]
Loading
Loading