review fixes: gate init next-steps on background mode, shared stdinIsTTY, daemon reconnects on token change, whoami surfaces settings errors, doc staleness (#41)

- init -f no longer prints 'daemon now keeps this folder in sync' after
  the foreground daemon has exited
- one stdinIsTTY() helper (TTY or Cygwin pty) shared by init's prompt
  gate and login's headless fallback — the two sites disagreed on Cygwin
- the daemon drops its remote backend when the device token changes, so
  an account switch mid-run reconnects with the new credential instead
  of pushing with the old one (httpBackend captures the token at open)
- whoami reports a settings read error instead of claiming 'not signed in'
- self-hosting/authentication and manual/setup-by-hand now describe the
  automatic device-code fallback instead of presenting --device as required

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Snow W. Lee (Sungwon)
2026-07-22 15:23:56 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 2b9beaf41a
commit 220e27a9c2
7 changed files with 39 additions and 13 deletions
+8
View File
@@ -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 {
+4 -2
View File
@@ -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.
+1 -2
View File
@@ -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
}
+4 -1
View File
@@ -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 + ">"
+16 -4
View File
@@ -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
@@ -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.
@@ -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