From 1ac128db2c24158120e0bd6d2aa6220b1aeb9044 Mon Sep 17 00:00:00 2001 From: Snow Lee Date: Tue, 7 Jul 2026 15:02:40 -0700 Subject: [PATCH] rename: sfs -> BearDrive everywhere; CLI becomes bdrive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Product and project are BearDrive; the CLI binary is bdrive (bdrive mnt, bdrive sync, ...), the web viewer is bdrive-web. All conventions follow the full name: .beardrive settings file, .beardriveignore, ~/.beardrive home, BEARDRIVE_HOME, .beardrive-conflict-* / .beardrive-tmp-* files. Plugin/skill/marketplace renamed to beardrive (/beardrive:mount). Module path is now github.com/runbear-io/beardrive — merge only after renaming the GitHub repo, or go install breaks. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HHEUaYfFHhmDvqLYw74Ehz --- .claude-plugin/marketplace.json | 8 +- .claude/settings.json | 2 +- .claude/skills/beardrive | 1 + .claude/skills/sfs | 1 - .gitignore | 3 +- .goreleaser.yaml | 18 +- CLAUDE.md | 30 +-- README.md | 108 +++++------ cmd/{sfs-web => bdrive-web}/main.go | 26 +-- cmd/{sfs => bdrive}/cmds.go | 16 +- cmd/{sfs => bdrive}/helpers.go | 16 +- cmd/{sfs => bdrive}/main.go | 16 +- cmd/{sfs => bdrive}/mount.go | 34 ++-- go.mod | 2 +- internal/config/config.go | 12 +- internal/config/project.go | 16 +- internal/daemon/daemon.go | 16 +- internal/journal/journal.go | 2 +- internal/remote/local.go | 4 +- internal/remote/remote.go | 2 +- internal/store/store.go | 8 +- internal/store/store_test.go | 12 +- internal/syncer/filter_sync_test.go | 14 +- internal/syncer/ignore.go | 8 +- internal/syncer/syncer.go | 20 +- internal/syncer/syncer_test.go | 8 +- internal/webapp/dir.go | 8 +- internal/webapp/dir_test.go | 2 +- internal/webapp/server.go | 10 +- internal/webapp/server_test.go | 6 +- internal/webapp/static/app.js | 4 +- plugin/.claude-plugin/plugin.json | 10 +- plugin/commands/mount.md | 18 +- plugin/commands/status.md | 14 +- plugin/hooks/hooks.json | 6 +- .../{sfs-sync.sh => beardrive-sync.sh} | 8 +- plugin/skills/{sfs => beardrive}/SKILL.md | 182 +++++++++--------- 37 files changed, 336 insertions(+), 335 deletions(-) create mode 120000 .claude/skills/beardrive delete mode 120000 .claude/skills/sfs rename cmd/{sfs-web => bdrive-web}/main.go (74%) rename cmd/{sfs => bdrive}/cmds.go (92%) rename cmd/{sfs => bdrive}/helpers.go (79%) rename cmd/{sfs => bdrive}/main.go (79%) rename cmd/{sfs => bdrive}/mount.go (81%) rename plugin/scripts/{sfs-sync.sh => beardrive-sync.sh} (57%) rename plugin/skills/{sfs => beardrive}/SKILL.md (51%) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 75ba798..6c9e73c 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,12 +1,12 @@ { - "name": "sfs", + "name": "beardrive", "owner": { "name": "runbear", "email": "snow@runbear.io" }, - "description": "BearDrive (sfs) — a synced file system for AI agents", + "description": "BearDrive (beardrive) — a synced file system for AI agents", "plugins": [ { - "name": "sfs", + "name": "beardrive", "source": "./plugin", - "description": "BearDrive: mount folders that stay in sync across devices through S3/GCS/any object store. Installs the sfs skill, /sfs:mount and /sfs:status commands, and turn-boundary sync hooks in one step.", + "description": "BearDrive: mount folders that stay in sync across devices through S3/GCS/any object store. Installs the beardrive skill, /beardrive:mount and /beardrive:status commands, and turn-boundary sync hooks in one step.", "category": "workflow", "tags": ["sync", "files", "workspace", "s3", "gcs", "agents"] } diff --git a/.claude/settings.json b/.claude/settings.json index 889e3b9..822b110 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -4,7 +4,7 @@ "Bash(go build:*)", "Bash(go test:*)", "Bash(go vet:*)", - "Bash(go run ./cmd/sfs:*)", + "Bash(go run ./cmd/bdrive:*)", "Bash(gofmt:*)", "Bash(go mod tidy)", "Bash(go doc:*)", diff --git a/.claude/skills/beardrive b/.claude/skills/beardrive new file mode 120000 index 0000000..f341deb --- /dev/null +++ b/.claude/skills/beardrive @@ -0,0 +1 @@ +../../plugin/skills/beardrive \ No newline at end of file diff --git a/.claude/skills/sfs b/.claude/skills/sfs deleted file mode 120000 index 436f55b..0000000 --- a/.claude/skills/sfs +++ /dev/null @@ -1 +0,0 @@ -../../plugin/skills/sfs \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8452385..a76c44f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -/sfs +/bdrive +/bdrive-web /dist/ .DS_Store .omc diff --git a/.goreleaser.yaml b/.goreleaser.yaml index f5d1c74..f12c6fa 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,15 +1,15 @@ # Release automation: `goreleaser release` on a tagged commit builds # macOS/Linux binaries and publishes the Homebrew formula to # runbear-io/homebrew-tap, enabling `brew install runbear-io/tap/beardrive`. -# The product is BearDrive; the binaries keep their short names (sfs, sfs-web). +# The product is BearDrive; the binaries keep their short names (bdrive, bdrive-web). version: 2 project_name: beardrive builds: - - id: sfs - main: ./cmd/sfs - binary: sfs + - id: beardrive + main: ./cmd/bdrive + binary: bdrive env: - CGO_ENABLED=0 goos: @@ -20,9 +20,9 @@ builds: - arm64 ldflags: - -s -w -X main.version={{.Version}} - - id: sfs-web - main: ./cmd/sfs-web - binary: sfs-web + - id: bdrive-web + main: ./cmd/bdrive-web + binary: bdrive-web env: - CGO_ENABLED=0 goos: @@ -46,11 +46,11 @@ brews: repository: owner: runbear-io name: homebrew-tap - homepage: https://github.com/runbear-io/sfs + homepage: https://github.com/runbear-io/beardrive description: "BearDrive: a synced file system for AI agents — mount, sync, and track folders" license: MIT test: | - assert_match "sfs", shell_output("#{bin}/sfs version") + assert_match "beardrive", shell_output("#{bin}/bdrive version") changelog: sort: asc diff --git a/CLAUDE.md b/CLAUDE.md index bc041d9..7d83a33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,9 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What this is -**BearDrive** is the product name; `sfs` is its CLI (and the historical project name — binary names, `.sfs`/`.sfsignore` files, and `~/.sfs` stay `sfs` deliberately). `sfs` is a Go CLI that mounts any folder as a synced volume: contents sync across devices through cloud object storage (S3, GCS, S3-compatible, or a plain directory), with per-file change history and offline support. No server — devices converge through append-only journals in a dumb object store. +**BearDrive** is the product name; **`bdrive`** is its CLI binary (file conventions use the full name: `.beardrive`, `.beardriveignore`, `~/.beardrive`, `BEARDRIVE_HOME`). BearDrive is a Go CLI that mounts any folder as a synced volume: contents sync across devices through cloud object storage (S3, GCS, S3-compatible, or a plain directory), with per-file change history and offline support. No server — devices converge through append-only journals in a dumb object store. -The repo ships two binaries from one Go module: `cmd/sfs` (the CLI + sync daemon) and `cmd/sfs-web` (a read-only web viewer for a remote). +The repo ships two binaries from one Go module: `cmd/bdrive` (the CLI + sync daemon) and `cmd/bdrive-web` (a read-only web viewer for a remote). ## Commands @@ -15,19 +15,19 @@ go build ./... # build everything go test ./... # run all tests go test ./internal/syncer -run TestConflict -v # run a single test go vet ./... # vet -go build -o sfs ./cmd/sfs # build the binary (gitignored at repo root) +go build -o bdrive ./cmd/bdrive # build the binary (gitignored at repo root) ``` -There is no Makefile, linter config, or CI config in-repo. Releases run `goreleaser release` on a tagged commit (see `.goreleaser.yaml`); the version is injected via `-ldflags "-X main.version=..."` into `cmd/sfs/main.go`. +There is no Makefile, linter config, or CI config in-repo. Releases run `goreleaser release` on a tagged commit (see `.goreleaser.yaml`); the version is injected via `-ldflags "-X main.version=..."` into `cmd/bdrive/main.go`. -When testing the CLI manually, set `SFS_HOME=/some/tmp/dir` to relocate all sfs state (device identity, mount registry, volume stores) away from the real `~/.sfs`. +When testing the CLI manually, set `BEARDRIVE_HOME=/some/tmp/dir` to relocate all beardrive state (device identity, mount registry, volume stores) away from the real `~/.beardrive`. ## Architecture Data flows in two hops; the local volume store is the pivot: ``` -working folder ←scan/materialize→ volume store (~/.sfs/volumes/) ←push/pull→ object store +working folder ←scan/materialize→ volume store (~/.beardrive/volumes/) ←push/pull→ object store (real files) blobs/ + journal/ + state + sync s3:// gs:// file:// ``` @@ -36,12 +36,12 @@ Package roles (`internal/`): - **`journal`** — the core data model. Every change is an `Op` (`put`/`delete`) in a per-device append-only JSONL log. `Less` defines the total order `(lamport, time, device, seq)`; `Replay` folds all ops into the volume state, last-writer-wins per path. Everything else is machinery around this. - **`store`** — a volume's local on-disk state: content-addressed blob store (`blobs//`), per-device journal copies, the per-mount materialization cache (`state-.json`, size+mtime fingerprints for cheap change detection), sync state (lamport clock + push cursor), and the exclusive flock that serializes cycles. - **`remote`** — the `Backend` interface (Put/Get/List/Exists) with `file://`, `s3://`, `gs://` implementations. Remote layout: `blobs/` + `journal/.jsonl` under the URL prefix. -- **`syncer`** — the heart: `Session.Cycle()` runs one pass: scan → commit local ops → pull peer journals → preserve conflict copies → materialize merged state → push blobs + own journal. Read the package doc comment in `syncer.go` first. `ignore.go` holds the path filter (`.sfsignore` rules + the `.sfs` include list), applied symmetrically in scan and materialize; a newly filtered path is dropped from the cache *without* a delete op so opting out locally never deletes remotely. -- **`daemon`** — per-mount background loop (detached process, pidfile `daemon-.pid` and log `daemon-.log` in the volume dir). Scans every `--scan-interval` (3s), talks to the remote every `--remote-interval` (10s) or immediately after local edits. Re-reads `mounts.json` each tick to pick up `sfs remote set` / `umnt --forget` without restart. -- **`config`** — global state under `$SFS_HOME` (default `~/.sfs`): device identity (`device.json`), mount registry (`mounts.json`), `MountID()` (sha256 of the folder path — one volume can be mounted at several folders, and everything folder-specific is keyed by it). Also the per-folder `.sfs` project file (`project.go`): volume/remote/include settings that live in the mounted folder itself, win over the registry (`EffectiveMount`), and are never synced. -- **`webapp`** — the `sfs-web` server: a `Source` interface with two implementations — `DirSource` (serves a local folder straight from disk; the default when no remote is given) and `RemoteSource` (reads journals straight from the remote, no local store, folds them into a file tree with per-file provenance). Renders markdown (goldmark + Obsidian `[[wikilinks]]`), streams/downloads content. Frontend is dependency-free vanilla JS embedded via `go:embed static`. +- **`syncer`** — the heart: `Session.Cycle()` runs one pass: scan → commit local ops → pull peer journals → preserve conflict copies → materialize merged state → push blobs + own journal. Read the package doc comment in `syncer.go` first. `ignore.go` holds the path filter (`.beardriveignore` rules + the `.beardrive` include list), applied symmetrically in scan and materialize; a newly filtered path is dropped from the cache *without* a delete op so opting out locally never deletes remotely. +- **`daemon`** — per-mount background loop (detached process, pidfile `daemon-.pid` and log `daemon-.log` in the volume dir). Scans every `--scan-interval` (3s), talks to the remote every `--remote-interval` (10s) or immediately after local edits. Re-reads `mounts.json` each tick to pick up `bdrive remote set` / `umnt --forget` without restart. +- **`config`** — global state under `$BEARDRIVE_HOME` (default `~/.beardrive`): device identity (`device.json`), mount registry (`mounts.json`), `MountID()` (sha256 of the folder path — one volume can be mounted at several folders, and everything folder-specific is keyed by it). Also the per-folder `.beardrive` project file (`project.go`): volume/remote/include settings that live in the mounted folder itself, win over the registry (`EffectiveMount`), and are never synced. +- **`webapp`** — the `bdrive-web` server: a `Source` interface with two implementations — `DirSource` (serves a local folder straight from disk; the default when no remote is given) and `RemoteSource` (reads journals straight from the remote, no local store, folds them into a file tree with per-file provenance). Renders markdown (goldmark + Obsidian `[[wikilinks]]`), streams/downloads content. Frontend is dependency-free vanilla JS embedded via `go:embed static`. -`cmd/sfs/` is a thin cobra CLI over these packages (`mnt`, `umnt`, `sync`, `status`, `log`, `remote`, `whoami`, `daemon`, `version`); `cmd/sfs-web/` wraps `webapp` with flags. +`cmd/bdrive/` is a thin cobra CLI over these packages (`mnt`, `umnt`, `sync`, `status`, `log`, `remote`, `whoami`, `daemon`, `version`); `cmd/bdrive-web/` wraps `webapp` with flags. ## Invariants — do not break these @@ -50,8 +50,8 @@ Package roles (`internal/`): - **Scan happens before pull** in `Cycle`, so local edits are journaled (and content captured) before remote state can overwrite the working folder. - **Replay must stay deterministic.** Any change to `journal.Less` or `Replay` changes what every device converges to. - **Materialize never clobbers dirty files**: a file whose size/mtime differs from the state cache changed mid-cycle and is left for the next scan. -- **All state files are written atomically** (temp file + rename, see `store.WriteFileAtomic`). Temp files are prefixed `.sfs-tmp-` and ignored by the scanner. -- **`Cycle` runs under the volume flock** — the daemon and one-shot CLI commands (`sfs sync`) coexist through it. +- **All state files are written atomically** (temp file + rename, see `store.WriteFileAtomic`). Temp files are prefixed `.beardrive-tmp-` and ignored by the scanner. +- **`Cycle` runs under the volume flock** — the daemon and one-shot CLI commands (`bdrive sync`) coexist through it. - Errors during pull/push degrade to `Result.Offline` rather than failing the cycle; unreadable/vanished files during scan are skipped and retried next cycle. Follow this "never break sync, retry next cycle" posture. ## Testing conventions @@ -60,8 +60,8 @@ The real coverage is the integration tests in `internal/syncer/syncer_test.go`: ## Claude Code plugin -`plugin/` is a Claude Code plugin (skill + `/sfs:mount` + `/sfs:status` commands + turn-boundary sync hooks), published via the marketplace manifest at `.claude-plugin/marketplace.json` (`/plugin marketplace add runbear-io/sfs`). The canonical skill lives at `plugin/skills/sfs/SKILL.md`; `.claude/skills/sfs` is a symlink to it. The hook script `plugin/scripts/sfs-sync.sh` must stay a fast no-op for folders without a `.sfs` file — it runs on every turn in every project. +`plugin/` is a Claude Code plugin (skill + `/beardrive:mount` + `/beardrive:status` commands + turn-boundary sync hooks), published via the marketplace manifest at `.claude-plugin/marketplace.json` (`/plugin marketplace add runbear-io/beardrive`). The canonical skill lives at `plugin/skills/beardrive/SKILL.md`; `.claude/skills/beardrive` is a symlink to it. The hook script `plugin/scripts/beardrive-sync.sh` must stay a fast no-op for folders without a `.beardrive` file — it runs on every turn in every project. ## Docs to keep in sync -- `README.md` and `plugin/skills/sfs/SKILL.md` both document CLI behavior, flags, output formats, and the on-disk layout. When changing CLI commands, flags, output, or layout, update both — the skill is what makes Claude Code sfs-aware for end users and must match the actual binary. +- `README.md` and `plugin/skills/beardrive/SKILL.md` both document CLI behavior, flags, output formats, and the on-disk layout. When changing CLI commands, flags, output, or layout, update both — the skill is what makes Claude Code beardrive-aware for end users and must match the actual binary. diff --git a/README.md b/README.md index e973cbf..c51f1bf 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **BearDrive** mounts any folder as a synced volume: its contents stay synchronized across all your devices through cloud object storage, every change is tracked (who, when, on which device), and everything keeps -working offline. The CLI is `sfs` — short for synced file system. +working offline. The CLI is `bdrive`. It is built for AI agent workflows — give your agents on every machine the same `~/agent-workspace`, and notes, plans, memory files, and artifacts @@ -11,7 +11,7 @@ follow them everywhere, with a full audit trail of which agent or human changed what. ```console -$ sfs mnt ./workspace --remote s3://my-bucket/workspace +$ bdrive mnt ./workspace --remote s3://my-bucket/workspace mounted /Users/snow/workspace volume: workspace remote: s3://my-bucket/workspace @@ -22,19 +22,19 @@ mounted /Users/snow/workspace On another machine: ```console -$ sfs mnt ./workspace --remote s3://my-bucket/workspace +$ bdrive mnt ./workspace --remote s3://my-bucket/workspace # … the same files appear, and stay in sync from now on ``` ## Features -- **Mount anywhere** — `sfs mnt ./folder` turns any folder into a synced +- **Mount anywhere** — `bdrive mnt ./folder` turns any folder into a synced volume. Files are *real files on disk*: every tool, editor, and agent can use them with zero integration work. - **Multi-device sync** — devices converge through a shared remote. Each device only writes its own append-only journal, so no locking service or server is needed — any object store works. -- **Change tracking** — `sfs log` shows which device and author changed +- **Change tracking** — `bdrive log` shows which device and author changed which file, when. Content is stored content-addressed, so history is never lost, even for overwritten or deleted files. - **Cloud-provider agnostic** — Amazon S3 (`s3://`), Google Cloud Storage @@ -45,52 +45,52 @@ $ sfs mnt ./workspace --remote s3://my-bucket/workspace reachable again. - **Conflict-safe** — concurrent edits resolve deterministically (last-writer-wins), and the losing version is preserved as a - `name.sfs-conflict--