Files
beardrive/cmd/bdrive/scope.go
T
0236e1b272 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>
2026-07-27 14:49:58 +09:00

170 lines
4.5 KiB
Go

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, "/"))
}
}