From 4c9a98ee34f6383e551987a14379280f65fe0520 Mon Sep 17 00:00:00 2001 From: Rizel Scarlett Date: Fri, 1 May 2026 07:18:50 -0400 Subject: [PATCH 1/2] Copy login device code to clipboard --- cmd/entire/cli/login.go | 29 +++++++++++++++++++++++++-- cmd/entire/cli/login_test.go | 38 ++++++++++++++++++++++++++++++++++++ go.mod | 2 +- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/cmd/entire/cli/login.go b/cmd/entire/cli/login.go index 88618c522e..7b83af7444 100644 --- a/cmd/entire/cli/login.go +++ b/cmd/entire/cli/login.go @@ -12,6 +12,7 @@ import ( "runtime" "time" + "github.com/atotto/clipboard" "github.com/entireio/cli/cmd/entire/cli/auth" "github.com/entireio/cli/cmd/entire/cli/interactive" "github.com/spf13/cobra" @@ -26,6 +27,9 @@ const maxTransientErrors = 5 // browserOpenFunc is the signature for opening a URL in the user's browser. type browserOpenFunc func(ctx context.Context, url string) error +// clipboardWriteFunc is the signature for copying text to the user's clipboard. +type clipboardWriteFunc func(text string) error + // deviceAuthClient abstracts the auth client so runLogin and waitForApproval can be unit-tested. type deviceAuthClient interface { StartDeviceAuth(ctx context.Context) (*auth.DeviceAuthStart, error) @@ -42,20 +46,23 @@ func newLoginCmd() *cobra.Command { if err := requireSecureBaseURL(insecureHTTPAuth); err != nil { return err } - return runLogin(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), auth.NewClient(nil), openBrowser) + return runLogin(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), auth.NewClient(nil), openBrowser, copyToClipboard) }, } addInsecureHTTPAuthFlag(cmd, &insecureHTTPAuth) return cmd } -func runLogin(ctx context.Context, outW, errW io.Writer, client deviceAuthClient, openURL browserOpenFunc) error { +func runLogin(ctx context.Context, outW, errW io.Writer, client deviceAuthClient, openURL browserOpenFunc, writeClipboard clipboardWriteFunc) error { start, err := client.StartDeviceAuth(ctx) if err != nil { return fmt.Errorf("start login: %w", err) } fmt.Fprintf(outW, "Device code: %s\n", start.UserCode) + if copied := copyDeviceCodeToClipboard(errW, start.UserCode, writeClipboard); copied { + fmt.Fprintln(outW, "Device code copied to clipboard.") + } approvalURL := start.VerificationURI @@ -94,6 +101,24 @@ func runLogin(ctx context.Context, outW, errW io.Writer, client deviceAuthClient return nil } +func copyDeviceCodeToClipboard(errW io.Writer, userCode string, writeClipboard clipboardWriteFunc) bool { + if writeClipboard == nil { + return false + } + if err := writeClipboard(userCode); err != nil { + fmt.Fprintf(errW, "Warning: failed to copy device code to clipboard: %v\n", err) + return false + } + return true +} + +func copyToClipboard(text string) error { + if err := clipboard.WriteAll(text); err != nil { + return fmt.Errorf("write clipboard: %w", err) + } + return nil +} + func waitForApproval(ctx context.Context, poller deviceAuthClient, deviceCode string, expiresIn int, interval, slowDownBackoff time.Duration) (string, error) { expiry := time.Duration(expiresIn) * time.Second if expiry <= 0 || expiry > maxExpiresIn { diff --git a/cmd/entire/cli/login_test.go b/cmd/entire/cli/login_test.go index 63834fec3a..66bd0a1a09 100644 --- a/cmd/entire/cli/login_test.go +++ b/cmd/entire/cli/login_test.go @@ -1,6 +1,7 @@ package cli import ( + "bytes" "context" "errors" "strings" @@ -38,6 +39,43 @@ func (m *mockClient) PollDeviceAuth(_ context.Context, _ string) (*auth.DeviceAu return r.result, r.err } +func TestCopyDeviceCodeToClipboard_Success(t *testing.T) { + t.Parallel() + + var errBuf bytes.Buffer + var copied string + ok := copyDeviceCodeToClipboard(&errBuf, "ABCD-1234", func(text string) error { + copied = text + return nil + }) + + if !ok { + t.Fatal("copyDeviceCodeToClipboard() = false, want true") + } + if copied != "ABCD-1234" { + t.Fatalf("copied = %q, want device code", copied) + } + if errBuf.Len() != 0 { + t.Fatalf("stderr = %q, want empty", errBuf.String()) + } +} + +func TestCopyDeviceCodeToClipboard_Failure(t *testing.T) { + t.Parallel() + + var errBuf bytes.Buffer + ok := copyDeviceCodeToClipboard(&errBuf, "ABCD-1234", func(_ string) error { + return errors.New("clipboard unavailable") + }) + + if ok { + t.Fatal("copyDeviceCodeToClipboard() = true, want false") + } + if !strings.Contains(errBuf.String(), "failed to copy device code to clipboard") { + t.Fatalf("stderr = %q, want clipboard warning", errBuf.String()) + } +} + func TestWaitForApproval_ImmediateSuccess(t *testing.T) { t.Parallel() diff --git a/go.mod b/go.mod index fe4a0873b2..86c59062a7 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( charm.land/glamour/v2 v2.0.0 charm.land/huh/v2 v2.0.3 charm.land/lipgloss/v2 v2.0.3 + github.com/atotto/clipboard v0.1.4 github.com/betterleaks/betterleaks v1.1.2 github.com/creack/pty v1.1.24 github.com/denisbrodbeck/machineid v1.0.1 @@ -40,7 +41,6 @@ require ( github.com/alecthomas/chroma/v2 v2.14.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect - github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect From 4f2a13a2636c99ff6fbf2f4b73660000561807ca Mon Sep 17 00:00:00 2001 From: goose-guest Date: Thu, 14 May 2026 22:30:52 -0700 Subject: [PATCH 2/2] Copy login device code after confirmation --- cmd/entire/cli/login.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/entire/cli/login.go b/cmd/entire/cli/login.go index 7b83af7444..24497dd63b 100644 --- a/cmd/entire/cli/login.go +++ b/cmd/entire/cli/login.go @@ -60,14 +60,11 @@ func runLogin(ctx context.Context, outW, errW io.Writer, client deviceAuthClient } fmt.Fprintf(outW, "Device code: %s\n", start.UserCode) - if copied := copyDeviceCodeToClipboard(errW, start.UserCode, writeClipboard); copied { - fmt.Fprintln(outW, "Device code copied to clipboard.") - } approvalURL := start.VerificationURI if interactive.CanPromptInteractively() { - fmt.Fprintf(outW, "Press Enter to open %s in your browser and enter the generated device code...", approvalURL) + fmt.Fprintf(outW, "Press Enter to copy the code to your clipboard, open %s in your browser, and enter the generated device code...", approvalURL) // Read from /dev/tty so we get a real keypress and don't consume piped stdin. if err := waitForEnter(ctx); err != nil { @@ -75,6 +72,9 @@ func runLogin(ctx context.Context, outW, errW io.Writer, client deviceAuthClient } fmt.Fprintln(outW) + if copied := copyDeviceCodeToClipboard(errW, start.UserCode, writeClipboard); copied { + fmt.Fprintln(outW, "Device code copied to clipboard.") + } if err := openURL(ctx, approvalURL); err != nil { fmt.Fprintf(errW, "Warning: failed to open browser: %v\n", err)