provider: a subscription used through its vendor's CLI — Devin's, Cursor's, Grok's — installs that CLI when its sign-in starts, if this machine lacks it, rather than leaving the sign-in to fail or, as Devin's did, to say "signed in" and show nothing: Devin's sign-in wrote credentials.toml, but magpie sees the account only through devin auth status, so with no devin CLI no provider ever came and nothing said why. The sign-in is now "installing" while the vendor's own installer runs (curl … | bash, or its PowerShell one on Windows, with magpie's proxy), then opens the vendor's page as before; the window says what it is installing and why, can cancel it, and a failed install says the installer's last line and the command to run by hand. What counts is the CLI being there afterwards — Devin's installer ends in devin setup, whose sign-in gives up without a terminal and fails the script after it installed everything — and on Windows the PATH the installer wrote is taken up so the running app finds it. A Devin sign-in the CLI then can't see fails, where it said done with no one; the tests no longer see this machine's own devin

This commit is contained in:
yetone committed 2026-09-25 16:30:00 +08:00
1 parent c5a887458f
commit e072af501d
12 files changed
+486 -49

No files matched your search

+10
View File
@@ -253,6 +253,16 @@ func addAccount(agentID string) error {
if err != nil {
return err
}
if st.State == "installing" {
fmt.Printf("Installing %s, which %s is used through…\n", st.Installing, agentID)
for st.State == "installing" {
time.Sleep(500 * time.Millisecond)
st, _ = provider.SignInStatus(st.ID)
}
if st.State != "waiting" {
return fmt.Errorf("sign-in didn't finish: %s", st.Error)
}
}
fmt.Println("Finish signing in in your browser. If it didn't open, go to:")
fmt.Println(faint.Render(st.URL))
openInBrowser(st.URL)
+20 -4
View File
@@ -2715,11 +2715,12 @@ const SUBS = [
{ agent: "antigravity", name: "Antigravity", icon: "antigravity-color", plans: "Google AI Pro · Ultra · free", risk: true },
];
const subOf = (agent) => SUBS.find((x) => x.agent === agent);
let signing = null; // the sign-in under way: { id, agent, url, state, error }
let signing = null; // the sign-in under way: { id, agent, url, state, installing, error }
const signingOpen = () => signing?.state === "waiting" || signing?.state === "installing";
let justAdded = ""; // the account that just came in, to greet it
async function startSignIn(agent, risky) {
if (signing?.state === "waiting") api("signin/" + signing.id + "/cancel", {}).catch(() => {});
if (signingOpen()) api("signin/" + signing.id + "/cancel", {}).catch(() => {});
// an account Google may suspend is added only once that is said
if (subOf(agent)?.risk && !risky) {
signing = { agent, state: "risk" };
@@ -2739,11 +2740,17 @@ async function startSignIn(agent, risky) {
}
async function followSignIn(id) {
while (signing?.id === id && signing.state === "waiting") {
while (signing?.id === id && signingOpen()) {
await new Promise((r) => setTimeout(r, 800));
let st;
try { st = await api("signin/" + id); } catch { continue; }
if (signing?.id !== id || st.state === "waiting") continue;
if (signing?.id !== id) continue;
if (st.state === "waiting" || st.state === "installing") {
// the CLI it needed is in: now the vendor's page can open
if (signing.state === "installing" && st.state === "waiting" && st.url) api("open", { url: st.url }).catch(() => {});
if (signing.state !== st.state || signing.url !== st.url) { signing = st; renderProviders(); }
continue;
}
if (st.state === "done") {
signing = null;
justAdded = st.user;
@@ -2797,6 +2804,15 @@ function renderSigning(sub) {
return box;
}
box.append(el("span", "spinner"));
if (signing.state === "installing") {
tt.append(el("span", "n", t("Installing {cli}…", { cli: signing.installing })),
el("span", "s", t("{name} is used through its own CLI, which isn't on this computer yet. magpie is installing it with the official installer; the sign-in page opens as soon as it's done.", { name: sub.name })));
box.append(tt);
const x = el("button", "text", t("Cancel"));
x.onclick = cancelSignIn;
box.append(x);
return box;
}
tt.append(el("span", "n", t("Finish signing in to {name} in your browser", { name: sub.name })),
el("span", "s", signing.code ? t("magpie opened GitHub's device page. Enter this code there; the account shows up here as soon as you're done.") : t("magpie opened the sign-in page. The account shows up here as soon as you're done.")));
if (signing.code) {
+2
View File
@@ -214,6 +214,8 @@ const I18N = {
"{name} accounts can be suspended": "{name} 账号可能被封禁",
"Google may suspend an Antigravity account it sees used outside Antigravity. Use one you can afford to lose.": "Google 发现 Antigravity 账号在 Antigravity 之外使用时,可能会封禁该账号。请用一个丢了也无妨的账号。",
"Sign in anyway": "仍然登录",
"Installing {cli}…": "正在安装 {cli}…",
"{name} is used through its own CLI, which isn't on this computer yet. magpie is installing it with the official installer; the sign-in page opens as soon as it's done.": "{name} 需要通过它自己的 CLI 使用,这台电脑上还没有安装。magpie 正在用官方安装脚本安装,装好后会立即打开登录页面。",
"magpie opened the sign-in page. The account shows up here as soon as you're done.": "magpie 已打开登录页面,完成后账号会立即出现在这里。",
"Open again": "重新打开",
"Copy link": "复制链接",
+5 -1
View File
@@ -523,7 +523,11 @@ func providerRoutes(mux *http.ServeMux, w Windows, gw *gateway.Server) {
fail(rw, err)
return
}
w.OpenURL(st.URL)
if st.URL != "" {
// one that installs a CLI first has no URL yet: the window
// opens it once there is one
w.OpenURL(st.URL)
}
writeJSON(rw, st)
})
mux.HandleFunc("GET /api/signin/{id}", func(rw http.ResponseWriter, r *http.Request) {
+6 -2
View File
@@ -66,16 +66,20 @@ func signIn(t *testing.T) string {
func isolate(t *testing.T) {
t.Helper()
oldKeychain, oldURL, oldBase, oldExe := claudeKeychain, claudeTokenURL, claudeBase, claudeExecutable
oldCursor := cursorKeychain
oldCursor, oldDevin := cursorKeychain, DevinExecutable
claudeKeychain, cursorKeychain = false, false
claudeExecutable = func() string { return "" }
// the machine's own devin, if it has one, is no test's
DevinExecutable = func() string { return "" }
forgetClaudeCredential()
forgetClaudeStatus()
forgetDevinStatus()
t.Cleanup(func() {
claudeKeychain, claudeTokenURL, claudeBase, claudeExecutable = oldKeychain, oldURL, oldBase, oldExe
cursorKeychain = oldCursor
cursorKeychain, DevinExecutable = oldCursor, oldDevin
forgetClaudeCredential()
forgetClaudeStatus()
forgetDevinStatus()
})
}
+7 -1
View File
@@ -307,7 +307,13 @@ func devinExchange(ctx context.Context, code, verifier, redirect string) (savedL
return savedLogin{}, err
}
forgetDevinStatus()
user, plan, _ := askDevinIdentity()
forgetAccountCaches()
user, plan, ok := askDevinIdentity()
if !ok {
// the account is only ever seen through the CLI: without it
// answering, magpie has nothing to show and nothing to run
return savedLogin{}, errors.New("signed in, but `devin auth status` doesn't show the account; run it in a terminal to see why")
}
return savedLogin{Agent: "devin", User: user, Plan: plan}, nil
}
+2
View File
@@ -262,7 +262,9 @@ func runCLISignIn(s *signInFlow, what string, env []string, using bool, failed f
cancel()
return err
}
s.mu.Lock()
s.stop = cancel
s.mu.Unlock()
got := make(chan string, 1)
go func() {
sc := bufio.NewScanner(out)
+116
View File
@@ -0,0 +1,116 @@
package provider
// Some subscriptions are used through the vendor's own CLI — Cursor's,
// Grok's, Devin's — and signed in with it too. One that isn't installed is
// installed with the vendor's own installer when its sign-in starts, as the
// vendor's page tells a person to, rather than leaving the sign-in to fail
// or, as Devin's did, to finish with nothing to show for it.
import (
"context"
"os"
"os/exec"
"runtime"
"strings"
"time"
"github.com/yetone/magpie/internal/netproxy"
"github.com/yetone/magpie/internal/proc"
)
// agentCLI is a CLI a subscription runs through: how to find it, and the
// vendor's install one-liners for a POSIX shell and for PowerShell.
type agentCLI struct {
Name string
find func() string
sh string
ps string
}
func cliFor(agent string) (agentCLI, bool) {
switch agent {
case "devin":
return agentCLI{"Devin CLI", func() string { return DevinExecutable() },
"curl -fsSL https://cli.devin.ai/install.sh | bash",
"irm https://static.devin.ai/cli/setup.ps1 | iex"}, true
case "cursor":
return agentCLI{"Cursor CLI", func() string { return CursorExecutable() },
"curl https://cursor.com/install -fsS | bash",
"irm 'https://cursor.com/install?win32=true' | iex"}, true
case "grok":
return agentCLI{"Grok CLI", func() string { return GrokExecutable() },
"curl -fsSL https://x.ai/cli/install.sh | bash",
"irm https://x.ai/cli/install.ps1 | iex"}, true
}
return agentCLI{}, false
}
// missingCLI is the CLI an agent's sign-in needs and this machine lacks.
func missingCLI(agent string) (agentCLI, bool) {
c, ok := cliFor(agent)
if !ok || c.find() != "" {
return agentCLI{}, false
}
return c, true
}
// installTimeout bounds an installer: they download a few hundred MB at most.
var installTimeout = 10 * time.Minute
// runInstaller runs a vendor's installer; a var so tests can fake it.
var runInstaller = func(ctx context.Context, c agentCLI) ([]byte, error) {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = proc.CommandContext(ctx, "powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", c.ps)
} else {
cmd = proc.CommandContext(ctx, "bash", "-c", c.sh)
}
cmd.Dir, _ = os.UserHomeDir()
cmd.Env = netproxy.Env(nil)
// no stdin: an installer that would ask something takes its default
return cmd.CombinedOutput()
}
// installCLI installs c with its vendor's installer and makes sure magpie
// finds it afterwards.
func installCLI(ctx context.Context, c agentCLI) error {
ctx, cancel := context.WithTimeout(ctx, installTimeout)
defer cancel()
out, err := runInstaller(ctx, c)
// an installer adds its directory to the user's PATH, which a running
// program never sees
refreshPath()
manual := c.sh
if runtime.GOOS == "windows" {
manual = c.ps
}
if ctx.Err() == context.Canceled {
return ctx.Err()
}
// what counts is the CLI being there: an installer may end in a step
// that wants a terminal — Devin's runs `devin setup`, whose sign-in
// gives up without one — and fail after it installed everything
if c.find() != "" {
return nil
}
if err != nil {
msg := lastLine(out)
if msg == "" {
msg = err.Error()
}
return errorf("installing %s failed (%s); install it yourself with `%s`, then sign in again", c.Name, msg, manual)
}
return errorf("the %s installer finished, but magpie can't find it; install it yourself with `%s`, then sign in again", c.Name, manual)
}
// lastLine is the last line of a command's output with something on it,
// without its colours.
func lastLine(out []byte) string {
lines := strings.Split(ansi.ReplaceAllString(string(out), ""), "\n")
for i := len(lines) - 1; i >= 0; i-- {
if l := strings.TrimSpace(lines[i]); l != "" {
return l
}
}
return ""
}
+7
View File
@@ -0,0 +1,7 @@
//go:build !windows
package provider
// refreshPath has nothing to do: magpie looks for the CLIs where their
// installers put them.
func refreshPath() {}
+160
View File
@@ -0,0 +1,160 @@
package provider
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// a sign-in that needs a CLI this machine lacks installs it, then goes on
func TestSignInInstallsMissingCLI(t *testing.T) {
if os.PathSeparator != '/' {
t.Skip("the fake devin is a shell script")
}
home := claudeHome(t)
t.Setenv("XDG_DATA_HOME", filepath.Join(home, "data"))
exe := filepath.Join(home, "bin", "devin")
oldExe := DevinExecutable
DevinExecutable = func() string {
if isFile(exe) {
return exe
}
return ""
}
t.Cleanup(func() { DevinExecutable = oldExe })
release := make(chan struct{})
var ran string
oldRun := runInstaller
runInstaller = func(ctx context.Context, c agentCLI) ([]byte, error) {
ran = c.sh
<-release
os.MkdirAll(filepath.Dir(exe), 0o755)
os.WriteFile(exe, []byte("#!/bin/sh\nprintf 'Logged in (via Devin).\\n Email: dev@example.com\\n Tier: Devin Pro\\n'\n"), 0o755)
// as Devin's does: `devin setup` gives up on its sign-in without a terminal
return []byte("Installed devin\nError: Login canceled\n"), errors.New("exit status 1")
}
t.Cleanup(func() { runInstaller = oldRun })
fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"sessionToken":"devin-session-token$sk-test"}`))
}))
defer fake.Close()
oldTok := devinExchangeURL
devinExchangeURL = fake.URL
t.Cleanup(func() { devinExchangeURL = oldTok })
st, err := StartSignIn("devin")
if err != nil {
t.Fatal(err)
}
if st.State != "installing" || st.Installing != "Devin CLI" || st.URL != "" {
t.Fatalf("started %+v", st)
}
close(release)
st = waitPast(t, st.ID, "installing")
if st.State != "waiting" || !strings.HasPrefix(st.URL, devinAuthorizeURL) || st.Installing != "" {
t.Fatalf("after install %+v", st)
}
if !strings.Contains(ran, "cli.devin.ai/install.sh") {
t.Fatalf("installer %q", ran)
}
finishInBrowser(t, st, "dv-code")
if st = waitDone(t, st.ID); st.State != "done" || st.User != "dev@example.com" {
t.Fatalf("state %+v", st)
}
}
// an installer that fails says so, and how to install it by hand
func TestSignInInstallFails(t *testing.T) {
claudeHome(t)
oldExe := DevinExecutable
DevinExecutable = func() string { return "" }
t.Cleanup(func() { DevinExecutable = oldExe })
oldRun := runInstaller
runInstaller = func(ctx context.Context, c agentCLI) ([]byte, error) {
return []byte("downloading…\n\x1b[0;31mError: Failed to download\x1b[0m\n"), errors.New("exit status 1")
}
t.Cleanup(func() { runInstaller = oldRun })
st, err := StartSignIn("devin")
if err != nil {
t.Fatal(err)
}
st = waitPast(t, st.ID, "installing")
if st.State != "failed" || !strings.Contains(st.Error, "Error: Failed to download") || !strings.Contains(st.Error, "install.") {
t.Fatalf("state %+v", st)
}
}
// canceled while installing: the sign-in never starts
func TestSignInCanceledWhileInstalling(t *testing.T) {
claudeHome(t)
oldExe := DevinExecutable
DevinExecutable = func() string { return "" }
t.Cleanup(func() { DevinExecutable = oldExe })
stopped := make(chan struct{})
oldRun := runInstaller
runInstaller = func(ctx context.Context, c agentCLI) ([]byte, error) {
<-ctx.Done()
close(stopped)
return nil, ctx.Err()
}
t.Cleanup(func() { runInstaller = oldRun })
st, _ := StartSignIn("devin")
CancelSignIn(st.ID)
select {
case <-stopped:
case <-time.After(5 * time.Second):
t.Fatal("the installer went on")
}
if st, _ = SignInStatus(st.ID); st.State != "canceled" {
t.Fatalf("state %+v", st)
}
}
// signed in, but the CLI can't see it: a failure, not "done" with no one
func TestDevinSignInUnseen(t *testing.T) {
if os.PathSeparator != '/' {
t.Skip("the fake devin is a shell script")
}
home := claudeHome(t)
t.Setenv("XDG_DATA_HOME", filepath.Join(home, "data"))
exe := filepath.Join(home, "devin")
os.WriteFile(exe, []byte("#!/bin/sh\necho 'Not logged in.'\n"), 0o755)
oldExe := DevinExecutable
DevinExecutable = func() string { return exe }
t.Cleanup(func() { DevinExecutable = oldExe })
fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"sessionToken":"devin-session-token$sk-test"}`))
}))
defer fake.Close()
oldTok := devinExchangeURL
devinExchangeURL = fake.URL
t.Cleanup(func() { devinExchangeURL = oldTok })
st, err := StartSignIn("devin")
if err != nil {
t.Fatal(err)
}
finishInBrowser(t, st, "dv-code")
if st = waitDone(t, st.ID); st.State != "failed" || !strings.Contains(st.Error, "devin auth status") {
t.Fatalf("state %+v", st)
}
}
func waitPast(t *testing.T, id, state string) SignInState {
t.Helper()
for end := time.Now().Add(5 * time.Second); time.Now().Before(end); time.Sleep(20 * time.Millisecond) {
if st, _ := SignInStatus(id); st.State != state {
return st
}
}
t.Fatalf("still %s", state)
return SignInState{}
}
+43
View File
@@ -0,0 +1,43 @@
package provider
import (
"os"
"strings"
"golang.org/x/sys/windows/registry"
)
// refreshPath takes up the PATH an installer just wrote to the registry,
// which this process, started before it, doesn't have.
func refreshPath() {
var dirs []string
for _, k := range []struct {
root registry.Key
path string
}{
{registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\Session Manager\Environment`},
{registry.CURRENT_USER, `Environment`},
} {
key, err := registry.OpenKey(k.root, k.path, registry.QUERY_VALUE)
if err != nil {
continue
}
if v, _, err := key.GetStringValue("Path"); err == nil {
for _, d := range strings.Split(v, ";") {
if d = strings.TrimSpace(os.ExpandEnv(strings.ReplaceAll(d, "%", "$"))); d != "" {
dirs = append(dirs, d)
}
}
}
key.Close()
}
seen := map[string]bool{}
var path []string
for _, d := range append(strings.Split(os.Getenv("PATH"), ";"), dirs...) {
if l := strings.ToLower(d); d != "" && !seen[l] {
seen[l] = true
path = append(path, d)
}
}
os.Setenv("PATH", strings.Join(path, ";"))
}
+108 -41
View File
@@ -48,13 +48,15 @@ var signInTimeout = 10 * time.Minute
type SignInState struct {
ID string `json:"id"`
Agent string `json:"agent"`
URL string `json:"url"` // the vendor's page, to open or copy
Code string `json:"code,omitempty"` // what to type there, for a device code
State string `json:"state"` // waiting, done, failed or canceled
User string `json:"user,omitempty"` // the account, once done
Plan string `json:"plan,omitempty"` //
Using bool `json:"using,omitempty"` // the agent was signed in to it too
Error string `json:"error,omitempty"`
URL string `json:"url"` // the vendor's page, to open or copy
Code string `json:"code,omitempty"` // what to type there, for a device code
State string `json:"state"` // installing, waiting, done, failed or canceled
// Installing is the CLI being installed before the sign-in can start
Installing string `json:"installing,omitempty"`
User string `json:"user,omitempty"` // the account, once done
Plan string `json:"plan,omitempty"` //
Using bool `json:"using,omitempty"` // the agent was signed in to it too
Error string `json:"error,omitempty"`
}
type signInFlow struct {
@@ -81,9 +83,84 @@ func randomToken(n int) string {
// StartSignIn begins adding an account to an agent: open the returned URL
// in a browser and the rest happens on its own; SignInStatus follows it.
//
// An agent signed in with a CLI that isn't installed has it installed first:
// the sign-in is then "installing", and gets its URL when that is done.
func StartSignIn(agent string) (SignInState, error) {
s := &signInFlow{verifier: randomToken(48), state: randomToken(24), done: make(chan struct{})}
s.st = SignInState{ID: randomToken(9), Agent: agent, State: "waiting"}
cli, install := missingCLI(agent)
var installing context.Context
if install {
s.st.State, s.st.Installing = "installing", cli.Name
// canceling the sign-in stops the installer
installing, s.stop = context.WithCancel(context.Background())
} else if err := s.begin(); err != nil {
return SignInState{}, err
}
timeout := signInTimeout
if install {
timeout += installTimeout
}
go func() {
select {
case <-s.done:
case <-time.After(timeout):
s.finish(SignInState{State: "failed", Error: "the sign-in timed out; start it again"})
}
}()
signIns.Lock()
// only one sign-in per agent at a time: a newer one replaces the older
for id, o := range signIns.m {
if o.st.Agent == agent {
o.finish(SignInState{State: "canceled"})
delete(signIns.m, id)
}
}
signIns.m[s.st.ID] = s
signIns.Unlock()
if install {
go s.installThenBegin(installing, cli)
}
return s.status(), nil
}
// installThenBegin installs the CLI the sign-in needs, then starts it.
func (s *signInFlow) installThenBegin(ctx context.Context, cli agentCLI) {
err := installCLI(ctx, cli)
s.mu.Lock()
s.stop = nil
s.mu.Unlock()
if err == nil {
forgetAccountCaches()
err = s.begin()
}
if err != nil {
s.finish(SignInState{State: "failed", Error: err.Error()})
return
}
s.mu.Lock()
if s.st.State == "installing" {
s.st.State, s.st.Installing = "waiting", ""
s.mu.Unlock()
return
}
// canceled while it began: let go of what it started
stop, srv := s.stop, s.srv
s.mu.Unlock()
if stop != nil {
stop()
}
if srv != nil {
_ = srv.Close()
}
}
// begin starts the vendor's sign-in: the URL to open, and whatever waits
// for the browser to come back.
func (s *signInFlow) begin() error {
agent := s.st.Agent
sum := sha256.Sum256([]byte(s.verifier))
challenge := base64.RawURLEncoding.EncodeToString(sum[:])
@@ -93,7 +170,7 @@ func StartSignIn(agent string) (SignInState, error) {
switch agent {
case "claude":
if ln, err = net.Listen("tcp", "127.0.0.1:0"); err != nil {
return SignInState{}, err
return err
}
s.redirect = fmt.Sprintf("http://localhost:%d/callback", ln.Addr().(*net.TCPAddr).Port)
q.Set("code", "true")
@@ -107,7 +184,7 @@ func StartSignIn(agent string) (SignInState, error) {
s.st.URL = claudeAuthorizeURL + "?" + q.Encode()
case "codex":
if ln, err = listenCodexCallback(); err != nil {
return SignInState{}, err
return err
}
_, port, _ := net.SplitHostPort(codexCallbackAddr)
s.redirect = "http://localhost:" + port + "/auth/callback"
@@ -125,18 +202,18 @@ func StartSignIn(agent string) (SignInState, error) {
case "cursor":
// Cursor has no sign-in of its own to borrow: its CLI signs in
if err := startCursorSignIn(s); err != nil {
return SignInState{}, err
return err
}
case "grok":
// so is Grok: its CLI signs in with a device code
if err := startGrokSignIn(s); err != nil {
return SignInState{}, err
return err
}
case "devin":
// `devin auth login` is a TUI over this same PKCE + localhost
// callback, so magpie runs the round itself
if ln, err = net.Listen("tcp", "127.0.0.1:0"); err != nil {
return SignInState{}, err
return err
}
s.redirect = fmt.Sprintf("http://127.0.0.1:%d/callback", ln.Addr().(*net.TCPAddr).Port)
q.Set("redirect_uri", s.redirect)
@@ -145,48 +222,37 @@ func StartSignIn(agent string) (SignInState, error) {
q.Set("code_challenge", challenge)
q.Set("code_challenge_method", "S256")
q.Set("cli_pkce_marker", "1")
// set with the lock: the window follows a sign-in that installed
// devin first while this runs
s.mu.Lock()
s.st.URL = devinAuthorizeURL + "?" + q.Encode()
s.mu.Unlock()
case "copilot":
// GitHub's device code, as Copilot's editors sign in
if err := startCopilotSignIn(s); err != nil {
return SignInState{}, err
return err
}
case "gemini", "antigravity":
// Google's sign-in, under the app's own OAuth client
app, _ := googleAppOf(agent)
if ln, err = net.Listen("tcp", "127.0.0.1:0"); err != nil {
return SignInState{}, err
return err
}
startGoogleSignIn(s, app, ln.Addr().(*net.TCPAddr).Port, challenge)
default:
return SignInState{}, fmt.Errorf("magpie can't sign in to %s accounts", agent)
return fmt.Errorf("magpie can't sign in to %s accounts", agent)
}
if ln != nil {
mux := http.NewServeMux()
mux.HandleFunc("/", s.callback)
s.srv = &http.Server{Handler: mux, ReadHeaderTimeout: 10 * time.Second}
go func() { _ = s.srv.Serve(ln) }()
srv := &http.Server{Handler: mux, ReadHeaderTimeout: 10 * time.Second}
s.mu.Lock()
s.srv = srv
s.mu.Unlock()
go func() { _ = srv.Serve(ln) }()
}
go func() {
select {
case <-s.done:
case <-time.After(signInTimeout):
s.finish(SignInState{State: "failed", Error: "the sign-in timed out; start it again"})
}
}()
signIns.Lock()
// only one sign-in per agent at a time: a newer one replaces the older
for id, o := range signIns.m {
if o.st.Agent == agent {
o.finish(SignInState{State: "canceled"})
delete(signIns.m, id)
}
}
signIns.m[s.st.ID] = s
signIns.Unlock()
return s.status(), nil
return nil
}
// listenCodexCallback takes Codex's callback port. A `codex login` left
@@ -255,22 +321,23 @@ func (s *signInFlow) status() SignInState {
// finish records the outcome once and lets the callback server go.
func (s *signInFlow) finish(out SignInState) bool {
s.mu.Lock()
if s.st.State != "waiting" {
if s.st.State != "waiting" && s.st.State != "installing" {
s.mu.Unlock()
return false
}
out.ID, out.Agent, out.URL, out.Code = s.st.ID, s.st.Agent, s.st.URL, s.st.Code
s.st = out
stop, srv := s.stop, s.srv
s.mu.Unlock()
if out.State == "done" {
// signing in again brings back an account removed from magpie
_ = ShowAccount(out.Agent)
}
close(s.done)
if s.stop != nil {
s.stop()
if stop != nil {
stop()
}
if s.srv == nil {
if srv == nil {
return true
}
go func() {
@@ -278,7 +345,7 @@ func (s *signInFlow) finish(out SignInState) bool {
time.Sleep(time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = s.srv.Shutdown(ctx)
_ = srv.Shutdown(ctx)
}()
return true
}