diff --git a/README.md b/README.md index 9d1071b..c4eb85a 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ hub's own storage, never something a syncing client points at directly: | Command | Description | |---|---| -| `bdrive login [server-url]` | Sign this device in (browser flow; `--device` for headless; default server beardrive.ai — the managed cloud, free personal workspace on signup; pass your hub URL to self-host). Switch hubs with `bdrive login ` | +| `bdrive login [server-url]` | Sign this device in (browser flow; `--device` forces the code flow, and shells without a TTY fall back to it automatically; default server beardrive.ai — the managed cloud, free personal workspace on signup; pass your hub URL to self-host). Switch hubs with `bdrive login ` | | `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 (files stay; `bdrive init` resumes) | @@ -148,7 +148,8 @@ hub's own storage, never something a syncing client points at directly: | `bdrive status [folder]` | Projects, daemon state, pending changes | | `bdrive log [folder] [-p path] [-n N]` | Change history: account, device, time, file | | `bdrive web [folder \| storage-root-url]` | Web server: viewer (rendered markdown, downloads, history), uploads, multi-project sync hub | -| `bdrive whoami` | Device identity used in change tracking | +| `bdrive whoami` | Signed-in account and device identity used in change tracking | +| `bdrive version` | Print the version (also `bdrive --version`) | ## Project files diff --git a/cmd/bdrive/init.go b/cmd/bdrive/init.go index 080b4b4..678d403 100644 --- a/cmd/bdrive/init.go +++ b/cmd/bdrive/init.go @@ -156,7 +156,18 @@ the folder was renamed or moved.`, if shared != "" { fmt.Printf(" syncing: ./%s only\n", shared) } - return startSync(cmd.Context(), folder, proj, foreground, 3*time.Second, 10*time.Second) + if err := startSync(cmd.Context(), folder, proj, foreground, 3*time.Second, 10*time.Second); err != nil { + return err + } + fmt.Printf(` +done — the daemon now keeps this folder in sync automatically. + +next steps: + connect another device or teammate: bdrive init --project %s + see who changed what: bdrive log + share a file by public URL: bdrive share +`, p.ID) + return nil }, } c.Flags().StringVar(&projectID, "project", "", "connect an existing project by id (p-xxxxxxxx)") diff --git a/cmd/bdrive/login.go b/cmd/bdrive/login.go index 5e3f708..d8db396 100644 --- a/cmd/bdrive/login.go +++ b/cmd/bdrive/login.go @@ -17,6 +17,7 @@ import ( "strings" "time" + "github.com/mattn/go-isatty" "github.com/spf13/cobra" "github.com/runbear-io/beardrive/internal/config" @@ -163,6 +164,13 @@ func runLogin(server string, cfg serverConfig, useDevice bool) error { if loginPath == "" { loginPath = "/auth/cli" } + // 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()) { + fmt.Println("no interactive terminal detected — using the device-code sign-in flow") + useDevice = true + } var token string var user serverUser var err error @@ -170,6 +178,10 @@ func runLogin(server string, cfg serverConfig, useDevice bool) error { token, user, err = deviceCodeLogin(server) } else { token, user, err = browserLogin(server, loginPath) + if errors.Is(err, errNoBrowser) { + fmt.Println("could not open a browser — switching to the device-code sign-in flow") + token, user, err = deviceCodeLogin(server) + } } if err != nil { return err @@ -238,6 +250,10 @@ func deviceName() string { return "cli" } +// errNoBrowser signals that the loopback flow can't proceed because no local +// browser could be opened; the caller falls back to the device-code flow. +var errNoBrowser = errors.New("no local browser") + // browserLogin runs the loopback-callback flow. func browserLogin(server, loginPath string) (string, serverUser, error) { ln, err := net.Listen("tcp", "127.0.0.1:0") @@ -272,8 +288,11 @@ func browserLogin(server, loginPath string) (string, serverUser, error) { fmt.Println("opening your browser to sign in (sign up there if you don't have an account):") fmt.Println(" " + loginURL) if err := openBrowser(loginURL); err != nil { - fmt.Println("could not open a browser — open the URL above manually, or rerun with --device") + // The loopback callback only works from a browser on this machine, so + // a URL the user pastes elsewhere would dead-end — use the code flow. + return "", serverUser{}, errNoBrowser } + fmt.Println("waiting for the browser sign-in… (no browser here? Ctrl-C and run `bdrive login --device`)") select { case code := <-codeCh: diff --git a/cmd/bdrive/main.go b/cmd/bdrive/main.go index 2f06ddc..ea03a7e 100644 --- a/cmd/bdrive/main.go +++ b/cmd/bdrive/main.go @@ -28,7 +28,9 @@ 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.`, SilenceUsage: true, + Version: version, } + root.SetVersionTemplate("beardrive {{.Version}}\n") root.AddCommand( loginCmd(), logoutCmd(), @@ -77,7 +79,17 @@ func whoamiCmd() *cobra.Command { } fmt.Printf("device id: %s\n", dev.ID) fmt.Printf("device name: %s\n", dev.Name) - fmt.Printf("author: %s\n", dev.Author) + if settings, _ := config.LoadSettings(); settings.Email != "" { + who := settings.Email + if settings.Name != "" { + who = settings.Name + " <" + settings.Email + ">" + } + fmt.Printf("account: %s (from `bdrive login`; changes are attributed to this)\n", who) + fmt.Printf("author: %s (git/OS fallback, used only when signed out)\n", dev.Author) + } else { + fmt.Printf("account: not signed in — changes are attributed to the author below (run `bdrive login`)\n") + fmt.Printf("author: %s (detected from git config / OS user)\n", dev.Author) + } fmt.Printf("beardrive home: %s\n", home) return nil }, diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 25e11dd..a9e28bc 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -134,8 +134,6 @@ func Run(folder string, scanInterval, remoteInterval time.Duration) error { } defer os.Remove(PidPath(volDir)) - settings, _ := config.LoadSettings() - log.Printf("daemon started: folder=%s mount=%s volume=%s remote=%q device=%s(%s) scan=%s sync=%s", folder, proj.ID, proj.Volume, proj.Remote, dev.Name, dev.ID, scanInterval, remoteInterval) @@ -191,6 +189,10 @@ 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/plugin/skills/beardrive/SKILL.md b/plugin/skills/beardrive/SKILL.md index 929dba1..36c1901 100644 --- a/plugin/skills/beardrive/SKILL.md +++ b/plugin/skills/beardrive/SKILL.md @@ -23,7 +23,7 @@ Use this skill whenever the user is working with the `bdrive` CLI: initializing | Mounts + daemon + pending state | `bdrive status []` | | Change history | `bdrive log [] [-p path] [-n N]` | | This device's identity | `bdrive whoami` | -| Sign this device in (once per device) | `bdrive login [url]` — bare form targets BearDrive Cloud (beardrive.ai): signing up there auto-creates a free personal workspace, no questions asked. Self-hosting teams pass their hub URL instead. Opens the sign-in page in a browser (sign-up available there); the terminal completes on its own and stores a per-device token. `--device` prints a code to approve from any browser (SSH/headless); `--status` shows server + account. Password reset: "Forgot password?" on the sign-in page (emailed via the server's SMTP config, or the link appears in the server log). **Switch hubs** with `bdrive login `, then re-run `bdrive init` in each folder. | +| Sign this device in (once per device) | `bdrive login [url]` — bare form targets BearDrive Cloud (beardrive.ai): signing up there auto-creates a free personal workspace, no questions asked. Self-hosting teams pass their hub URL instead. Opens the sign-in page in a browser (sign-up available there); the terminal completes on its own and stores a per-device token. `--device` prints a code to approve from any browser (SSH/headless), and login falls back to that code flow automatically when there is no TTY (agent shells, CI) or no browser opens; `--status` shows server + account. Password reset: "Forgot password?" on the sign-in page (emailed via the server's SMTP config, or the link appears in the server log). **Switch hubs** with `bdrive login `, then re-run `bdrive init` in each folder. | | Sign this device out | `bdrive logout` — clears the saved token + account (folders untouched); `--forget` also drops the remembered server. The device token stays valid server-side until it expires — revoke it from the hub's device list to be sure. | | Link a synced file for teammates | `bdrive url ` — prints the file's hub viewer URL (sign-in + project membership required; always the latest content). Computed locally, no network; `--sync` pushes first so a just-created file's link resolves immediately; no arg = the project home page. **After creating a shareable artifact (.md/.html/.csv/report/plan) in the shared folder, include this link in your reply** so teammates can open it. | | Share a synced file publicly by URL | `bdrive share ` — prints a link anyone can open (HTML renders as a page, markdown rendered, PDFs inline; sandboxed; always the latest content; no account needed). `--expires 24h` for self-destructing links; `--list` / `--revoke ` to manage. Put generated reports in the shared folder, sync, then share. | @@ -484,12 +484,14 @@ History is content-addressed — overwritten and deleted files are still in the ``` device id: d380dea58598 device name: macbook -author: snow@runbear.io +account: Snow (from `bdrive login`; changes are attributed to this) +author: snow@runbear.io (git/OS fallback, used only when signed out) beardrive home: /Users/snow/.bdrive ``` - **device id** — random 12-hex, generated on first run, persisted to `~/.bdrive/device.json`. - **device name** — hostname (without `.local`). +- **account** — the signed-in hub account; changes are attributed to it in `bdrive log` and hub history. When signed out, the author fallback is used instead. - **author** — `git config user.email` if present, else `$USER@`. To change name/author, edit `~/.bdrive/device.json` and restart the daemon (`bdrive stop`/`bdrive init`). diff --git a/web/docs/src/content/docs/reference/cli.md b/web/docs/src/content/docs/reference/cli.md index e254b3b..7d068d2 100644 --- a/web/docs/src/content/docs/reference/cli.md +++ b/web/docs/src/content/docs/reference/cli.md @@ -9,7 +9,7 @@ One binary, `bdrive` — the CLI, the sync daemon, and the web server. | Command | Description | |---|---| -| `bdrive login [server-url]` | Sign this device in. Browser flow; `--device` for headless. Default server is beardrive.ai — the managed cloud, free personal workspace on signup; pass your hub URL to self-host. Switch hubs with `bdrive login `. `--status` shows the current server and account | +| `bdrive login [server-url]` | Sign this device in. Browser flow; `--device` forces the code flow, and shells without a TTY (agents, CI, SSH) fall back to it automatically. Default server is beardrive.ai — the managed cloud, free personal workspace on signup; pass your hub URL to self-host. Switch hubs with `bdrive login `. `--status` shows the current server and account | | `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. Files stay on disk; `bdrive init` resumes | @@ -22,8 +22,8 @@ One binary, `bdrive` — the CLI, the sync daemon, and the web server. | `bdrive status [folder]` | Projects, daemon state, pending changes | | `bdrive log [folder] [-p path] [-n N]` | Change history: account, device, time, file | | `bdrive web [folder \| storage-root-url]` | Web server: viewer, uploads, multi-project sync hub | -| `bdrive whoami` | Device identity used in change tracking | -| `bdrive version` | Version | +| `bdrive whoami` | Signed-in account and device identity used in change tracking | +| `bdrive version` | Version (also `bdrive --version`) | ## Notes on a few