Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
69 changes: 69 additions & 0 deletions .github/workflows/cloudflare-remote.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Check Graft remote packages

on:
push:
branches: ["main"]
paths:
- "packages/graft-remote/**"
- "packages/graft-remote-cloudflare/**"
- "packages/graft-remote-hono/**"
- "services/graft-remote-cloudflare/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- ".github/workflows/cloudflare-remote.yml"
pull_request:
branches: ["main"]
paths:
- "packages/graft-remote/**"
- "packages/graft-remote-cloudflare/**"
- "packages/graft-remote-hono/**"
- "services/graft-remote-cloudflare/**"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
- ".github/workflows/cloudflare-remote.yml"
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false

- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10.14.0

- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
node-version: 22
cache: pnpm
cache-dependency-path: pnpm-lock.yaml

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Typecheck generated bindings and source
run: pnpm check:remote

- name: Test in the Workers runtime
run: pnpm test:remote

- name: Validate the publishable packages
run: |
pnpm --filter @eidos.space/graft-remote publish --dry-run --no-git-checks
pnpm --filter @eidos.space/graft-remote-hono publish --dry-run --no-git-checks
pnpm --filter @eidos.space/graft-remote-cloudflare publish --dry-run --no-git-checks

- name: Validate the deployment bundle
run: pnpm --filter graft-remote-cloudflare-verification exec wrangler deploy --dry-run --outdir /tmp/graft-remote-worker
2 changes: 1 addition & 1 deletion .github/workflows/docs-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
persist-credentials: false
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10.27
package_json_file: docs/package.json
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
with:
cache-dependency-path: "docs"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/mutants.out*
/dist/
/distx/
/node_modules/
**/*.rs.bk
*.pdb
.env
Expand Down
25 changes: 25 additions & 0 deletions .impeccable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Design Context

### Users

Developers evaluating or learning Graft who want a zero-install, browser-only sandbox. Their main job is to understand how Graft versions a SQLite-backed worktree by running real commands, inspecting OPFS state, and seeing version-control operations reflected immediately in a GUI.

### Brand Personality

Technical, tangible, and trustworthy. The interface should feel like a focused database workbench: direct enough for terminal users, legible enough for someone learning Graft, and calm while exposing low-level state.

### Aesthetic Direction

Use a light, Flexoki-adjacent palette that connects naturally to the existing documentation. Build a dense but breathable desktop workbench rather than a marketing dashboard: OPFS file tree at the left, command terminal as the primary surface, and version history/change review at the right. Avoid generic card grids, neon-on-dark developer styling, glass effects, and oversized decorative metrics. Adapt the panes into a useful tabbed workflow on narrow screens instead of hiding functionality.

### Design Principles

1. Make cause and effect visible: every command should visibly update files, repository state, and history.
2. Preserve the real mental model: distinguish the OPFS worktree, `.graft` metadata, staged changes, and committed history.
3. Keep the terminal first-class while giving common Graft operations clear GUI controls.
4. Prefer information density with strong hierarchy over decorative containers.
5. Meet WCAG AA contrast, provide keyboard operation, and respect reduced-motion preferences.

### Component Stack

Use `@pierre/trees` for the OPFS explorer, `@pierre/diffs` for file/version comparisons, and `@wterm/react` for the terminal surface.
13 changes: 7 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 8 additions & 7 deletions DEMO.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ just run sqlite shell --release --client f1
```

```sql
-- Low-level volume demo. Repository workflows use the graft CLI.
-- volume on s3
pragma graft_clone = '74ggoCYV4P-2r2cmkXpB2nJ5';
pragma graft_debug_volume_clone = '74ggoCYV4P-2r2cmkXpB2nJ5';
-- volume on fs
pragma graft_clone = '74ggoDeWBQ-2o7qkWGgrdYDn.';
pragma graft_debug_volume_clone = '74ggoDeWBQ-2o7qkWGgrdYDn.';

pragma graft_pull;
pragma graft_push;
pragma graft_info;
pragma graft_status;
pragma graft_audit;
pragma graft_debug_volume_pull;
pragma graft_debug_volume_push;
pragma graft_debug_volume_info;
pragma graft_debug_volume_status;
pragma graft_debug_volume_audit;

-- get the total balance of all accounts
SELECT SUM(balance) FROM accounts;
Expand Down
49 changes: 22 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,42 +100,37 @@ graft push
Prebuilt CLI and SQLite extension archives are published on the
[GitHub releases page](https://github.com/eidos-space/graft/releases).

## Use From SQLite

The Graft SQLite extension lets applications call repository operations through
SQLite pragmas, which makes the workflow available from Electron, Node.js,
Python, Ruby, Swift, and any runtime with native SQLite support.

```sql
pragma graft_init;
pragma graft_add = '--all';
pragma graft_commit = 'Initial version';
pragma graft_json_status;
pragma graft_json_log;
pragma graft_json_diff = '--rows HEAD';
pragma graft_json_fetch;
pragma graft_json_pull;
pragma graft_json_push;
```
## Use With SQLite

Conflict-oriented pragmas expose structured state for app UIs:
The default integration uses ordinary SQLite files. Electron, Node.js, Python,
Ruby, Swift, Rust, and standard SQLite tools can open the worktree database
without a custom VFS. After a transaction commits, stage the database with the
CLI:

```sql
pragma graft_json_conflicts;
pragma graft_json_resolve_conflict = '--theirs assets/model.bin';
pragma graft_json_resolve_conflict = '--theirs --row docs 42';
pragma graft_merge_continue = 'Merge remote changes';
pragma graft_merge_abort;
```bash
sqlite3 data.sqlite "INSERT INTO notes(id, body) VALUES ('1', 'hello')"
graft add data.sqlite
graft commit -m "Add first note"
```

`graft add` takes a consistent SQLite backup, including committed WAL frames,
then compares it with the staged or committed snapshot. Only changed 4 KiB
pages are written to Graft storage; an unchanged database creates no new
storage commit.

The SQLite extension remains available for applications that deliberately want
the Graft VFS as a live page-storage data plane. It exposes version and
`graft_debug_*` diagnostics, not repository commands. Use the CLI and its JSON
output for status, staging, history, merge, and sync.

## Learn More

- [CLI quickstart](./docs/src/content/docs/docs/get-started/cli.mdx)
- [SQLite extension guide](./docs/src/content/docs/docs/get-started/sqlite-extension.mdx)
- [CLI quickstart](./docs/src/content/docs/docs/quickstart/cli.mdx)
- [SQLite extension guide](./docs/src/content/docs/docs/quickstart/sqlite-extension.mdx)
- [App state versioning](./docs/src/content/docs/docs/concepts/app-state-versioning.mdx)
- [Repository model](./docs/src/content/docs/docs/concepts/repository-model.mdx)
- [CLI reference](./docs/src/content/docs/docs/reference/cli.mdx)
- [Pragmas reference](./docs/src/content/docs/docs/reference/pragmas.mdx)
- [VFS pragmas reference](./docs/src/content/docs/docs/reference/pragmas.mdx)
- [Configuration reference](./docs/src/content/docs/docs/reference/configuration.mdx)

## Development
Expand Down
2 changes: 1 addition & 1 deletion crates/graft-ext/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graft-ext"
version = "0.6.1"
version = "0.7.0"
edition = "2024"
authors = { workspace = true }
license = { workspace = true }
Expand Down
10 changes: 7 additions & 3 deletions crates/graft-sqlite/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graft-sqlite"
version = "0.6.1"
version = "0.7.0"
edition = "2024"
authors = { workspace = true }
license = { workspace = true }
Expand All @@ -14,7 +14,7 @@ description = "A SQLite extension which uses Graft to replicate to and from obje
workspace = true

[dependencies]
graft = { path = "../graft", version = "0.6.1" }
graft = { path = "../graft", version = "0.7.0" }
serde = { workspace = true, features = ["derive"] }
serde_json = "1.0"

Expand All @@ -23,8 +23,9 @@ enum_dispatch = { workspace = true }
indoc = { workspace = true }
itertools = { workspace = true }
parking_lot = { workspace = true }
rusqlite = { workspace = true }
rusqlite = { workspace = true, features = ["backup"] }
sqlite-plugin = { workspace = true, default-features = false }
tempfile = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tryiter = { workspace = true }
Expand All @@ -36,3 +37,6 @@ default = []
# statically setup in a Rust project
register-static = ["sqlite-plugin/static"]
bundled-sqlite = ["rusqlite/bundled"]
# Keeps the pre-migration repository/VFS integration suite available without widening the
# production SQLite extension surface. Only graft-test enables this constructor.
test-repository-pragmas = []
43 changes: 42 additions & 1 deletion crates/graft-sqlite/src/file/vol_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{
fmt::Debug,
hash::{DefaultHasher, Hash, Hasher},
mem,
path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicBool, AtomicUsize, Ordering},
Expand Down Expand Up @@ -123,6 +124,7 @@ pub struct VolFile {
repo_runtimes: Arc<RepoRuntimeRegistry>,
workspace: Arc<WorkspaceCoordinator>,
binding_enabled: bool,
repository_database: Option<PathBuf>,
tag_bound: bool,
workspace_writer_active: bool,

Expand All @@ -143,6 +145,32 @@ impl Debug for VolFile {
}

impl VolFile {
pub(crate) fn new_repository_session(
runtime: Runtime,
tag: String,
repository_database: Option<PathBuf>,
repo: Option<Repository>,
repo_runtimes: Arc<RepoRuntimeRegistry>,
) -> Result<Self, ErrCtx> {
let volume = runtime.volume_open(None, None, None)?;
let mut file = Self::new_workspace_session(
runtime,
tag,
volume.vid,
OpenOpts::new(
sqlite_plugin::vars::SQLITE_OPEN_MAIN_DB
| sqlite_plugin::vars::SQLITE_OPEN_READWRITE
| sqlite_plugin::vars::SQLITE_OPEN_CREATE,
),
Arc::new(Mutex::new(())),
repo,
repo_runtimes,
Arc::new(WorkspaceCoordinator::default()),
);
file.repository_database = repository_database;
Ok(file)
}

pub fn new(
runtime: Runtime,
tag: String,
Expand All @@ -166,7 +194,7 @@ impl VolFile {
)
}

pub fn new_workspace_session(
pub(crate) fn new_workspace_session(
runtime: Runtime,
tag: String,
vid: VolumeId,
Expand Down Expand Up @@ -210,6 +238,7 @@ impl VolFile {
repo_runtimes,
workspace,
binding_enabled,
repository_database: None,
tag_bound: binding_enabled,
workspace_writer_active: false,
reserved,
Expand All @@ -222,6 +251,18 @@ impl VolFile {
&self.runtime
}

/// Returns the `SQLite` database selected for row-aware repository operations.
///
/// VFS-backed files use their open database path. A control-plane-only repository session has
/// no current volume binding, so the CLI passes its optional `--db` path separately.
pub(crate) fn repository_database_path(&self) -> Option<&Path> {
if self.binding_enabled {
Some(Path::new(&self.tag))
} else {
self.repository_database.as_deref()
}
}

pub fn attach_repo(&mut self, repo: Repository) -> Result<(), ErrCtx> {
if !self.is_idle() {
return Err(ErrCtx::InvalidVolumeState);
Expand Down
Loading
Loading