From 118abc67e261134ff15e9b28c633e7b74c9555ec Mon Sep 17 00:00:00 2001 From: "Snow Lee (Sungwon)" Date: Thu, 30 Jul 2026 17:46:58 +0900 Subject: [PATCH] feat(cli): "bdrive serve" replaces "bdrive web" (web stays as an alias), and the README leads with the agent install (#94) --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- CLAUDE.md | 12 +- CONTRIBUTING.md | 2 +- README.md | 155 +++++++++++------- architecture/README.md | 2 +- architecture/overview.md | 2 +- architecture/webapp-server.md | 2 +- cmd/bdrive/main.go | 2 +- cmd/bdrive/web.go | 19 ++- sandbox/boot.sh | 2 +- web/docs/src/content/docs/index.md | 2 +- web/docs/src/content/docs/reference/cli.md | 2 +- .../src/content/docs/reference/hub-config.md | 14 +- .../src/content/docs/reference/migration.md | 2 +- .../docs/self-hosting/authentication.md | 2 +- .../content/docs/self-hosting/run-a-hub.md | 2 +- 16 files changed, 130 insertions(+), 94 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6a85838..03aa65b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -23,7 +23,7 @@ body: label: Where does it happen? options: - Syncing client (bdrive CLI / daemon) - - Self-hosted hub (bdrive web) + - Self-hosted hub (bdrive serve) - Web UI in the browser - Claude Code plugin / agent hooks - Not sure diff --git a/CLAUDE.md b/CLAUDE.md index 536688b..25d8cbb 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; **`bdrive`** is its CLI binary (file conventions: the `.bdrive/` settings directory and `.bdriveignore` at the project root, `~/.bdrive` home, `BDRIVE_HOME`). BearDrive is a Go CLI that mounts any folder as a synced volume: contents sync across devices and teammates through a **`bdrive web` hub**, with accounts, per-file change history, and offline support. Clients are storage-blind — they sync through the hub over `https://` and never hold storage credentials; the hub owns the object store (S3, GCS, S3-compatible, or a plain directory) and devices converge through append-only journals in it. (Direct client-to-bucket sync without a hub is no longer supported; the object-storage backends exist only as the hub's own storage.) +**BearDrive** is the product name; **`bdrive`** is its CLI binary (file conventions: the `.bdrive/` settings directory and `.bdriveignore` at the project root, `~/.bdrive` home, `BDRIVE_HOME`). BearDrive is a Go CLI that mounts any folder as a synced volume: contents sync across devices and teammates through a **`bdrive serve` hub**, with accounts, per-file change history, and offline support. Clients are storage-blind — they sync through the hub over `https://` and never hold storage credentials; the hub owns the object store (S3, GCS, S3-compatible, or a plain directory) and devices converge through append-only journals in it. (Direct client-to-bucket sync without a hub is no longer supported; the object-storage backends exist only as the hub's own storage.) -The repo ships one binary: `cmd/bdrive` — the CLI, the sync daemon, and the web server (`bdrive web`: viewer, uploads, multi-project sync hub). +The repo ships one binary: `cmd/bdrive` — the CLI, the sync daemon, and the web server (`bdrive serve`: viewer, uploads, multi-project sync hub). ## Commands @@ -40,14 +40,14 @@ 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://`, and `https://` implementations (`https://` syncs through a `bdrive web` server's `/api/store` API — the client device holds no storage credentials). `PutSigner` is the optional presign capability (S3/GCS). Remote layout: `blobs/` + `journal/.jsonl` under the URL prefix. +- **`remote`** — the `Backend` interface (Put/Get/List/Exists) with `file://`, `s3://`, `gs://`, and `https://` implementations (`https://` syncs through a `bdrive serve` server's `/api/store` API — the client device holds no storage credentials). `PutSigner` is the optional presign capability (S3/GCS). 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 (`.bdriveignore` rules + the `.bdrive` 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, `daemon.pid`/`daemon.log`/`daemon.lock` in the mount's volume dir). Scans every `--scan-interval` (3s), talks to the remote every `--remote-interval` (10s) or immediately after local edits. Re-reads `.bdrive/config.json` each tick; if it vanishes (folder moved/renamed/deleted) the daemon **exits cleanly without propagating deletes** — the next bdrive command at the new location resumes it (self-heal on next touch). **Liveness is the flock on `daemon.lock`, never the pidfile**: the pidfile outlives its process (it lives in `$BDRIVE_HOME`, which survives reboots), so a recycled pid used to read as a live daemon — making `status` lie and `Start` a silent no-op, which broke the one documented recovery. The kernel drops the flock when the holder dies, including at reboot and on a crash; the pid is display-only, and holding the lock also makes two daemons on one mount impossible (two writers of one journal). - **`autostart`** — the login registration that undoes a reboot: one unit per machine, user-level, running `bdrive resume` — macOS `~/Library/LaunchAgents/ai.beardrive.daemon.plist` (`RunAtLoad`, deliberately no `KeepAlive` since the job exits); Linux `$XDG_CONFIG_HOME/systemd/user/beardrive.service` (`Type=oneshot`, no `Restart=`) plus the `default.target.wants` symlink that `systemctl --user enable` would create, since systemd ignores a unit nothing wants. Linux also requires systemd to be booted (`/run/systemd/system`, i.e. `sd_booted`) — otherwise `Install` returns `ErrUnsupported` rather than writing a file nothing would ever read (Alpine/runit, WSL1, slim containers), which starts a daemon for every enrolled, unpaused mount — so mounts added later need no re-registration and `bdrive stop` still means stay stopped. `init` installs it (`--no-autostart` skips). Writing the file is the whole job: no `launchctl` shell-out, so a test or a packaging script can't register a real login item as a side effect, and launchd loads it at the next login anyway. Windows uses a per-user `HKCU\...\CurrentVersion\Run` value (`golang.org/x/sys/windows/registry`) — no admin, no COM (a Startup `.lnk` would need it), no `schtasks`, and it shows up in Task Manager's Startup tab where a user can disable it. Its tests exist but have **never been executed** (written and compile-checked from macOS); they run the first time the suite runs on Windows. **`GOOS=windows go build ./...` does not pass yet** and this package is not the reason: `internal/store`'s `Lock` uses `syscall.Flock` and `internal/daemon` uses `syscall.Kill`/`Setsid`, all unix-only — a Windows port means `LockFileEx` and a kill story for a platform with no SIGTERM, which is a separate change against the sync invariants. `autostart_other.go` (`!darwin && !linux && !windows`) covers the BSDs with `ErrUnsupported`; shared bits (`writeIfDifferent`, `selfPath`) live in the tag-free `autostart.go`. - **`config`** — global state under `$BDRIVE_HOME` (default `~/.bdrive`): device identity (`device.json`), settings (`settings.json`: default server + device token + signed-in account), and the mount registry (`mounts.json`, keyed by **stable mount id**, holding only each mount's last-known path). The per-folder `.bdrive/` directory (`project.go`) holds `config.json` with the mount id + volume/remote/include; **nothing is keyed by the folder path**, so renames/moves are free — `ResolveMount` self-heals the registry path, and the volume store lives at `~/.bdrive/volumes//`. `.bdrive/` is never synced and holds no credentials. -- **`webapp`** — the `bdrive web` server, in two modes. Single-volume: `Source` is a `DirSource` (plain folder from disk) or `RemoteSource` (folds journals into a file tree with per-file provenance). Hub: `Root` + `Projects` host many projects on one storage root, each under `//` via `remote.Prefixed`; `ProjectDB` (`projects.go`) is a file-backed registry (JSON, loaded at open, rewritten atomically per change) with create-or-join-by-name semantics, name-scoped per organization. Orgs (`orgs.go`, file-backed `orgs.json`) wall projects by membership (email → owner|member): every per-project route — viewer APIs, uploads, history, shares management, the `/store/*` sync proxy — 403s for non-members, `/api/projects` lists only your orgs' projects, owners mint expiring multi-use invite links (`/join/`), and a pre-org hub migrates all projects into a "default" org (all existing accounts join, oldest owns) at startup. `QuotaProvider` (`quota.go`) is the plan-enforcement seam mirroring `AuthProvider` — CheckWrite/RecordUsage on every write path, CheckSeat on invite redemption; OSS ships only `UnlimitedQuota`, managed deployments swap the provider. Renders markdown (goldmark + Obsidian `[[wikilinks]]`). With `--upload` it accepts writes: browser uploads (`upload.go` — direct-to-storage via presigned URLs when the backend implements `remote.PutSigner`, relayed otherwise; ops journaled under the server's own device) and the per-project `/api/p//store/*` proxy (`store.go`) that whole devices sync through — the `https://` remote backend (`remote/http.go`) is its client; journals are never presigned, only immutable blobs. Frontend is a React + TypeScript app (`webapp/frontend/`, Vite + Tailwind v4 + shadcn/ui — Radix-based components copied into `src/components/ui`, themed from the BearDrive tokens in `src/tw.css`; TanStack query/table/virtual, react-hook-form + zod, cmdk, sonner, lucide-react) whose **built output is committed** at `webapp/static/` — the `go:embed static` target — so plain `go build` needs no Node; after any `frontend/src` change run `npm run build` there and commit the new `static/` (`frontend/check-dist.sh` verifies freshness; e2e suite: `npm run e2e` — Playwright against the seeded harness in `e2e_serve_test.go`, port 8993). It learns everything from `/api/config` (+ `/api/projects` in hub mode) and never sees storage info or credentials. It uses native History-API path routing (`//` in hub mode, `/` in volume mode, `/join/` for invites — no `#`, slashes stay literal). **Every user-facing page owns a URL path**: new surfaces are view routes (`//{dashboard|history|install|settings}[/]`, `VIEW_ROUTES` in `router.ts`; renamed segments live in `LEGACY_VIEWS` and are normalized away on arrival) so deep links, reload, and back/forward always work — never URL-less panel state (the org/hub admin panels are the legacy exceptions; don't add more) implemented by the in-repo synchronous router `frontend/src/nav.ts` + `router.ts` (deliberately NOT a router library: react-router v7's startTransition navigation left stale views on screen); `Server.frontend` serves `index.html` as the SPA fallback for any non-asset, non-API/auth/share route so deep links and refreshes resolve (hashed `assets/*` are cached immutable, everything else no-cache), and all client API/asset URLs are root-absolute so a deep path doesn't break relative resolution. Rendered markdown is transformed as a string before mounting and link clicks are delegated on the container — never patch the `dangerouslySetInnerHTML` subtree after commit (React re-applies the markup on unrelated updates and discards DOM patches). **Read heat** (`reads.go`): a `ReadLedger` (hub-only, nil = off, config `reads` block) aggregates read telemetry into daily per-actor buckets, debounced to 10-minute visits, folded into all-time rows past `retention_days` — viewer file/render/download = human (recorded via the project id the `proj()` resolver stashes in the request context), `/s/*` hits = share, device-reported reads (`POST /api/p//reads`) = agent; `/store/*` replication and history `/blob` views are NEVER reads. `GET /api/p//heat?prefix=&days=` returns counts/distinct-readers/last-read only — actor identities (the email/device/token in the buckets) must never appear in an API response. Recording and flushing degrade silently (log once); telemetry must never fail a request or a sync cycle. The frontend shows heat dots on folder listings and a per-project Dashboard quadrant (reads × staleness, route `//dashboard`) — both visible to every project member, since `/heat` is membership-gated and identity-free. **Hub metadata persistence** (accounts, projects, orgs+invites, shares, devices, read buckets — never blobs or journals) sits behind a pluggable `MetaStore` of typed repos (`db.go`): the service structs (`BuiltinAuth`, `OrgDB`, `ProjectDB`, `ShareDB`, `DeviceRegistry`, `ReadLedger`) keep their in-memory maps + logic and persist each change as one record through a repo (the `ReadRepo` alone is batch-oriented — one flush, one write). Two backends — `db_file.go` (the historical JSON files, still the zero-dep default, reached via the `Open*(path)` constructors) and `db_sql.go` (one `database/sql` impl over pure-Go drivers: `modernc.org/sqlite` locally, `jackc/pgx` for Postgres/Supabase, portable schema + idempotent migrations + transactional multi-row writes). `web.go`'s `database` config (`{driver:file|sqlite|postgres, dsn}`) selects it; file is default and untouched. `db_conformance_test.go` runs the same service ops against every backend. +- **`webapp`** — the `bdrive serve` server, in two modes. Single-volume: `Source` is a `DirSource` (plain folder from disk) or `RemoteSource` (folds journals into a file tree with per-file provenance). Hub: `Root` + `Projects` host many projects on one storage root, each under `//` via `remote.Prefixed`; `ProjectDB` (`projects.go`) is a file-backed registry (JSON, loaded at open, rewritten atomically per change) with create-or-join-by-name semantics, name-scoped per organization. Orgs (`orgs.go`, file-backed `orgs.json`) wall projects by membership (email → owner|member): every per-project route — viewer APIs, uploads, history, shares management, the `/store/*` sync proxy — 403s for non-members, `/api/projects` lists only your orgs' projects, owners mint expiring multi-use invite links (`/join/`), and a pre-org hub migrates all projects into a "default" org (all existing accounts join, oldest owns) at startup. `QuotaProvider` (`quota.go`) is the plan-enforcement seam mirroring `AuthProvider` — CheckWrite/RecordUsage on every write path, CheckSeat on invite redemption; OSS ships only `UnlimitedQuota`, managed deployments swap the provider. Renders markdown (goldmark + Obsidian `[[wikilinks]]`). With `--upload` it accepts writes: browser uploads (`upload.go` — direct-to-storage via presigned URLs when the backend implements `remote.PutSigner`, relayed otherwise; ops journaled under the server's own device) and the per-project `/api/p//store/*` proxy (`store.go`) that whole devices sync through — the `https://` remote backend (`remote/http.go`) is its client; journals are never presigned, only immutable blobs. Frontend is a React + TypeScript app (`webapp/frontend/`, Vite + Tailwind v4 + shadcn/ui — Radix-based components copied into `src/components/ui`, themed from the BearDrive tokens in `src/tw.css`; TanStack query/table/virtual, react-hook-form + zod, cmdk, sonner, lucide-react) whose **built output is committed** at `webapp/static/` — the `go:embed static` target — so plain `go build` needs no Node; after any `frontend/src` change run `npm run build` there and commit the new `static/` (`frontend/check-dist.sh` verifies freshness; e2e suite: `npm run e2e` — Playwright against the seeded harness in `e2e_serve_test.go`, port 8993). It learns everything from `/api/config` (+ `/api/projects` in hub mode) and never sees storage info or credentials. It uses native History-API path routing (`//` in hub mode, `/` in volume mode, `/join/` for invites — no `#`, slashes stay literal). **Every user-facing page owns a URL path**: new surfaces are view routes (`//{dashboard|history|install|settings}[/]`, `VIEW_ROUTES` in `router.ts`; renamed segments live in `LEGACY_VIEWS` and are normalized away on arrival) so deep links, reload, and back/forward always work — never URL-less panel state (the org/hub admin panels are the legacy exceptions; don't add more) implemented by the in-repo synchronous router `frontend/src/nav.ts` + `router.ts` (deliberately NOT a router library: react-router v7's startTransition navigation left stale views on screen); `Server.frontend` serves `index.html` as the SPA fallback for any non-asset, non-API/auth/share route so deep links and refreshes resolve (hashed `assets/*` are cached immutable, everything else no-cache), and all client API/asset URLs are root-absolute so a deep path doesn't break relative resolution. Rendered markdown is transformed as a string before mounting and link clicks are delegated on the container — never patch the `dangerouslySetInnerHTML` subtree after commit (React re-applies the markup on unrelated updates and discards DOM patches). **Read heat** (`reads.go`): a `ReadLedger` (hub-only, nil = off, config `reads` block) aggregates read telemetry into daily per-actor buckets, debounced to 10-minute visits, folded into all-time rows past `retention_days` — viewer file/render/download = human (recorded via the project id the `proj()` resolver stashes in the request context), `/s/*` hits = share, device-reported reads (`POST /api/p//reads`) = agent; `/store/*` replication and history `/blob` views are NEVER reads. `GET /api/p//heat?prefix=&days=` returns counts/distinct-readers/last-read only — actor identities (the email/device/token in the buckets) must never appear in an API response. Recording and flushing degrade silently (log once); telemetry must never fail a request or a sync cycle. The frontend shows heat dots on folder listings and a per-project Dashboard quadrant (reads × staleness, route `//dashboard`) — both visible to every project member, since `/heat` is membership-gated and identity-free. **Hub metadata persistence** (accounts, projects, orgs+invites, shares, devices, read buckets — never blobs or journals) sits behind a pluggable `MetaStore` of typed repos (`db.go`): the service structs (`BuiltinAuth`, `OrgDB`, `ProjectDB`, `ShareDB`, `DeviceRegistry`, `ReadLedger`) keep their in-memory maps + logic and persist each change as one record through a repo (the `ReadRepo` alone is batch-oriented — one flush, one write). Two backends — `db_file.go` (the historical JSON files, still the zero-dep default, reached via the `Open*(path)` constructors) and `db_sql.go` (one `database/sql` impl over pure-Go drivers: `modernc.org/sqlite` locally, `jackc/pgx` for Postgres/Supabase, portable schema + idempotent migrations + transactional multi-row writes). `web.go`'s `database` config (`{driver:file|sqlite|postgres, dsn}`) selects it; file is default and untouched. `db_conformance_test.go` runs the same service ops against every backend. -`cmd/bdrive/` is a thin cobra CLI over these packages (`login`, `logout`, `init`, `stop`, `sync`, `scope`, `forget`, `status`, `log`, `share`, `export`, `import`, `url`, `hooks`, `resume`, `autostart`, `read-log`, `web`, `whoami`, `daemon`, `version` — `mnt`/`umnt`/`remote` are gone; `init` is the front door and `stop` pauses). `export`/`import` (`migrate.go`) move a whole project between hubs with full fidelity: the archive is the remote store layout (all devices' journals + all blobs) in a tar.gz, streamed through the existing `remote.Backend` — no server-side support needed, so it works against any hub in either direction (the anti-lock-in story for cloud-hesitant users). `bdrive login` signs the device in (bare form uses the remembered server or `config.DefaultServer` = beardrive.ai; loopback-callback browser flow in `login.go`, `--device` for headless) and stores server+token+account in `settings.json`; `bdrive logout` clears the saved token+account (keeps the remembered server unless `--forget`). Switching hubs is `bdrive login ` then re-`init` — `init` is the only thing that writes a folder's remote (always a hub, `server + "/p/" + id`); there is no client command to point a folder at a raw bucket. `bdrive init` is interactive on a TTY (survey menus: create-new vs connect-existing with a project list; whole-folder vs only-some-subfolders) with full flag bypass (`--name/--project/--only/--yes`) and never prompts without a TTY; it runs the login flow first when there is no session, writes `.bdrive/config.json`, seeds `.bdriveignore`, registers agent sync hooks in each platform's USER config (`~/.claude/settings.json` and friends — once per machine, never inside a project: platforms read hook config only from the directory a session starts in, so a per-project file covers only sessions that start there and, living in a mount, would sync to the team; `Install` also migrates away hooks older versions wrote into projects), and starts sync via `startSync`; re-running it resumes — including after a folder move. **A mount is always exactly the folder named** — there is no re-rooting flag. Syncing only part of a mount is `--only wiki,docs`, which writes a bdrive-managed block of `.bdriveignore` negation rules (`cmd/bdrive/scopefile.go`; `bdrive scope add/rm` edits the same block) rather than a second scope mechanism: the old `Include` list in `config.json` is legacy — still honored, never written. Because the rules live in the synced `.bdriveignore`, scope is team-wide, which is why `sync --prune` refuses when `!` rules are present (it would strip everything outside the scope from the hub for everyone; `bdrive forget ` is the per-path tool). `init` also refuses a second folder for a project this device already syncs — one device writes one journal per project, so two mounts would overwrite each other's ops. `bdrive web -c config.json` configures the server from a file, explicit flags winning. +`cmd/bdrive/` is a thin cobra CLI over these packages (`login`, `logout`, `init`, `stop`, `sync`, `scope`, `forget`, `status`, `log`, `share`, `export`, `import`, `url`, `hooks`, `resume`, `autostart`, `read-log`, `serve`, `whoami`, `daemon`, `version` — `mnt`/`umnt`/`remote` are gone; `init` is the front door and `stop` pauses). `export`/`import` (`migrate.go`) move a whole project between hubs with full fidelity: the archive is the remote store layout (all devices' journals + all blobs) in a tar.gz, streamed through the existing `remote.Backend` — no server-side support needed, so it works against any hub in either direction (the anti-lock-in story for cloud-hesitant users). `bdrive login` signs the device in (bare form uses the remembered server or `config.DefaultServer` = beardrive.ai; loopback-callback browser flow in `login.go`, `--device` for headless) and stores server+token+account in `settings.json`; `bdrive logout` clears the saved token+account (keeps the remembered server unless `--forget`). Switching hubs is `bdrive login ` then re-`init` — `init` is the only thing that writes a folder's remote (always a hub, `server + "/p/" + id`); there is no client command to point a folder at a raw bucket. `bdrive init` is interactive on a TTY (survey menus: create-new vs connect-existing with a project list; whole-folder vs only-some-subfolders) with full flag bypass (`--name/--project/--only/--yes`) and never prompts without a TTY; it runs the login flow first when there is no session, writes `.bdrive/config.json`, seeds `.bdriveignore`, registers agent sync hooks in each platform's USER config (`~/.claude/settings.json` and friends — once per machine, never inside a project: platforms read hook config only from the directory a session starts in, so a per-project file covers only sessions that start there and, living in a mount, would sync to the team; `Install` also migrates away hooks older versions wrote into projects), and starts sync via `startSync`; re-running it resumes — including after a folder move. **A mount is always exactly the folder named** — there is no re-rooting flag. Syncing only part of a mount is `--only wiki,docs`, which writes a bdrive-managed block of `.bdriveignore` negation rules (`cmd/bdrive/scopefile.go`; `bdrive scope add/rm` edits the same block) rather than a second scope mechanism: the old `Include` list in `config.json` is legacy — still honored, never written. Because the rules live in the synced `.bdriveignore`, scope is team-wide, which is why `sync --prune` refuses when `!` rules are present (it would strip everything outside the scope from the hub for everyone; `bdrive forget ` is the per-path tool). `init` also refuses a second folder for a project this device already syncs — one device writes one journal per project, so two mounts would overwrite each other's ops. `bdrive serve -c config.json` configures the server from a file, explicit flags winning. Authentication (`webapp/auth.go`, `authlocal.go`, `mail.go`) is **mandatory in hub mode** — the config's `auth` block tunes `users_db`/`allow_signup`/`allowed_domains`/`require_verification`/`require_approval`/`admins`/`smtp`; the plain-folder viewer stays auth-free — and sits behind the `AuthProvider` interface — the OSS server ships only `BuiltinAuth` (email+password accounts and device tokens in a file-backed `auth.json`; bcrypt for passwords, SHA-256 digests for tokens, plaintext never stored; server-owned `/auth/*` pages; one-time codes for the CLI callback and device flows; SMTP reset mail with a log-link fallback). **Signup is invite-only by default** (`allow_signup` defaults false): a valid org invite bootstraps an account even when self-signup is closed — `BuiltinAuth.InviteValid` (wired to `OrgDB.ValidInvite`) lets `pageSignup`/`pageLogin` offer account creation for a `/join/` target, and `signupInvited` skips the domain/verification/approval gates and activates immediately (the invite is the vetting). `BuiltinAuth.ValidateSignupPolicy` (called at hub startup, `web.go`) refuses an ungated open hub and email-verification-without-SMTP rather than silently leaving the door open. The three postures: invite-only (default), approval-gated (`require_approval`), and domain-restricted+verified (`allowed_domains`+`require_verification`+`smtp`); `allow_signup`/`allowed_domains`/`admins` stay server-config-owned so a browser session can't widen access. A managed deployment can swap in a different provider (e.g. PropelAuth) without touching the CLI or API — keep provider-specific code out of this repo. The sync client picks up its token from `BDRIVE_TOKEN` or `settings.json` and sends `X-Bdrive-Device{,-Name,-Os}` headers (`remote/http.go`); the hub's file-backed device registry (`webapp/devices.go`) records per-device name/OS/account/server-observed IP. Journal ops carry the signed-in account (`Op.User`/`UserName` from `Session.Account`; `Author` remains the git/OS fallback). History (`webapp/history.go`): `GET /api/p//history?path=|prefix=` (newest first, device-registry join) and `GET /api/p//blob?sha=` stream any exact version — blobs are retained forever, so the future revert phase is just re-putting an old blob as a new op. Share links (`webapp/shares.go`, file-backed `shares.json`): any signed-in member mints `/s/` public URLs (`bdrive share`, or the UI's Share button) serving the file's LATEST content until revoked (optional expiry); `/s/*` responses are sandboxed (CSP `sandbox allow-scripts`, no auth cookies) so shared HTML can't attack hub sessions — keep that header on any change; `/s/*` also sits behind a per-IP token bucket (`ratelimit.go`, `share_rpm` config), and markdown share pages get a "Shared with BearDrive" footer (raw HTML is never injected into). @@ -96,7 +96,7 @@ Every PR body starts with a `## TL;DR` section: at most 5 bullet points, one sho ## Architecture diagrams in PRs -`architecture/` holds mermaid diagrams covering every application package in the repo: `overview.md` (system diagram — the package map and how the pieces connect), `cli-sync.md` (the CLI and sync engine — `cmd/bdrive` + `internal/{syncer,store,journal,config,daemon,agenthooks,autostart}`), `webapp-server.md` (the `bdrive web` server — `internal/webapp` + `internal/remote`), and `webapp-frontend.md` (the hub's React SPA — `internal/webapp/frontend/src`). Every code change lands in exactly one detail diagram's scope; `overview.md` changes only when packages appear/disappear or the cross-piece wiring changes (`web/docs` and the private `cloud/` repo are deliberately out of scope). Before `gh pr create`: if the branch changes types or relationships drawn in any of them (new/removed types, new seams, changed fields/implements/ownership in those packages), update the affected diagram and commit it on the branch, then add an "Architecture changes" section to the PR description with, per changed diagram: (1) a sentence naming exactly which types/relationships changed and how, (2) a **Before** mermaid block and an **After** mermaid block (GitHub renders them), each an *excerpt* containing only the affected classes and their immediate relationships — never paste the full diagram. The committed diagram file stays the full current state; the before/after excerpts live only in the PR description (take Before from the diagram at the merge base). No structural change → no section, and append `# skip-diagram-check` to the `gh pr create` command to satisfy the pre-PR hook. +`architecture/` holds mermaid diagrams covering every application package in the repo: `overview.md` (system diagram — the package map and how the pieces connect), `cli-sync.md` (the CLI and sync engine — `cmd/bdrive` + `internal/{syncer,store,journal,config,daemon,agenthooks,autostart}`), `webapp-server.md` (the `bdrive serve` server — `internal/webapp` + `internal/remote`), and `webapp-frontend.md` (the hub's React SPA — `internal/webapp/frontend/src`). Every code change lands in exactly one detail diagram's scope; `overview.md` changes only when packages appear/disappear or the cross-piece wiring changes (`web/docs` and the private `cloud/` repo are deliberately out of scope). Before `gh pr create`: if the branch changes types or relationships drawn in any of them (new/removed types, new seams, changed fields/implements/ownership in those packages), update the affected diagram and commit it on the branch, then add an "Architecture changes" section to the PR description with, per changed diagram: (1) a sentence naming exactly which types/relationships changed and how, (2) a **Before** mermaid block and an **After** mermaid block (GitHub renders them), each an *excerpt* containing only the affected classes and their immediate relationships — never paste the full diagram. The committed diagram file stays the full current state; the before/after excerpts live only in the PR description (take Before from the diagram at the merge base). No structural change → no section, and append `# skip-diagram-check` to the `gh pr create` command to satisfy the pre-PR hook. ## Docs to keep in sync diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fa55ae9..ba7f524 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,7 +21,7 @@ touch your real `~/.bdrive`. ```sh go build -o bdrive ./cmd/bdrive mkdir -p /tmp/hub-storage -./bdrive web /tmp/hub-storage --addr :8080 --upload # plain-folder viewer +./bdrive serve /tmp/hub-storage --addr :8080 --upload # plain-folder viewer ``` For hub mode with accounts, see the self-hosting guide diff --git a/README.md b/README.md index ce3e267..d468d75 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ synchronized across all your devices and teammates through a BearDrive **hub**, every change is tracked (who, when, on which device), and everything keeps working offline. The CLI is `bdrive`; a hub is a -`bdrive web` server you (or we) run on an object store — clients sync +`bdrive serve` server you (or we) run on an object store — clients sync through it over HTTPS and never touch the storage directly. What it's for, first and foremost: **sharing context across AI agents** — @@ -46,6 +46,41 @@ $ bdrive login https://your-hub && cd ~/workspace && bdrive init # … connect the same project; the files appear and stay in sync ``` +## What this looks like + +Say your teammate Dana spent yesterday afternoon with her agent working out +why checkout fails for EU cards, and the agent wrote it down. Today you hit a +related bug. Here's the difference a shared folder makes. + +**Without one**, your agent starts from nothing: + +> **You:** why do EU checkouts fail? +> +> **Agent:** Let me investigate — searching the payment flow… + +Some minutes and a few thousand tokens later it rediscovers what Dana's agent +already knew. Or it reaches a different conclusion and writes a second doc +that quietly contradicts hers. + +**With BearDrive**, the folder is already fresh before the agent reads a +single file — the pull hook runs the moment you hit enter: + +> **You:** why do EU checkouts fail? +> +> **Agent:** `shared/findings/eu-checkout.md` [🔗](https://your-hub/p/…) +> covers this. Dana's agent traced it to the 3-D Secure redirect dropping the +> session cookie on Safari, and the fix it proposes isn't applied yet. Want me +> to apply it? + +When your agent writes its own findings down, the push hook puts them on the +hub seconds later, and Dana's next session starts from *your* work. Both +directions, no one exporting or pasting anything. + +Because these are real files, the same folder is also a website: any file can +become a public link that renders as a page, the History view shows who +changed what from which device, and the Dashboard shows which docs your agents +actually read (and which hot ones nobody maintains). + ## Features - **Any folder is a project** — `bdrive init` turns any folder into a synced @@ -76,6 +111,51 @@ $ bdrive login https://your-hub && cd ~/workspace && bdrive init ## Install +BearDrive is meant to be set up by the agent that will use it. The fastest +path is to have your agent do it; the CLI route below is the same +destination, by hand. + +### Have your agent set it up (recommended) + +No terminal needed: start any agent (Claude Code, Codex, Gemini CLI, Hermes) +in the folder you want synced and give it one paste: + +``` +Follow https://raw.githubusercontent.com/runbear-io/beardrive/main/INSTALL_FOR_AGENTS.md +to set up BearDrive project on . Ask me which folder to +sync (the project is named ""). +``` + +Joining a teammate's project? They can copy that paste with the hub URL and +project id already filled in from the project's home page in the web UI. +Starting fresh, drop the trailing sentence — the agent recommends `shared/` +and names the new project `shared`. + +The agent fetches [INSTALL_FOR_AGENTS.md](INSTALL_FOR_AGENTS.md) and follows +it: install the CLI, then one `bdrive init` — which signs in (an approval link +when there is no local browser), registers the sync hooks, and prints the +project link. The instructions live at that URL rather than inside the prompt +so they never go stale in someone's copy, and the agent handles every +deviation (already installed, no Homebrew, sign-in, wrong folder). + +Those hooks are the whole integration, and `bdrive init` registers them in +each platform's user config (`~/.claude/settings.json` and friends), once per +machine, so every session in every folder is covered: + +- a **blocking pull** when you send a message, so the agent always reads + fresh team files — it also injects the project's link convention, so the + agent appends a hub link to any synced path it mentions; +- an **async push** after every file edit, so artifacts are on the hub + seconds after the agent writes them; +- **read tracking**, so the hub's Dashboard can show what your agents + actually read. + +Each hook no-ops instantly outside BearDrive projects, which is what makes a +machine-wide registration safe. `bdrive hooks` prints what's set up on this +machine; re-run it after a CLI upgrade. + +### Install the CLI yourself + ```sh brew install runbear-io/tap/beardrive # macOS (and Linuxbrew); installs the `bdrive` CLI ``` @@ -136,7 +216,7 @@ hub's own storage, never something a syncing client points at directly: | `s3://bucket/prefix` | `AWS_PROFILE`, `~/.aws/credentials`, env vars, IAM roles. S3-compatible stores via `AWS_ENDPOINT_URL`. | | `gs://bucket/prefix` | Application Default Credentials (`gcloud auth application-default login`) or `GOOGLE_APPLICATION_CREDENTIALS`. | | `file:///path` | none — any local or network-mounted directory | -| `https://host:port/p/` | none — syncs through a bdrive web hub; only the server holds storage credentials (see [The sync hub and `bdrive init`](#the-sync-hub-and-bdrive-init)) | +| `https://host:port/p/` | none — syncs through a BearDrive hub; only the server holds storage credentials (see [The sync hub and `bdrive init`](#the-sync-hub-and-bdrive-init)) | ## Commands @@ -160,7 +240,7 @@ hub's own storage, never something a syncing client points at directly: | `bdrive restore [version]` | Put an earlier version of a file back, as a new change (`--list` shows the versions; no version = the previous one). Nothing is erased and it syncs everywhere like any edit. A file that was *created* can't be un-created yet | | `bdrive export [folder]` | Export the whole project — every device's journal, all blobs, full history — from its hub to a portable `.tar.gz` (`-o` names the file) | | `bdrive import ` | Import an export archive as a new project on the hub you're logged into (`--name` overrides); history and authorship carry over. Move projects between hubs — cloud → self-hosted or back — with `export` + `login` + `import` | -| `bdrive web [folder \| storage-root-url]` | Web server: viewer (rendered markdown, downloads, history), uploads, multi-project sync hub | +| `bdrive serve [folder \| storage-root-url]` | Web server: viewer (rendered markdown, downloads, history), uploads, multi-project sync hub (`bdrive web` is a deprecated alias) | | `bdrive whoami` | Signed-in account and device identity used in change tracking | | `bdrive version` | Print the version (also `bdrive --version`) | @@ -197,16 +277,16 @@ removal and simply stops tracking the path. ## Web server -`bdrive web` serves a website — browse folders and files, read markdown +`bdrive serve` serves a website — browse folders and files, read markdown rendered Obsidian-style (including `[[wikilinks]]`, task lists, and tables), download any file — and, pointed at a storage root, becomes a **multi-project sync hub**. It is read-only unless started with `--upload`. ```sh -bdrive web # serve the current directory (viewer) -bdrive web ./notes # serve a folder from disk (viewer) -bdrive web -c config.json # everything from a config file -bdrive web s3://my-bucket/root --upload # multi-project sync hub +bdrive serve # serve the current directory (viewer) +bdrive serve ./notes # serve a folder from disk (viewer) +bdrive serve -c config.json # everything from a config file +bdrive serve s3://my-bucket/root --upload # multi-project sync hub ``` With a folder it serves files straight from disk — on a BearDrive mount the @@ -222,7 +302,7 @@ the positional argument), `--upload` (allow client writes, off by default), `-c/--config` (read all of the above from a JSON file; explicit flags win): ```jsonc -// bdrive web -c config.json +// bdrive serve -c config.json { "remote": "s3://my-bucket/root", // storage root (hub) — or "dir": "./folder" (viewer) "addr": ":4173", @@ -304,7 +384,7 @@ access does not kill links they already minted. ```sh # On the server device (knows the storage) -bdrive web -c config.json +bdrive serve -c config.json # On each client device (knows only the server) — one command does it all: bdrive login https://drive.example.com:4173 # once per device @@ -382,16 +462,10 @@ means LAN-only links. ### Agent integration -Point any agent at [INSTALL_FOR_AGENTS.md](INSTALL_FOR_AGENTS.md) (see -[Set up an agent to do it for you](#set-up-an-agent-to-do-it-for-you)) and it -sets a project up conversationally: install the CLI, sign in, create or -connect a project (whole folder or a shared subfolder like `wiki/`), and -register the sync hooks in your user config (`~/.claude/settings.json` and -friends, once per machine) — a blocking pull when you submit a prompt so the -agent reads fresh team files, and an async push after every file edit so -artifacts are on the hub seconds after the agent writes them. The payoff: -"write a report and share it" becomes the agent generating -`wiki/report.html` and replying with a link. +Setup is conversational — one paste and the agent does the rest, hooks +included ([Have your agent set it up](#have-your-agent-set-it-up-recommended)). +The payoff, once those hooks are in place: "write a report and share it" +becomes the agent generating `wiki/report.html` and replying with a link. The web UI lists your orgs' projects in the sidebar (⌘K opens a command palette: fuzzy file search, project switching, share/history/upload @@ -457,47 +531,6 @@ With `--upload` set, the server decides per upload how the bytes travel: (object store + journal, or straight to disk for a served folder, where the daemon will pick it up like any local edit). -## Set up an agent to do it for you - -No terminal needed: start any agent (Claude Code, Codex, Gemini CLI, Hermes) -in the folder you want synced and give it one paste: - -``` -Follow https://raw.githubusercontent.com/runbear-io/beardrive/main/INSTALL_FOR_AGENTS.md -to set up BearDrive project on . Ask me which folder to -sync (the project is named ""). -``` - -Naming the project makes the agent recommend a folder of the same name, so -every teammate's checkout looks alike; with no project at all it recommends -`shared/` and names the new project `shared`. - -The agent fetches [INSTALL_FOR_AGENTS.md](INSTALL_FOR_AGENTS.md) and follows -it: install the CLI, then one `bdrive init` — which signs in (an approval link -when there is no local browser), registers the sync hooks, and prints the -project link. The instructions live at that URL rather than inside the prompt -so they never go stale in someone's copy, and the agent handles every -deviation (already installed, no Homebrew, sign-in, wrong folder). - -Those hooks are the whole integration, and `bdrive init` registers them in -each platform's user config (`~/.claude/settings.json` and friends), once per -machine, so every session in every folder is covered: - -- a **blocking pull** when you send a message, so the agent always reads - fresh team files — it also injects the project's link convention, so the - agent appends a hub link to any synced path it mentions; -- an **async push** after every file edit, so artifacts are on the hub - seconds after the agent writes them; -- **read tracking**, so the hub's Dashboard can show what your agents - actually read. - -Each hook no-ops instantly outside BearDrive projects, which is what makes a -machine-wide registration safe. `bdrive hooks` prints what's set up on this -machine; re-run it after a CLI upgrade. - -A project's home page in the web UI shows this paste with the hub URL and -project id already filled in (plus the plain-terminal version). - ## How it works ``` diff --git a/architecture/README.md b/architecture/README.md index 3d7cf4e..58267e6 100644 --- a/architecture/README.md +++ b/architecture/README.md @@ -24,7 +24,7 @@ when the package map or cross-piece wiring changes): - [overview.md](overview.md) — system diagram: every package and surface on one page, and how they connect - [cli-sync.md](cli-sync.md) — class diagram of the CLI and sync engine (`cmd/bdrive` + `internal/{syncer,store,journal,config,daemon,agenthooks,autostart}`) -- [webapp-server.md](webapp-server.md) — class diagram of the `bdrive web` server (`internal/webapp` + its `internal/remote` seam) +- [webapp-server.md](webapp-server.md) — class diagram of the `bdrive serve` server (`internal/webapp` + its `internal/remote` seam) - [webapp-frontend.md](webapp-frontend.md) — module diagram of the hub's React SPA (`internal/webapp/frontend/src`) Not covered on purpose: `web/docs` (content site, no application code) and diff --git a/architecture/overview.md b/architecture/overview.md index d13d6ec..f9f3ea0 100644 --- a/architecture/overview.md +++ b/architecture/overview.md @@ -22,7 +22,7 @@ flowchart LR hooks["internal/agenthooks
turn-boundary sync hooks"] end - subgraph hub["bdrive web hub"] + subgraph hub["bdrive serve hub"] srv["internal/webapp Server
auth, orgs, projects, shares,
history, read heat, store proxy"] fe["webapp/frontend React SPA
committed dist go:embed'ed at webapp/static"] meta["MetaStore: file JSON (default)
or sqlite / postgres (db_sql)"] diff --git a/architecture/webapp-server.md b/architecture/webapp-server.md index c18a343..75eaedf 100644 --- a/architecture/webapp-server.md +++ b/architecture/webapp-server.md @@ -1,4 +1,4 @@ -# `bdrive web` server — class diagram +# `bdrive serve` server — class diagram Source of truth: `internal/webapp` (server, services, persistence) and `internal/remote` (storage backends). Reflects the code as of this commit; diff --git a/cmd/bdrive/main.go b/cmd/bdrive/main.go index f1912cc..d8bc900 100644 --- a/cmd/bdrive/main.go +++ b/cmd/bdrive/main.go @@ -37,7 +37,7 @@ func main() { system for AI agents. Mount any folder and BearDrive keeps it synchronized across your devices and -teammates through a BearDrive hub (bdrive web — self-hosted or BearDrive +teammates through a BearDrive hub (bdrive serve — self-hosted or BearDrive Cloud). Every change is journaled — you can always see which device and author changed which file, and when. Files are real files on disk, so everything keeps working offline; changes sync when the remote is reachable.`, diff --git a/cmd/bdrive/web.go b/cmd/bdrive/web.go index 3561bd0..2eb58e7 100644 --- a/cmd/bdrive/web.go +++ b/cmd/bdrive/web.go @@ -19,8 +19,8 @@ import ( "github.com/runbear-io/beardrive/internal/webapp" ) -// webConfig mirrors the web command's flags so a server can be configured -// from a file (bdrive web -c config.json). Explicitly-passed flags win over +// webConfig mirrors the serve command's flags so a server can be configured +// from a file (bdrive serve -c config.json). Explicitly-passed flags win over // file values. type webConfig struct { Remote string `json:"remote,omitempty"` @@ -87,8 +87,9 @@ func webCmd() *cobra.Command { var upload bool var cfg webConfig c := &cobra.Command{ - Use: "web [folder | storage-root-url]", - Short: "Serve the bdrive web server: viewer, uploads, and sync hub", + Use: "serve [folder | storage-root-url]", + Aliases: []string{"web"}, + Short: "Serve the bdrive web server: viewer, uploads, and sync hub", Long: `Serve the bdrive web server: browse folders and files, read rendered markdown (Obsidian-style, including [[wikilinks]]), and download any file. @@ -106,10 +107,12 @@ The server is read-only unless --upload is set. With uploads on, content travels directly between clients and the object store through short-lived presigned URLs when the backend supports it (S3, GCS with signing credentials); otherwise it is relayed through this server.`, - Example: ` bdrive web # serve the current directory - bdrive web ./notes # serve a folder - bdrive web -c config.json # everything from a config file - bdrive web s3://bucket/root --upload # multi-project sync hub`, + Example: ` bdrive serve # serve the current directory + bdrive serve ./notes # serve a folder + bdrive serve -c config.json # everything from a config file + bdrive serve s3://bucket/root --upload # multi-project sync hub + + bdrive web # deprecated alias for "bdrive serve"`, Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { // Config file first; flags that were explicitly passed override diff --git a/sandbox/boot.sh b/sandbox/boot.sh index d448641..287d7fd 100755 --- a/sandbox/boot.sh +++ b/sandbox/boot.sh @@ -95,7 +95,7 @@ if [ ! -f /data/hub.json ]; then JSON fi -BDRIVE_HOME=/data/hubhome bdrive web -c /data/hub.json >/data/hub.log 2>&1 & +BDRIVE_HOME=/data/hubhome bdrive serve -c /data/hub.json >/data/hub.log 2>&1 & for _ in $(seq 1 40); do curl -sf -o /dev/null "$HUB/auth/login" && break sleep 1 diff --git a/web/docs/src/content/docs/index.md b/web/docs/src/content/docs/index.md index 2682ea3..5102afc 100644 --- a/web/docs/src/content/docs/index.md +++ b/web/docs/src/content/docs/index.md @@ -12,7 +12,7 @@ one paste in Codex, Gemini CLI, or Hermes, and the folder is connected: your agent reads the team's current files before it answers and pushes whatever it writes when the turn ends. -Underneath there is a CLI called `bdrive` and a **hub** — a `bdrive web` server +Underneath there is a CLI called `bdrive` and a **hub** — a `bdrive serve` server you (or we) run on an object store, which clients sync through over HTTPS and never touch the storage directly. You can drive all of it by hand, and most people never do. diff --git a/web/docs/src/content/docs/reference/cli.md b/web/docs/src/content/docs/reference/cli.md index 1eea746..8f0f5a9 100644 --- a/web/docs/src/content/docs/reference/cli.md +++ b/web/docs/src/content/docs/reference/cli.md @@ -28,7 +28,7 @@ One binary, `bdrive` — the CLI, the sync daemon, and the web server. | `bdrive restore [version]` | Put an earlier version of a file back, as a new change. No version restores the previous one; `--list` shows the versions with their short hashes | | `bdrive export [folder]` | Export the whole project — all devices' history and content — to a portable `.tar.gz` (`-o` names the file) | | `bdrive import ` | Import an export archive as a new project on the hub you're logged into (`--name` overrides the archive's name) | -| `bdrive web [folder \| storage-root-url]` | Web server: viewer, uploads, multi-project sync hub | +| `bdrive serve [folder \| storage-root-url]` | Web server: viewer, uploads, multi-project sync hub (`bdrive web` is a deprecated alias) | | `bdrive whoami` | Signed-in account and device identity used in change tracking | | `bdrive version` | Version (also `bdrive --version`) | diff --git a/web/docs/src/content/docs/reference/hub-config.md b/web/docs/src/content/docs/reference/hub-config.md index 0464d03..18b5163 100644 --- a/web/docs/src/content/docs/reference/hub-config.md +++ b/web/docs/src/content/docs/reference/hub-config.md @@ -1,19 +1,19 @@ --- title: Hub config -description: Every bdrive web flag and config-file key. +description: Every bdrive serve flag and config-file key. --- -`bdrive web` serves a website — browse folders and files, read markdown rendered +`bdrive serve` serves a website — browse folders and files, read markdown rendered Obsidian-style (including `[[wikilinks]]`, task lists, and tables), download any file. Pointed at a storage root, it becomes a multi-project sync hub. It is read-only unless started with `--upload`. ```sh -bdrive web # serve the current directory (viewer) -bdrive web ./notes # serve a folder from disk (viewer) -bdrive web -c config.json # everything from a config file -bdrive web s3://my-bucket/root --upload # multi-project sync hub +bdrive serve # serve the current directory (viewer) +bdrive serve ./notes # serve a folder from disk (viewer) +bdrive serve -c config.json # everything from a config file +bdrive serve s3://my-bucket/root --upload # multi-project sync hub ``` With a folder it serves files straight from disk. On a BearDrive mount the @@ -36,7 +36,7 @@ cloud credentials on the serving machine. ## Config file ```jsonc -// bdrive web -c config.json +// bdrive serve -c config.json { "remote": "s3://my-bucket/root", // storage root (hub) — or "dir": "./folder" (viewer) "addr": ":4173", diff --git a/web/docs/src/content/docs/reference/migration.md b/web/docs/src/content/docs/reference/migration.md index 72515fd..694326f 100644 --- a/web/docs/src/content/docs/reference/migration.md +++ b/web/docs/src/content/docs/reference/migration.md @@ -18,7 +18,7 @@ that's a member of the project. - A device that syncs the project (any member device works). - An account on the destination hub (sign up there first if you don't have one). - Uploads enabled on the destination hub (`--upload` on a self-hosted - `bdrive web`; BearDrive Cloud has them on). + `bdrive serve`; BearDrive Cloud has them on). ## Step 1 — sync, so the export is complete diff --git a/web/docs/src/content/docs/self-hosting/authentication.md b/web/docs/src/content/docs/self-hosting/authentication.md index b4c8ac7..fb69693 100644 --- a/web/docs/src/content/docs/self-hosting/authentication.md +++ b/web/docs/src/content/docs/self-hosting/authentication.md @@ -6,7 +6,7 @@ description: The three signup postures, admin controls, device sign-in, and SMTP Hubs always require sign-in — every change is attributed to a real account. The whole API (web UI, uploads, project creation, device sync) needs a session; only `/api/config` and the auth pages stay open. The plain-folder viewer, -`bdrive web ./folder`, remains auth-free. +`bdrive serve ./folder`, remains auth-free. Accounts are email, password, and name, kept in a file-backed registry (`auth.json`): bcrypt password hashes and SHA-256 token digests, atomically diff --git a/web/docs/src/content/docs/self-hosting/run-a-hub.md b/web/docs/src/content/docs/self-hosting/run-a-hub.md index af4fd52..f728277 100644 --- a/web/docs/src/content/docs/self-hosting/run-a-hub.md +++ b/web/docs/src/content/docs/self-hosting/run-a-hub.md @@ -59,7 +59,7 @@ Every knob: [Hub config](/reference/hub-config/). Signup postures and SMTP: ## 4. Run it ```sh -bdrive web -c config.json +bdrive serve -c config.json ``` Put TLS in front — Caddy, nginx, or your platform's load balancer. Device tokens