From 90363ebf0cb6289c3ba695283edb9911bf506b54 Mon Sep 17 00:00:00 2001 From: Alessandro Rinaldi Date: Sun, 6 Sep 2026 00:04:29 +0200 Subject: [PATCH] login: ask which account, so a bare login can add one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The accounts feature says an email not seen before registers a new account, but `dwshell login` could never see one: with an account already configured it took that email and asked only for a password. Adding a second account required knowing to pass --user, and typing the new account's password into a prompt bound to the old account's email failed with nothing pointing at the cause — the prompt never named the account it was for. The email is the account's identity, so it is asked for whenever there is someone to ask, offering the configured account as the answer. Refreshing it stays one keystroke, registering another is just typing it. --user still answers up front, and with no terminal a script uses the default account and is asked nothing, exactly as before. This was shipped broken in v1.4.0: the README documented the flow with --user, which works, but the flow the feature promised did not. Verified in a real terminal: typing a different email at the prompt registered a second account and left the default alone, Enter refreshed the default, and a script with no terminal saw no prompt. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MvidAFW9a2r4hTgHPW9ywG --- README.md | 10 +++++- cmd/dwshell/login_test.go | 70 +++++++++++++++++++++++++++++++++++++++ cmd/dwshell/main.go | 44 +++++++++++++++++++----- 3 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 cmd/dwshell/login_test.go diff --git a/README.md b/README.md index 60a8bf5..fb4f122 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,10 @@ pass, and an existing configuration is carried over the first time dwshell saves anything. ```sh -$ dwshell login --user info@example.com # the email is the account's identity +$ dwshell login # the email is the account's identity +User (email) [ale@example.net]: info@example.com +Password: … + $ dwshell account list ale@example.net (default) info@example.com @@ -269,6 +272,11 @@ $ dwshell list --account info@example.com # the other one $ DWSHELL_ACCOUNT=info@example.com dwshell list # for a whole session ``` +`login` asks which email to use, offering the account you already have: press +Enter to refresh it, or type another to register a second. `--user` answers the +question up front, and without a terminal — a script — the default account is +used and nothing is asked. + The first account you log in with becomes the default; change it with `dwshell account default `. Logging in again with an email already registered just refreshes that account. diff --git a/cmd/dwshell/login_test.go b/cmd/dwshell/login_test.go new file mode 100644 index 0000000..bb98f1c --- /dev/null +++ b/cmd/dwshell/login_test.go @@ -0,0 +1,70 @@ +package main + +import "testing" + +// asking records what the prompt was offered as a default, and answers with a +// canned reply, standing in for a person at a terminal. +func asking(answer string, seen *string) func(string) string { + return func(suggested string) string { + *seen = suggested + return answer + } +} + +func TestLoginUserPrefersTheFlag(t *testing.T) { + var asked string + got := loginUser("typed@x", "default@x", true, asking("ignored@x", &asked)) + if got != "typed@x" { + t.Fatalf("user = %q, want the --user value", got) + } + if asked != "" { + t.Error("with --user given there is nothing to ask") + } +} + +// The whole point: at a terminal the email is asked, so a bare `dwshell login` +// can register an account that is not the default one. +func TestLoginUserAsksSoAnAccountCanBeAdded(t *testing.T) { + var asked string + got := loginUser("", "default@x", true, asking("second@x", &asked)) + if got != "second@x" { + t.Fatalf("user = %q: a new email typed at the prompt must be honoured", got) + } + if asked != "default@x" { + t.Errorf("the prompt should offer the default account, offered %q", asked) + } +} + +// Answering nothing accepts the default, so refreshing the account you already +// have stays a single keystroke. +func TestLoginUserEmptyAnswerKeepsTheDefault(t *testing.T) { + var asked string + got := loginUser("", "default@x", true, asking("", &asked)) + if got != "default@x" { + t.Fatalf("user = %q, want the default", got) + } +} + +// With no terminal there is nobody to ask, so a script refreshing the default +// account keeps working exactly as before. +func TestLoginUserFallsBackToTheDefaultWithoutATerminal(t *testing.T) { + var asked string + got := loginUser("", "default@x", false, asking("unused@x", &asked)) + if got != "default@x" { + t.Fatalf("user = %q, want the default", got) + } + if asked != "" { + t.Error("nothing may be asked when there is no terminal") + } +} + +func TestLoginUserAsksWithNothingConfigured(t *testing.T) { + var asked string + got := loginUser("", "", true, asking("first@x", &asked)) + if got != "first@x" { + t.Fatalf("user = %q", got) + } + if asked != "" { + t.Errorf("with no account there is no default to offer, offered %q", asked) + } +} diff --git a/cmd/dwshell/main.go b/cmd/dwshell/main.go index 240b67d..6c99383 100644 --- a/cmd/dwshell/main.go +++ b/cmd/dwshell/main.go @@ -196,14 +196,7 @@ func cmdLogin(ctx context.Context, args []string) int { if err != nil { return fail("%v", err) } - if user == "" { - // Re-logging in with no --user refreshes the default account. - user = c.Config().Default - } - if user == "" { - fmt.Fprint(os.Stderr, "User (email): ") - fmt.Scanln(&user) - } + user = loginUser(user, c.Config().Default, term.IsTTY(), promptUser) if user == "" { return fail("a user is required") } @@ -223,6 +216,41 @@ func cmdLogin(ctx context.Context, args []string) int { return 0 } +// loginUser decides which account `login` authenticates. +// +// The email is asked for whenever there is someone to ask, because it is the +// account's identity: skipping the question when an account already exists +// would make a bare `dwshell login` able only ever to refresh that one, never +// to register a second. The account already configured is offered as the +// answer, so refreshing it stays one keystroke. +// +// With no terminal — a script — there is nobody to ask, so the default account +// is used, which is what such a script has always done. +func loginUser(flagUser, defaultAccount string, interactive bool, prompt func(suggested string) string) string { + if flagUser != "" { + return flagUser + } + if !interactive { + return defaultAccount + } + if answer := strings.TrimSpace(prompt(defaultAccount)); answer != "" { + return answer + } + return defaultAccount +} + +// promptUser asks for the account email, offering the one already configured. +func promptUser(suggested string) string { + if suggested != "" { + fmt.Fprintf(os.Stderr, "User (email) [%s]: ", suggested) + } else { + fmt.Fprint(os.Stderr, "User (email): ") + } + var answer string + fmt.Scanln(&answer) + return answer +} + func readPassword() (string, error) { if p := os.Getenv("DWSHELL_PASSWORD"); p != "" { return p, nil