mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
feat(cli): multi-folder --shared at init + bdrive scope to edit the sync scope (#53)
* feat(cli): init --shared accepts multiple subfolders (repeatable or comma-separated) --shared is now a slice flag: `--shared wiki --shared docs` or `--shared wiki,docs` sync several subfolders into one project (include list ["wiki/", "docs/"]). The interactive scope prompt takes a space- or comma-separated list. Entries resolving to ".", "", or ".." error out — silently dropping them would widen scope to the whole folder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG * docs(plugin): skills/commands propose multiple --shared folders at init The init/install flows now scan for all knowledge folder candidates and offer them as one --shared list (one project, one permission set), noting that folders needing different access belong in separate projects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG * feat(cli): bdrive scope — add/remove shared subfolders without editing JSON `bdrive scope` shows the include list; `scope add`/`scope rm` edit it from the mount root. The daemon re-reads config each tick, so changes apply in seconds. rm deletes nothing (newly filtered paths drop from the cache with no delete op); removing the last entry is refused since an empty include list means whole-folder sync. add onto a whole-folder project is refused for the same narrowing hazard. Skill/README/docs updated; scope added to the cli-sync diagram's command list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG * docs+cli: scoping guide covers multi-folder --shared and bdrive scope; init hints on ignored --shared at resume The scoping guide (the dedicated page for this feature) now shows --shared wiki,docs and a "Change the scope later" section for bdrive scope; setup-by-hand and project-files point at it. init resume with an explicit --shared now says the flag is ignored instead of staying silent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AubcaQscjcQucXwh578vqG --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
69e7231a70
commit
0236e1b272
@@ -67,8 +67,8 @@ $ bdrive login https://your-hub && cd ~/workspace && bdrive init
|
||||
(last-writer-wins), and the losing version is preserved as a
|
||||
`name.bdrive-conflict-<device>-<time>` file. Nothing is silently dropped.
|
||||
- **Selective sync** — a gitignore-style `.bdriveignore` opts files out, and
|
||||
`bdrive init --shared <dir>` (or the interactive prompt) narrows sync to
|
||||
one shared subfolder.
|
||||
`bdrive init --shared <dirs>` (or the interactive prompt) narrows sync to
|
||||
one or more shared subfolders (`--shared wiki,docs`, or repeat the flag).
|
||||
- **macOS & Linux.**
|
||||
|
||||
## Install
|
||||
@@ -140,6 +140,7 @@ hub's own storage, never something a syncing client points at directly:
|
||||
| `bdrive logout` | Sign this device out — clear the saved token/account (`--forget` also drops the remembered server) |
|
||||
| `bdrive init [folder]` | Create/connect a project and start syncing — interactive on a TTY, flags (`--name/--project/--shared/--yes`) for scripts; re-run to resume |
|
||||
| `bdrive stop [folder]` | Stop syncing, including agent sync hooks (files stay; `bdrive init` resumes) |
|
||||
| `bdrive scope [add\|rm <dirs...>]` | Show or change which subfolders sync (the include list set by `init --shared`) — no JSON editing; the daemon picks changes up in seconds. `rm` deletes nothing, locally or on the hub |
|
||||
| `bdrive url [path]` | Internal hub link for a file/folder (sign-in + membership required; `--sync` pushes first; no arg = project home). Computed locally |
|
||||
| `bdrive share <file>` | Public URL for a synced file (`--list`, `--revoke`, `--expires`) |
|
||||
| `bdrive sync [folder]` | Run one sync cycle now. `--note <text>` stamps session context (e.g. an agent session id) onto changes — shown in `bdrive log` and hub history; keeps applying to daemon-committed changes until `--note-ttl` (default 30m) expires. `--hook <label>` is agent-hook plumbing: event JSON on stdin, sync + note, gated-link formula (Claude Code hook JSON) on stdout |
|
||||
|
||||
@@ -88,7 +88,7 @@ classDiagram
|
||||
|
||||
class Commands {
|
||||
init login logout
|
||||
sync stop status log
|
||||
sync stop scope status log
|
||||
url share export import
|
||||
web daemon hooks read-log skill
|
||||
}
|
||||
|
||||
+51
-24
@@ -47,7 +47,8 @@ venv/
|
||||
// folder just resumes syncing (which is also how a moved/renamed folder
|
||||
// picks up where it left off).
|
||||
func initCmd() *cobra.Command {
|
||||
var projectID, projectName, shared string
|
||||
var projectID, projectName string
|
||||
var shared []string
|
||||
var yes, foreground bool
|
||||
c := &cobra.Command{
|
||||
Use: "init [folder]",
|
||||
@@ -56,8 +57,8 @@ func initCmd() *cobra.Command {
|
||||
syncing it through your bdrive server.
|
||||
|
||||
On a terminal, init asks what you want: create a new project or connect an
|
||||
existing one, and whether to sync the whole folder or only a shared
|
||||
subfolder (e.g. ./shared). Flags answer those questions non-interactively;
|
||||
existing one, and whether to sync the whole folder or only shared
|
||||
subfolders (e.g. ./shared). Flags answer those questions non-interactively;
|
||||
without a TTY init never prompts (it creates-or-joins a project named after
|
||||
the folder and syncs the whole folder).
|
||||
|
||||
@@ -70,6 +71,7 @@ the folder was renamed or moved.`,
|
||||
bdrive init ./notes --name shared-notes
|
||||
bdrive init --project p-7f3a2c91 # connect an existing project
|
||||
bdrive init --shared shared # only ./shared syncs
|
||||
bdrive init --shared wiki,docs # only ./wiki and ./docs sync
|
||||
bdrive init --yes # accept all defaults (no prompts)`,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
@@ -86,6 +88,9 @@ the folder was renamed or moved.`,
|
||||
return err
|
||||
} else if ok && proj.Remote != "" {
|
||||
fmt.Printf("resuming %s (project %s)\n", folder, proj.Volume)
|
||||
if cmd.Flags().Changed("shared") {
|
||||
fmt.Println("note: --shared is ignored on resume — change what syncs with `bdrive scope add`/`rm`")
|
||||
}
|
||||
return startSync(cmd.Context(), folder, proj, foreground, 3*time.Second, 10*time.Second)
|
||||
}
|
||||
|
||||
@@ -115,20 +120,18 @@ the folder was renamed or moved.`,
|
||||
}
|
||||
|
||||
// What syncs?
|
||||
if shared == "" && interactive && !cmd.Flags().Changed("shared") {
|
||||
if len(shared) == 0 && interactive && !cmd.Flags().Changed("shared") {
|
||||
shared, err = chooseScope()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var include []string
|
||||
if shared != "" {
|
||||
shared = strings.Trim(path.Clean(filepath.ToSlash(shared)), "/")
|
||||
if shared == "" || shared == "." || strings.HasPrefix(shared, "..") {
|
||||
return fmt.Errorf("invalid shared folder %q", shared)
|
||||
}
|
||||
include = []string{shared + "/"}
|
||||
if err := os.MkdirAll(filepath.Join(folder, filepath.FromSlash(shared)), 0o755); err != nil {
|
||||
include, err := cleanShared(shared)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, inc := range include {
|
||||
if err := os.MkdirAll(filepath.Join(folder, filepath.FromSlash(strings.TrimSuffix(inc, "/"))), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -152,8 +155,12 @@ the folder was renamed or moved.`,
|
||||
}
|
||||
}
|
||||
fmt.Printf("initialized %s\n server: %s\n project: %s (%s)\n", folder, server, p.Name, p.ID)
|
||||
if shared != "" {
|
||||
fmt.Printf(" syncing: ./%s only\n", shared)
|
||||
if len(include) > 0 {
|
||||
dirs := make([]string, len(include))
|
||||
for i, inc := range include {
|
||||
dirs[i] = "./" + strings.TrimSuffix(inc, "/")
|
||||
}
|
||||
fmt.Printf(" syncing: %s only\n", strings.Join(dirs, ", "))
|
||||
}
|
||||
if err := startSync(cmd.Context(), folder, proj, foreground, 3*time.Second, 10*time.Second); err != nil {
|
||||
return err
|
||||
@@ -174,7 +181,7 @@ next steps:
|
||||
}
|
||||
c.Flags().StringVar(&projectID, "project", "", "connect an existing project by id (p-xxxxxxxx)")
|
||||
c.Flags().StringVar(&projectName, "name", "", "project name to create or join (default: folder name)")
|
||||
c.Flags().StringVar(&shared, "shared", "", "sync only this subfolder (e.g. shared)")
|
||||
c.Flags().StringSliceVar(&shared, "shared", nil, "sync only these subfolders (repeatable or comma-separated, e.g. wiki,docs)")
|
||||
c.Flags().BoolVarP(&yes, "yes", "y", false, "accept defaults, never prompt")
|
||||
c.Flags().BoolVarP(&foreground, "foreground", "f", false, "run the sync daemon in the foreground")
|
||||
return c
|
||||
@@ -248,23 +255,43 @@ func chooseProject(server, token, defaultName string) (serverProject, error) {
|
||||
return projects[idx], nil
|
||||
}
|
||||
|
||||
// chooseScope returns "" for whole-folder sync, or the shared subfolder.
|
||||
func chooseScope() (string, error) {
|
||||
// chooseScope returns nil for whole-folder sync, or the shared subfolders.
|
||||
func chooseScope() ([]string, error) {
|
||||
var mode string
|
||||
if err := survey.AskOne(&survey.Select{
|
||||
Message: "What should sync?",
|
||||
Options: []string{"The whole folder", "Only a shared subfolder"},
|
||||
Options: []string{"The whole folder", "Only shared subfolders"},
|
||||
}, &mode); err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
if mode == "The whole folder" {
|
||||
return "", nil
|
||||
return nil, nil
|
||||
}
|
||||
dir := "shared"
|
||||
if err := survey.AskOne(&survey.Input{Message: "Shared subfolder:", Default: "shared"}, &dir); err != nil {
|
||||
return "", err
|
||||
dirs := "shared"
|
||||
if err := survey.AskOne(&survey.Input{Message: "Shared subfolder(s), space- or comma-separated:", Default: "shared"}, &dirs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dir, nil
|
||||
return strings.Fields(strings.ReplaceAll(dirs, ",", " ")), nil
|
||||
}
|
||||
|
||||
// cleanShared normalizes --shared entries into include patterns ("wiki/"):
|
||||
// slashes cleaned, duplicates dropped. Any entry that resolves to the mount
|
||||
// root or escapes it is an error — a silently-dropped "." would widen the
|
||||
// scope to the whole folder.
|
||||
func cleanShared(shared []string) ([]string, error) {
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
for _, s := range shared {
|
||||
s = strings.Trim(path.Clean(filepath.ToSlash(strings.TrimSpace(s))), "/")
|
||||
if s == "" || s == "." || strings.HasPrefix(s, "..") {
|
||||
return nil, fmt.Errorf("invalid shared folder %q", s)
|
||||
}
|
||||
if !seen[s] {
|
||||
seen[s] = true
|
||||
out = append(out, s+"/")
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type serverProject struct {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScopeRemove(t *testing.T) {
|
||||
include := []string{"wiki/", "docs/", "*.md"}
|
||||
for _, tc := range []struct {
|
||||
args []string
|
||||
want []string
|
||||
err bool
|
||||
}{
|
||||
{args: []string{"docs"}, want: []string{"wiki/", "*.md"}},
|
||||
{args: []string{"docs/"}, want: []string{"wiki/", "*.md"}}, // normalized match
|
||||
{args: []string{"*.md"}, want: []string{"wiki/", "docs/"}}, // literal pattern match
|
||||
{args: []string{"wiki", "docs"}, want: []string{"*.md"}},
|
||||
{args: []string{"notes"}, err: true}, // not in scope
|
||||
} {
|
||||
got, err := scopeRemove(include, tc.args)
|
||||
if tc.err != (err != nil) {
|
||||
t.Errorf("scopeRemove(%q) err = %v, want err %v", tc.args, err, tc.err)
|
||||
continue
|
||||
}
|
||||
if !tc.err && !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("scopeRemove(%q) = %q, want %q", tc.args, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanShared(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
in []string
|
||||
want []string
|
||||
err bool
|
||||
}{
|
||||
{in: nil, want: nil},
|
||||
{in: []string{"wiki"}, want: []string{"wiki/"}},
|
||||
{in: []string{"wiki", "docs"}, want: []string{"wiki/", "docs/"}},
|
||||
{in: []string{" wiki ", "./docs/", "wiki"}, want: []string{"wiki/", "docs/"}}, // trimmed, cleaned, deduped
|
||||
{in: []string{"a/b"}, want: []string{"a/b/"}},
|
||||
{in: []string{""}, err: true},
|
||||
{in: []string{"wiki", ""}, err: true}, // "wiki,,docs" typo must not half-apply
|
||||
{in: []string{"."}, err: true}, // would silently mean whole-folder sync
|
||||
{in: []string{"../up"}, err: true},
|
||||
} {
|
||||
got, err := cleanShared(tc.in)
|
||||
if tc.err != (err != nil) {
|
||||
t.Errorf("cleanShared(%q) err = %v, want err %v", tc.in, err, tc.err)
|
||||
continue
|
||||
}
|
||||
if !tc.err && !reflect.DeepEqual(got, tc.want) {
|
||||
t.Errorf("cleanShared(%q) = %q, want %q", tc.in, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ everything keeps working offline; changes sync when the remote is reachable.`,
|
||||
shareCmd(),
|
||||
urlCmd(),
|
||||
stopCmd(),
|
||||
scopeCmd(),
|
||||
syncCmd(),
|
||||
readLogCmd(),
|
||||
hooksCmd(),
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/runbear-io/beardrive/internal/config"
|
||||
)
|
||||
|
||||
// bdrive scope shows and edits the project's sync scope — the include list
|
||||
// in .bdrive/config.json that `init --shared` seeds — so growing or
|
||||
// shrinking what syncs never means hand-editing JSON. The daemon re-reads
|
||||
// the config every tick, so changes apply within seconds.
|
||||
func scopeCmd() *cobra.Command {
|
||||
c := &cobra.Command{
|
||||
Use: "scope",
|
||||
Short: "Show or change which subfolders sync (the include list)",
|
||||
Long: `Show or change the project's sync scope: the include list in
|
||||
.bdrive/config.json, as set by init --shared. An empty list means the whole
|
||||
folder syncs. Run from the mount root; the daemon picks changes up within
|
||||
seconds.
|
||||
|
||||
Removing a folder stops syncing it but deletes nothing — local files stay,
|
||||
and the hub keeps everything already synced.`,
|
||||
Example: ` bdrive scope # show what syncs
|
||||
bdrive scope add docs # also sync ./docs
|
||||
bdrive scope rm docs # stop syncing ./docs (files stay everywhere)`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
folder, err := absFolder(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proj, err := mustProject(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printScope(proj)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
c.AddCommand(scopeAddCmd(), scopeRmCmd())
|
||||
return c
|
||||
}
|
||||
|
||||
func scopeAddCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "add <dir>...",
|
||||
Short: "Add shared subfolders to the sync scope",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
folder, err := absFolder(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proj, err := mustProject(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(proj.Include) == 0 {
|
||||
return fmt.Errorf("this project syncs the whole folder; adding %s would narrow it to only that — re-run `bdrive init` with --shared if you want a scoped sync", strings.Join(args, ", "))
|
||||
}
|
||||
incs, err := cleanShared(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, i := range proj.Include {
|
||||
seen[i] = true
|
||||
}
|
||||
added := 0
|
||||
for _, inc := range incs {
|
||||
if seen[inc] {
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(folder, filepath.FromSlash(strings.TrimSuffix(inc, "/"))), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
proj.Include = append(proj.Include, inc)
|
||||
seen[inc] = true
|
||||
added++
|
||||
}
|
||||
if added == 0 {
|
||||
fmt.Println("already in the sync scope")
|
||||
} else if _, err := config.SaveProject(folder, proj); err != nil {
|
||||
return err
|
||||
}
|
||||
printScope(proj)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func scopeRmCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm <dir>...",
|
||||
Short: "Remove shared subfolders from the sync scope (deletes nothing)",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
folder, err := absFolder(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proj, err := mustProject(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kept, err := scopeRemove(proj.Include, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(kept) == 0 {
|
||||
return fmt.Errorf("removing the last shared folder would switch to syncing the whole folder; run `bdrive stop` to stop syncing instead")
|
||||
}
|
||||
proj.Include = kept
|
||||
if _, err := config.SaveProject(folder, proj); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("removed from the sync scope — nothing was deleted, locally or on the hub")
|
||||
printScope(proj)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// scopeRemove drops the named dirs from the include list, matching each
|
||||
// argument both literally and in normalized "dir/" form (hand-edited
|
||||
// configs may hold arbitrary patterns). Unknown entries are an error.
|
||||
func scopeRemove(include, args []string) ([]string, error) {
|
||||
remove := map[string]bool{}
|
||||
for _, a := range args {
|
||||
keys := map[string]bool{strings.TrimSpace(a): true}
|
||||
if norm, err := cleanShared([]string{a}); err == nil {
|
||||
keys[norm[0]] = true
|
||||
}
|
||||
found := false
|
||||
for _, i := range include {
|
||||
if keys[i] {
|
||||
remove[i] = true
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%q is not in the sync scope (see `bdrive scope`)", a)
|
||||
}
|
||||
}
|
||||
var kept []string
|
||||
for _, i := range include {
|
||||
if !remove[i] {
|
||||
kept = append(kept, i)
|
||||
}
|
||||
}
|
||||
return kept, nil
|
||||
}
|
||||
|
||||
func printScope(proj config.Project) {
|
||||
if len(proj.Include) == 0 {
|
||||
fmt.Println("the whole folder syncs (no include list)")
|
||||
return
|
||||
}
|
||||
fmt.Println("syncing only:")
|
||||
for _, i := range proj.Include {
|
||||
fmt.Println(" ./" + strings.TrimSuffix(i, "/"))
|
||||
}
|
||||
}
|
||||
+11
-7
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: Start syncing a project in this folder — create a new BearDrive project or connect an existing one, whole folder or a shared subfolder, and start the sync daemon
|
||||
argument-hint: "[folder] [--name <project> | --project <p-id>] [--shared <dir>]"
|
||||
description: Start syncing a project in this folder — create a new BearDrive project or connect an existing one, whole folder or shared subfolders, and start the sync daemon
|
||||
argument-hint: "[folder] [--name <project> | --project <p-id>] [--shared <dirs>]"
|
||||
---
|
||||
|
||||
Start syncing a project with BearDrive. Arguments: `$ARGUMENTS` (optional
|
||||
@@ -43,17 +43,21 @@ Follow these steps:
|
||||
|
||||
4. **Initialize** — two hard rules:
|
||||
|
||||
- **Never sync a repo root**: inside a repo, knowledge syncs as a
|
||||
scoped subfolder via `--shared`. A dedicated knowledge folder
|
||||
(empty dir, standalone vault) may be the mount itself.
|
||||
- **Never sync a repo root**: inside a repo, knowledge syncs as
|
||||
scoped subfolders via `--shared` (one or more — several folders can
|
||||
share one project when the same people should see all of them;
|
||||
folders needing different access go in separate projects). A
|
||||
dedicated knowledge folder (empty dir, standalone vault) may be the
|
||||
mount itself.
|
||||
- **One transport per folder**: a git-tracked dir must leave git
|
||||
tracking before it syncs (`git rm -r --cached <dir>` + gitignore;
|
||||
stage it, let the user commit). Offer one-way git snapshots if they
|
||||
want a git record; `bdrive log -p <path>` covers history for most.
|
||||
|
||||
```sh
|
||||
bdrive init --name <project> --yes # dedicated knowledge folder
|
||||
bdrive init --name <project> --shared wiki # in a repo: only ./wiki syncs
|
||||
bdrive init --name <project> --yes # dedicated knowledge folder
|
||||
bdrive init --name <project> --shared wiki # in a repo: only ./wiki syncs
|
||||
bdrive init --name <project> --shared wiki,docs # several shared subfolders, one project
|
||||
```
|
||||
|
||||
5. **Register agent sync hooks**: run `bdrive hooks install <folder>`. It
|
||||
|
||||
+17
-11
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: Set up BearDrive for this project — install the CLI, sign in, create/connect a project, optionally document the shared folder in CLAUDE.md, and register project-level sync hooks so every teammate's files stay fresh during Claude sessions
|
||||
argument-hint: "[project-name] [--shared <dir>]"
|
||||
argument-hint: "[project-name] [--shared <dirs>]"
|
||||
---
|
||||
|
||||
Set up BearDrive for the current project, end to end. Work through these
|
||||
@@ -27,19 +27,22 @@ browser window is coming, then sign in:
|
||||
|
||||
## 3. Initialize the project
|
||||
|
||||
If `$ARGUMENTS` gives a project name and/or `--shared <dir>`, use them.
|
||||
If `$ARGUMENTS` gives a project name and/or `--shared <dirs>` (one or more,
|
||||
comma-separated or repeated), use them.
|
||||
Otherwise ask the user two questions (or infer from their request):
|
||||
- **Create a new project or connect an existing one?** (`bdrive init
|
||||
--name <name>` creates-or-joins by name; `bdrive init --project <p-id>`
|
||||
connects by id.)
|
||||
- **Sync the whole folder, or only a shared subfolder?** Hard rule:
|
||||
**never sync a repo root** — inside a repo, knowledge always syncs as a
|
||||
scoped subfolder via `--shared`. Whole-folder is only for a dedicated
|
||||
- **Sync the whole folder, or only shared subfolders?** Hard rule:
|
||||
**never sync a repo root** — inside a repo, knowledge always syncs as
|
||||
scoped subfolders via `--shared`. Whole-folder is only for a dedicated
|
||||
knowledge folder (an empty dir, a standalone vault) that is the mount
|
||||
itself. Don't ask open-endedly: scan the repo for an existing knowledge
|
||||
folder (`wiki/`, `docs/`, `notes/`, `handbook/`, an Obsidian vault —
|
||||
markdown-heavy, not source code) and propose the best candidate for
|
||||
confirmation, e.g. "I found `./wiki` — sync that?".
|
||||
itself. Don't ask open-endedly: scan the repo for existing knowledge
|
||||
folders (`wiki/`, `docs/`, `notes/`, `handbook/`, an Obsidian vault —
|
||||
markdown-heavy, not source code) and propose the candidates for
|
||||
confirmation, e.g. "I found `./wiki` and `./docs` — sync both?"
|
||||
(`--shared wiki,docs` puts them in one project — one membership, one
|
||||
permission set; folders needing different access go in separate projects).
|
||||
|
||||
**One transport per folder.** If the chosen folder is currently git-tracked,
|
||||
BearDrive and git would both write it — the silent-revert hazard. Get consent,
|
||||
@@ -50,10 +53,13 @@ Obsidian, symlinks — in the beardrive skill's "Connecting knowledge tooling".)
|
||||
Then run it non-interactively, e.g.:
|
||||
```sh
|
||||
bdrive init --name <project-name> --yes # dedicated knowledge folder
|
||||
bdrive init --name <project-name> --shared wiki # in a repo: only ./wiki syncs
|
||||
bdrive init --name <project-name> --shared wiki # in a repo: only ./wiki syncs
|
||||
bdrive init --name <project-name> --shared wiki,docs # several shared subfolders, one project
|
||||
```
|
||||
Re-running `bdrive init --yes` later is always safe: it resumes syncing
|
||||
(including after the folder was renamed or moved).
|
||||
(including after the folder was renamed or moved). To add or remove shared
|
||||
subfolders later, use `bdrive scope add <dir>` / `bdrive scope rm <dir>`
|
||||
from the mount root — never hand-edit `.bdrive/config.json`.
|
||||
|
||||
After init, tell git what's what: add `.bdrive/` to `.gitignore` (per-machine
|
||||
state, never committed) and COMMIT `.bdriveignore` (on a `--shared` mount the
|
||||
|
||||
@@ -13,9 +13,10 @@ Use this skill whenever the user is working with the `bdrive` CLI: initializing
|
||||
|
||||
| Action | Command |
|
||||
|---|---|
|
||||
| Start syncing a project (create/connect; the front door) | `bdrive init [<folder>]` — interactive on a TTY; flags `--name <x>` / `--project <id>` / `--shared <dir>` / `--yes` for scripts and agents (NEVER prompts without a TTY). Re-run to resume, including after the folder was renamed/moved. Runs the login flow first (against your hub URL) if the device has no session. |
|
||||
| Start syncing a project (create/connect; the front door) | `bdrive init [<folder>]` — interactive on a TTY; flags `--name <x>` / `--project <id>` / `--shared <dirs>` (comma-separated or repeated) / `--yes` for scripts and agents (NEVER prompts without a TTY). Re-run to resume, including after the folder was renamed/moved. Runs the login flow first (against your hub URL) if the device has no session. |
|
||||
| Run the daemon in the foreground | `bdrive init -f` |
|
||||
| Stop syncing | `bdrive stop [<folder>]` — pauses daemon *and* agent hooks; `bdrive init` resumes (`--forget` also unregisters) |
|
||||
| Show/change which subfolders sync | `bdrive scope` / `bdrive scope add <dirs...>` / `bdrive scope rm <dirs...>` — edits the include list set by `init --shared` (run from the mount root; NEVER hand-edit config.json for this). The daemon applies it within seconds. `rm` stops syncing a folder but deletes nothing, locally or on the hub; removing the last entry is refused (that would flip to whole-folder sync — use `bdrive stop` instead) |
|
||||
| One sync cycle now | `bdrive sync [<folder>]` — `--note <text>` stamps session context; `--hook <label>` is the Claude turn-start hook's plumbing (event JSON in, sync + note, gated-link formula out) |
|
||||
| Register agent sync hooks (Claude Code, Codex, Gemini CLI, Hermes) | `bdrive hooks install [<folder>]` — auto-detects the platforms in use and merges pull/push/session-note/read-tracking hooks into each one's own hook config, idempotently; bare `bdrive hooks` shows the status table |
|
||||
| Install this skill on another agent (Codex, Gemini CLI, Hermes, Claude Code) | `bdrive skill install [<folder>]` — writes the binary's own copy of this skill to each detected platform's user-level skills dir (`~/.codex/skills/beardrive/SKILL.md` and friends), idempotently; bare `bdrive skill` shows the status table. Then the user asks that agent to set the folder up and it runs `init` + `hooks install` itself |
|
||||
@@ -47,7 +48,7 @@ Two files at the mount root control a folder's sync behavior:
|
||||
"id": "m-5a10b713",
|
||||
"volume": "agent-workspace",
|
||||
"remote": "https://drive.example.com/p/p-7f3a2c91",
|
||||
"include": ["shared/"] // optional: sync ONLY these (set by init --shared)
|
||||
"include": ["shared/"] // optional: sync ONLY these (init --shared; edit with bdrive scope add/rm)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -72,7 +73,7 @@ Selective-sync semantics — important when advising users:
|
||||
### Init flow
|
||||
|
||||
1. Sign-in happens lazily: `bdrive init` runs the login flow itself when the device has no session, so don't ask users to sign up ahead of time. Bare `bdrive login` targets BearDrive Cloud (beardrive.ai) — signing up in the browser auto-creates a free personal workspace; a pending team invite routes them into that team instead. Self-hosting teams: `bdrive login https://your-hub`.
|
||||
2. Pick what to sync BEFORE running init. In a repo, look for an existing knowledge folder (`wiki/`, `docs/`, `notes/`, `handbook/`, an Obsidian vault) and propose it as `--shared <dir>` — confirm, don't interrogate. Never sync a repo root. Then run `bdrive init` in the folder. Interactive on a TTY (create new / connect existing project; whole folder / shared subfolder); with flags or without a TTY it creates-or-joins a project named after the folder and syncs everything. It:
|
||||
2. Pick what to sync BEFORE running init. In a repo, look for existing knowledge folders (`wiki/`, `docs/`, `notes/`, `handbook/`, an Obsidian vault) and propose them as `--shared <dirs>` — several folders can share one project (`--shared wiki,docs`) when the same people should see all of them; folders needing different access belong in separate projects. Confirm, don't interrogate. Never sync a repo root. Then run `bdrive init` in the folder. Interactive on a TTY (create new / connect existing project; whole folder / shared subfolder); with flags or without a TTY it creates-or-joins a project named after the folder and syncs everything. It:
|
||||
- writes `<folder>/.bdrive/config.json` (mount id + project + remote) and registers the mount id in `~/.bdrive/mounts.json`,
|
||||
- seeds a starter `.bdriveignore` (node_modules, build dirs, caches, `.env*`) when none exists,
|
||||
- opens the volume store under `~/.bdrive/volumes/<mount-id>/`,
|
||||
@@ -84,7 +85,7 @@ Selective-sync semantics — important when advising users:
|
||||
|
||||
- `--name <x>` — project name to create-or-join (default: folder basename).
|
||||
- `--project <id>` — connect an existing project by id (`p-xxxxxxxx`).
|
||||
- `--shared <dir>` — sync only this subfolder (becomes the include list; remote paths keep the prefix so all devices see the same layout).
|
||||
- `--shared <dirs>` — sync only these subfolders (repeatable or comma-separated: `--shared wiki,docs`; becomes the include list; remote paths keep the prefix so all devices see the same layout).
|
||||
- `--yes, -y` — accept defaults, never prompt.
|
||||
- `--foreground, -f` — run the daemon in the foreground (systemd/launchd/containers).
|
||||
|
||||
@@ -235,7 +236,7 @@ bdrive stop ./notes --forget
|
||||
When guiding `bdrive init`, detect existing knowledge tooling and connect it instead of blind-syncing the folder. Two rules govern every case:
|
||||
|
||||
- **One transport per folder.** Never sync a folder that has another writer. Git-tracked paths: a teammate's `git pull` or branch switch rewrites files with older content, and sync broadcasts that as a fresh edit — silently reverting the team's latest pages. A gbrain brain root: every private capture and overnight enrichment would become team-visible, and each member's cron rewriting the same pages fills the project with conflict copies. Moving a folder from git to beardrive is a **handoff**: `git rm -r --cached <dir>` + add `<dir>/` to `.gitignore`, stage the change but let the user commit it (teammates then pull and re-init; identical content converges with no conflicts). If they want a git record anyway, offer **one-way snapshots** (a scheduled job commits the synced folder's state to an archive branch — git only ever reads the folder) and note that hub history (`bdrive log -p <path>`) usually covers the need.
|
||||
- **Knowledge syncs as a scoped folder.** Inside a repo, always `--shared <dir>` — never the repo root. A dedicated knowledge folder (an empty dir, a standalone vault) may be the mount itself. The sync scope is per-device (`.bdrive/` never syncs), so recommend the same `--shared <dir>` when each teammate connects.
|
||||
- **Knowledge syncs as a scoped folder.** Inside a repo, always `--shared <dirs>` (one or more subfolders) — never the repo root. A dedicated knowledge folder (an empty dir, a standalone vault) may be the mount itself. The sync scope is per-device (`.bdrive/` never syncs), so recommend the same `--shared <dirs>` when each teammate connects.
|
||||
|
||||
Detection ladder — first match wins; if two rungs match, ask which to connect:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Scoping the folder
|
||||
description: Decide what agents can see — narrow a project to one subfolder, and opt files out with a gitignore-style .bdriveignore.
|
||||
description: Decide what agents can see — narrow a project to chosen subfolders, change the scope later with bdrive scope, and opt files out with a gitignore-style .bdriveignore.
|
||||
---
|
||||
|
||||
Shared agent memory works better when it's curated. A folder holding
|
||||
@@ -12,15 +12,18 @@ subfolder, and **`.bdriveignore`** that opts individual paths out. Both are
|
||||
applied symmetrically — the same filter governs what's read from disk and what's
|
||||
written back to it.
|
||||
|
||||
## Sync only a subfolder
|
||||
## Sync only subfolders
|
||||
|
||||
```sh
|
||||
bdrive init --shared wiki
|
||||
bdrive init --shared wiki,docs # several subfolders, one project
|
||||
```
|
||||
|
||||
This is the right shape inside a code repository: sync `wiki/` or `docs/` and
|
||||
leave the source tree alone. The agent gets a knowledge folder; the code stays
|
||||
in git where it belongs. The interactive `bdrive init` asks the same question.
|
||||
in git where it belongs. `--shared` takes one folder or several — comma-separated
|
||||
or repeated — and they all join the same project, with one membership and one
|
||||
permission set. The interactive `bdrive init` asks the same question.
|
||||
|
||||
The result lands in `.bdrive/config.json` as an include list:
|
||||
|
||||
@@ -29,6 +32,23 @@ The result lands in `.bdrive/config.json` as an include list:
|
||||
"remote": "https://drive.example.com/p/p-7f3a2c91", "include": ["wiki/"] }
|
||||
```
|
||||
|
||||
## Change the scope later
|
||||
|
||||
`bdrive scope` shows the include list; `scope add` / `scope rm` edit it — no
|
||||
JSON editing, and the running daemon applies the change within seconds:
|
||||
|
||||
```sh
|
||||
bdrive scope # what syncs now
|
||||
bdrive scope add notes # also sync ./notes
|
||||
bdrive scope rm docs # stop syncing ./docs
|
||||
```
|
||||
|
||||
Removing a folder stops syncing it but deletes nothing — local files stay, and
|
||||
the hub keeps everything already synced (the same
|
||||
[non-destructive rule](#opting-out-is-non-destructive) as `.bdriveignore`).
|
||||
Removing the *last* entry is refused, because an empty include list means the
|
||||
whole folder syncs; if you want to stop syncing entirely, that's `bdrive stop`.
|
||||
|
||||
:::tip
|
||||
A `--shared` mount is also where the two-file
|
||||
[`AGENTS.md` pattern](/guides/shared-agent-memory/) earns its keep — the synced
|
||||
|
||||
@@ -58,9 +58,11 @@ Init writes `.bdrive/config.json`, seeds a starter `.bdriveignore`
|
||||
in yet? It runs the login flow first.
|
||||
|
||||
:::tip[Working inside a repository]
|
||||
Sync a subfolder rather than the repo root: `bdrive init --shared docs`. Git
|
||||
directories are never synced (per-file last-writer-wins would corrupt a
|
||||
repository), but a narrower scope keeps the sync surface honest.
|
||||
Sync subfolders rather than the repo root: `bdrive init --shared docs` (or
|
||||
several at once: `--shared wiki,docs`). Git directories are never synced
|
||||
(per-file last-writer-wins would corrupt a repository), but a narrower scope
|
||||
keeps the sync surface honest. Adjust it later with `bdrive scope add`/`rm` —
|
||||
see [Scoping the folder](/guides/scoping/).
|
||||
:::
|
||||
|
||||
## 3. Work normally
|
||||
|
||||
@@ -13,6 +13,7 @@ One binary, `bdrive` — the CLI, the sync daemon, and the web server.
|
||||
| `bdrive logout` | Sign this device out — clear the saved token and account. `--forget` also drops the remembered server |
|
||||
| `bdrive init [folder]` | Create or connect a project and start syncing. Interactive on a TTY; flags (`--name`, `--project`, `--shared`, `--yes`) for scripts. Re-run to resume |
|
||||
| `bdrive stop [folder]` | Stop syncing — daemon and agent sync hooks both pause. Files stay on disk; `bdrive init` resumes |
|
||||
| `bdrive scope [add\|rm <dirs...>]` | Show or change which subfolders sync — the include list set by `init --shared`. Run from the mount root; the daemon picks changes up in seconds. `rm` stops syncing a folder but deletes nothing, locally or on the hub |
|
||||
| `bdrive url [path]` | Internal hub link for a file or folder — sign-in and membership required. `--sync` pushes first; no argument gives the project home. Computed locally |
|
||||
| `bdrive share <file>` | Public URL for a synced file. `--list`, `--revoke`, `--expires` |
|
||||
| `bdrive sync [folder]` | Run one sync cycle now. Refuses folders this device never `init`ed and folders paused by `bdrive stop`. `--note <text>` stamps session context onto changes; `--note-ttl` (default 30m) bounds it. `--hook <label>` is agent-hook plumbing |
|
||||
@@ -33,7 +34,8 @@ One binary, `bdrive` — the CLI, the sync daemon, and the web server.
|
||||
|
||||
The front door. Interactive on a TTY, with survey menus for create-new versus
|
||||
connect-existing (showing a project list) and whole-folder versus
|
||||
`--shared <dir>` (which becomes the include list). Full flag bypass with
|
||||
`--shared <dirs>` (one or more subfolders, repeatable or comma-separated —
|
||||
`--shared wiki,docs` — which become the include list). Full flag bypass with
|
||||
`--name`, `--project`, `--shared`, `--yes`, and it never prompts without a TTY.
|
||||
|
||||
It runs the login flow first when there is no session, writes
|
||||
|
||||
@@ -18,7 +18,9 @@ plus project, remote, and include settings.
|
||||
```
|
||||
|
||||
Written by `bdrive init` and safe to hand-edit — a running daemon picks changes
|
||||
up automatically.
|
||||
up automatically. The `include` list (which subfolders sync, set by
|
||||
`init --shared`) has a friendlier editor: `bdrive scope add`/`rm` from the
|
||||
mount root.
|
||||
|
||||
It is **never synced** and holds **no credentials**; the session token stays in
|
||||
`~/.bdrive`.
|
||||
|
||||
Reference in New Issue
Block a user