diff --git a/internal/cli/login.go b/internal/cli/login.go index f6935ef..0b6983a 100644 --- a/internal/cli/login.go +++ b/internal/cli/login.go @@ -3,6 +3,7 @@ package cli import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -26,6 +27,7 @@ const loginWaitTimeout = 10 * time.Minute func newLoginDoc(app *App) *CommandDoc { var noBrowser bool + var device bool return &CommandDoc{ Use: "login", Summary: "Sign in to Extend through your browser (no API key needed)", @@ -34,17 +36,25 @@ func newLoginDoc(app *App) *CommandDoc { "log in to the extend cli with my browser", "authenticate the cli without creating an api key", "oauth login for the extend cli", + "sign in to extend over ssh or on a headless machine", }, WhenToUse: `Use to authenticate interactively without creating an API key: it opens your browser, you pick one workspace and one environment on the consent screen, and the CLI stores the resulting tokens securely. Prefer an API key (EXTEND_API_KEY or 'extend setup') for CI and unattended use; login -needs a browser once, though the stored session then works headlessly -until it expires or is revoked.`, +needs a browser once — on this machine or, with the device flow, on any +other device — and the stored session then works headlessly until it +expires or is revoked.`, Details: `The flow is a standard native-app OAuth authorization code grant with PKCE: the CLI listens on an ephemeral 127.0.0.1 port, sends you to the Extend consent screen, and exchanges the returned code for tokens. +When no usable local browser is detected (an SSH session, or a Linux +host with no display), login automatically switches to the OAuth device +flow: it prints a short one-time code and a URL, you approve the +sign-in from a browser on any device, and the CLI picks the tokens up. +Pass --device to use that flow explicitly. + Tokens are stored in the OS keychain when one is available, otherwise in a 0600 file next to the config file (~/.config/extend/oauth_tokens.json). Logins are stored per API base URL, so sessions against different @@ -59,6 +69,7 @@ config file always wins over a stored login. Run 'extend config' to see which source is in effect.`, Examples: []Example{ {Label: "Sign in", Cmd: "extend login"}, + {Label: "Sign in from an SSH session or headless machine", Cmd: "extend login --device"}, {Label: "Print the URL instead of opening a browser", Cmd: "extend login --no-browser"}, {Label: "Sign in to the EU region", Cmd: "extend login --region eu"}, }, @@ -67,18 +78,24 @@ which source is in effect.`, "Each login targets exactly one workspace and one environment, chosen on the consent screen; log in again to switch.", "Logins are keyed by API base URL: switching --region or EXTEND_BASE_URL selects a different stored session (or none).", "With --env