feat(login): use account chosen on the device-approval page; stdin picker becomes fallback

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Alex Dunmow 2026-07-07 10:37:05 +08:00
parent 098a2091b9
commit 2b685348c9
4 changed files with 84 additions and 7 deletions

View File

@ -57,9 +57,13 @@ func newLoginCmd() *cobra.Command {
cr.Hosts = map[string]creds.HostCreds{}
}
hc := creds.HostCreds{Token: poll.Msg.AccessToken}
authed := orchclient.New(host, hc.Token)
if err := selectActiveAccount(ctx, authed, &hc); err != nil {
return err
if applyPolledAccount(&hc, poll.Msg.AccountId, poll.Msg.AccountSlug) {
fmt.Printf("Active account: %s\n", hc.ActiveAccountSlug)
} else {
authed := orchclient.New(host, hc.Token)
if err := selectActiveAccount(ctx, authed, &hc); err != nil {
return err
}
}
cr.Hosts[host] = hc
if err := cr.Save(); err != nil {
@ -103,6 +107,18 @@ func newWhoamiCmd() *cobra.Command {
}
}
// applyPolledAccount copies the account chosen on the device-approval page
// into hc. Returns false when the server sent no account (older orchestrator,
// or an approval without a selection) — the stdin picker should run instead.
func applyPolledAccount(hc *creds.HostCreds, accountID, accountSlug string) bool {
if accountID == "" {
return false
}
hc.ActiveAccountID = accountID
hc.ActiveAccountSlug = accountSlug
return true
}
// selectActiveAccount fetches the user's accounts and writes the active one
// into hc. With 0 accounts it errors (the server contract guarantees every
// user has at least one). With 1 it auto-selects silently. With ≥2 it

View File

@ -0,0 +1,27 @@
package cmd
import (
"testing"
"git.dev.alexdunmow.com/block/cli/internal/creds"
)
func TestApplyPolledAccount_Set(t *testing.T) {
hc := creds.HostCreds{Token: "tok"}
if !applyPolledAccount(&hc, "acct-id-1", "blockninja") {
t.Fatal("want true when server supplies an account")
}
if hc.ActiveAccountID != "acct-id-1" || hc.ActiveAccountSlug != "blockninja" {
t.Fatalf("hc = %+v, want active account set", hc)
}
}
func TestApplyPolledAccount_EmptyFallsThrough(t *testing.T) {
hc := creds.HostCreds{Token: "tok"}
if applyPolledAccount(&hc, "", "") {
t.Fatal("want false when server sent no account (older orchestrator)")
}
if hc.ActiveAccountID != "" || hc.ActiveAccountSlug != "" {
t.Fatalf("hc = %+v, want untouched", hc)
}
}

View File

@ -5326,6 +5326,8 @@ type PollDeviceResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
AccountId string `protobuf:"bytes,3,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"`
AccountSlug string `protobuf:"bytes,4,opt,name=account_slug,json=accountSlug,proto3" json:"account_slug,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -5374,9 +5376,24 @@ func (x *PollDeviceResponse) GetStatus() string {
return ""
}
func (x *PollDeviceResponse) GetAccountId() string {
if x != nil {
return x.AccountId
}
return ""
}
func (x *PollDeviceResponse) GetAccountSlug() string {
if x != nil {
return x.AccountSlug
}
return ""
}
type ApproveDeviceRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserCode string `protobuf:"bytes,1,opt,name=user_code,json=userCode,proto3" json:"user_code,omitempty"`
AccountId string `protobuf:"bytes,2,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -5418,6 +5435,13 @@ func (x *ApproveDeviceRequest) GetUserCode() string {
return ""
}
func (x *ApproveDeviceRequest) GetAccountId() string {
if x != nil {
return x.AccountId
}
return ""
}
type ApproveDeviceResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
@ -6290,12 +6314,17 @@ const file_orchestrator_v1_plugin_registry_proto_rawDesc = "" +
"\x12expires_in_seconds\x18\x05 \x01(\x05R\x10expiresInSeconds\"4\n" +
"\x11PollDeviceRequest\x12\x1f\n" +
"\vdevice_code\x18\x01 \x01(\tR\n" +
"deviceCode\"O\n" +
"deviceCode\"\x91\x01\n" +
"\x12PollDeviceResponse\x12!\n" +
"\faccess_token\x18\x01 \x01(\tR\vaccessToken\x12\x16\n" +
"\x06status\x18\x02 \x01(\tR\x06status\"3\n" +
"\x06status\x18\x02 \x01(\tR\x06status\x12\x1d\n" +
"\n" +
"account_id\x18\x03 \x01(\tR\taccountId\x12!\n" +
"\faccount_slug\x18\x04 \x01(\tR\vaccountSlug\"R\n" +
"\x14ApproveDeviceRequest\x12\x1b\n" +
"\tuser_code\x18\x01 \x01(\tR\buserCode\"\x17\n" +
"\tuser_code\x18\x01 \x01(\tR\buserCode\x12\x1d\n" +
"\n" +
"account_id\x18\x02 \x01(\tR\taccountId\"\x17\n" +
"\x15ApproveDeviceResponse\"0\n" +
"\x11DenyDeviceRequest\x12\x1b\n" +
"\tuser_code\x18\x01 \x01(\tR\buserCode\"\x14\n" +

View File

@ -650,9 +650,14 @@ message PollDeviceRequest { string device_code = 1; }
message PollDeviceResponse {
string access_token = 1;
string status = 2;
string account_id = 3;
string account_slug = 4;
}
message ApproveDeviceRequest { string user_code = 1; }
message ApproveDeviceRequest {
string user_code = 1;
string account_id = 2;
}
message ApproveDeviceResponse {}
message DenyDeviceRequest { string user_code = 1; }