diff --git a/cmd/bdrive/helpers.go b/cmd/bdrive/helpers.go index 81ee49d..d05df31 100644 --- a/cmd/bdrive/helpers.go +++ b/cmd/bdrive/helpers.go @@ -6,12 +6,20 @@ import ( "os" "path/filepath" + "github.com/mattn/go-isatty" + "github.com/runbear-io/beardrive/internal/config" "github.com/runbear-io/beardrive/internal/remote" "github.com/runbear-io/beardrive/internal/store" "github.com/runbear-io/beardrive/internal/syncer" ) +// stdinIsTTY is the one answer to "is this an interactive shell?" — used both +// to decide whether init may prompt and whether login can drive a browser. +func stdinIsTTY() bool { + return isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) +} + func absFolder(args []string) (string, error) { arg := "." if len(args) > 0 { diff --git a/cmd/bdrive/init.go b/cmd/bdrive/init.go index 678d403..d6bdd43 100644 --- a/cmd/bdrive/init.go +++ b/cmd/bdrive/init.go @@ -14,7 +14,6 @@ import ( "time" "github.com/AlecAivazis/survey/v2" - "github.com/mattn/go-isatty" "github.com/spf13/cobra" "github.com/runbear-io/beardrive/internal/config" @@ -97,7 +96,7 @@ the folder was renamed or moved.`, } server := settings.Server - interactive := isatty.IsTerminal(os.Stdin.Fd()) && !yes + interactive := stdinIsTTY() && !yes // Which project? var p serverProject @@ -159,6 +158,9 @@ the folder was renamed or moved.`, if err := startSync(cmd.Context(), folder, proj, foreground, 3*time.Second, 10*time.Second); err != nil { return err } + if foreground { + return nil // daemon already ran and exited; "syncing automatically" would be false now + } fmt.Printf(` done — the daemon now keeps this folder in sync automatically. diff --git a/cmd/bdrive/login.go b/cmd/bdrive/login.go index d8db396..a8f69ef 100644 --- a/cmd/bdrive/login.go +++ b/cmd/bdrive/login.go @@ -17,7 +17,6 @@ import ( "strings" "time" - "github.com/mattn/go-isatty" "github.com/spf13/cobra" "github.com/runbear-io/beardrive/internal/config" @@ -167,7 +166,7 @@ func runLogin(server string, cfg serverConfig, useDevice bool) error { // Headless shells (agents, CI, SSH) can't complete the loopback-callback // flow — the browser would open nowhere and the CLI would hang. Fall back // to the device-code flow automatically instead of waiting. - if !useDevice && !isatty.IsTerminal(os.Stdin.Fd()) && !isatty.IsCygwinTerminal(os.Stdin.Fd()) { + if !useDevice && !stdinIsTTY() { fmt.Println("no interactive terminal detected — using the device-code sign-in flow") useDevice = true } diff --git a/cmd/bdrive/main.go b/cmd/bdrive/main.go index ea03a7e..caaccb8 100644 --- a/cmd/bdrive/main.go +++ b/cmd/bdrive/main.go @@ -79,7 +79,10 @@ func whoamiCmd() *cobra.Command { } fmt.Printf("device id: %s\n", dev.ID) fmt.Printf("device name: %s\n", dev.Name) - if settings, _ := config.LoadSettings(); settings.Email != "" { + settings, serr := config.LoadSettings() + if serr != nil { + fmt.Printf("account: unknown — cannot read settings: %v\n", serr) + } else if settings.Email != "" { who := settings.Email if settings.Name != "" { who = settings.Name + " <" + settings.Email + ">" diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index a9e28bc..e09397d 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -144,6 +144,7 @@ func Run(folder string, scanInterval, remoteInterval time.Duration) error { } }() var lastRemote time.Time + var lastToken string for { // Re-read the project config each tick: picks up `bdrive remote set` @@ -177,6 +178,21 @@ func Run(folder string, scanInterval, remoteInterval time.Duration) error { } proj = cur + // Re-read settings each tick too, so a login/logout/account switch + // after the daemon started is reflected in op authorship — otherwise + // a long-lived daemon stamps every change with a stale identity. The + // http backend captures its credential at open, so drop it when the + // token changes and reconnect with the new one. + settings, _ := config.LoadSettings() + if settings.Token != lastToken { + if be != nil { + be.Close() + be = nil + } + lastToken = settings.Token + lastRemote = time.Time{} + } + doRemote := proj.Remote != "" && time.Since(lastRemote) >= remoteInterval if doRemote && be == nil { b, err := remote.Open(ctx, proj.Remote) @@ -189,10 +205,6 @@ func Run(folder string, scanInterval, remoteInterval time.Duration) error { } } - // Re-read settings each tick too, so a login/logout/account switch - // after the daemon started is reflected in op authorship — otherwise - // a long-lived daemon stamps every change with a stale identity. - settings, _ := config.LoadSettings() sess := &syncer.Session{Folder: folder, MountID: proj.ID, Store: st, Device: dev, Account: settings} if doRemote { sess.Backend = be diff --git a/web/docs/src/content/docs/manual/setup-by-hand.md b/web/docs/src/content/docs/manual/setup-by-hand.md index 92c8af1..a24efaa 100644 --- a/web/docs/src/content/docs/manual/setup-by-hand.md +++ b/web/docs/src/content/docs/manual/setup-by-hand.md @@ -28,8 +28,9 @@ bdrive login https://your-hub ``` This opens your browser, and the terminal finishes on its own. On a headless or -SSH machine, `bdrive login --device` prints a short code you approve from any -signed-in browser instead. +SSH machine login falls back to the device-code flow automatically (no TTY, or +no browser can open): it prints a short code you approve from any signed-in +browser. `bdrive login --device` forces that flow. `bdrive login --status` shows the current server and account. diff --git a/web/docs/src/content/docs/self-hosting/authentication.md b/web/docs/src/content/docs/self-hosting/authentication.md index 6474405..5ad11ce 100644 --- a/web/docs/src/content/docs/self-hosting/authentication.md +++ b/web/docs/src/content/docs/self-hosting/authentication.md @@ -63,8 +63,9 @@ right there if needed. When the user signs in, the page bounces a one-time code to the CLI's loopback listener and the terminal finishes on its own, storing a long-lived per-device token that is revocable server-side. -On headless or SSH machines, `bdrive login --device` prints a short code to -approve from any signed-in browser instead. +On headless or SSH machines login falls back to the device-code flow +automatically (no TTY, or no browser can open): it prints a short code to +approve from any signed-in browser. `bdrive login --device` forces that flow. Every sync and every `bdrive init` then authenticates with that token. The hub's device registry records per-device name, OS, account, and the IP the server