Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
18 changes: 18 additions & 0 deletions .changeset/cli-read-commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@libredb/libredb": minor
---

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`).
15 changes: 15 additions & 0 deletions .changeset/distribution-browser-entry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@libredb/libredb": minor
---

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.
21 changes: 21 additions & 0 deletions .changeset/opfs-browser-persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@libredb/libredb": minor
---

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.
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/ + the tsconfigs to
# compile the CLI. Everything else is excluded.
Comment thread
cevheri marked this conversation as resolved.
Outdated
node_modules
dist
coverage
.git
.github
.attw
*.tgz
*.log
docs
*.md
.changeset
.remember
.ralph
loop
.vscode
.idea
libredb
libredb-*
*.sha256
123 changes: 121 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
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 and JSR 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.
on:
release:
types: [published]
Expand Down Expand Up @@ -45,3 +50,117 @@ jobs:
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.
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
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .bun-version
# JSR publishes from source and runs its own slow-types check; no build or
# dependency install is needed (the source has no external runtime deps).
- run: bunx 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
# Independent of the registry publishes: a self-contained executable of the
# CLI, attached to the GitHub Release (not shipped to npm/JSR). Bun cross-
# compiles every target from one Linux runner.
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 and pushed to
# GHCR. The Dockerfile compiles the CLI from source, so this needs no registry
# publish to have happened first.
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"
Comment thread
cevheri marked this conversation as resolved.
Outdated
Comment thread
cevheri marked this conversation as resolved.
Outdated
- uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
Comment thread
cevheri marked this conversation as resolved.
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- 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
- name: Job summary
if: success()
env:
OWNER: ${{ github.repository_owner }}
VERSION: ${{ steps.meta.outputs.version }}
run: |
{
echo "## Published Docker image"
echo ""
echo "- \`ghcr.io/${OWNER}/libredb:${VERSION}\`"
echo "- \`ghcr.io/${OWNER}/libredb:latest\`"
} >> "$GITHUB_STEP_SUMMARY"
5 changes: 5 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
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"
}
]
26 changes: 26 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# 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).
FROM oven/bun:1 AS build
WORKDIR /src
COPY package.json tsconfig.json tsconfig.build.json ./
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.
FROM gcr.io/distroless/cc-debian12
COPY --from=build /libredb /usr/local/bin/libredb
WORKDIR /data
ENTRYPOINT ["/usr/local/bin/libredb"]
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,79 @@ Each lens has its own guide: [key-value](./docs/guides/key-value.md) ·
[document](./docs/guides/document.md) · [relational](./docs/guides/relational.md) ·
[catalog](./docs/guides/catalog.md).

## Install elsewhere: JSR, CDN, and the browser

LibreDB is the same ESM-only package everywhere; only how you reach it changes.

**JSR** — published to [jsr.io](https://jsr.io/@libredb/libredb) alongside npm:

```sh
bunx jsr add @libredb/libredb
# or: npx jsr add @libredb/libredb / deno add jsr:@libredb/libredb
```

**CDN** — every release is served from the npm registry by the usual CDNs. Pin a version:

```ts
import { open, kv } from "https://esm.sh/@libredb/libredb@0.0.4";
```

**Browser** — a dedicated entry that imports nothing from `node:`, so it bundles for the browser
cleanly. Its `open` carries no default filesystem: an in-memory database works anywhere, and a
path-backed open takes a filesystem you inject (e.g. a future OPFS adapter).

```ts
import { open, kv } from "@libredb/libredb/browser";

const db = open(); // in-memory
kv(db).set("greeting", "hello");
```

For durable storage in the browser, run inside a Web Worker and back the database with an OPFS sync
access handle (the kernel stays synchronous — no async core):

```ts
import { open, opfsFileSystem } from "@libredb/libredb/browser";

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

A browser-targeting bundler resolves the browser build automatically via the package's `browser`
export condition, so importing the main `@libredb/libredb` entry works too.
Comment thread
cevheri marked this conversation as resolved.
Outdated

## Command-line tool

The package ships a `libredb` bin for inspecting and editing `.libredb` files — no code required:

Comment thread
cevheri marked this conversation as resolved.
```sh
npx libredb inspect data.libredb # namespaces, kinds, and table schemas
npx libredb stats data.libredb # file size and namespace counts
npx libredb get data.libredb user:1 # print one value
npx libredb scan data.libredb user: # print key=value under a prefix
npx libredb set data.libredb user:1 Ada # set a key
npx libredb delete data.libredb user:1 # remove a key
npx libredb import data.libredb seed.json # bulk-set from a JSON object, atomically
```

Read commands open the file read-only, so inspection never mutates it. Write commands take an
advisory `<path>.lock` to refuse a second concurrent writer; pass `--force` to override a stale lock.

Prefer a standalone binary with no Node or Bun installed? Each release attaches self-contained
executables (Linux, macOS, Windows; x64 and arm64) with `.sha256` checksums on its
[GitHub Release](https://github.com/libredb/libredb-database/releases). Or build one locally with
`bun run compile`.

Or run the CLI from a container (multi-arch, published to GHCR) — mount your data and pass a command:

```sh
docker run --rm -v "$PWD:/data" ghcr.io/libredb/libredb inspect /data/app.libredb
```

The image is a CLI shell, not a server: LibreDB stays an embedded, in-process database.

## How it works: one core, three lenses

<picture>
Expand Down
5 changes: 5 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ coverage = true
# CI-based coverage ingestion. coverage/ is git-ignored.
coverageReporter = ["text", "lcov"]
coverageSkipTestFiles = true
# The bin shim (cli/main.ts) is pure process glue — shebang, process.argv,
# stdout/stderr and exit code — with all behaviour in cli/run.ts (held at 100%).
# It cannot be line-covered without spawning a process, so it is excluded here
# and validated behaviourally by an end-to-end smoke test (cli/main.test.ts).
coveragePathIgnorePatterns = ["src/cli/main.ts"]
# Start at 100% and hold it. When the kernel lands this stays the bar for core.ts.
# Bun enforces line/function/statement (not branch); all three held at 100%.
coverageThreshold = { line = 1.0, function = 1.0, statement = 1.0 }
Expand Down
Loading
Loading