Skip to content

feat(auth): browser-assisted CLI login + profiles + auth/team commands#84

Merged
LanusseMorais merged 5 commits into
mainfrom
feature/PD-6071-cli-browser-login
Jun 9, 2026
Merged

feat(auth): browser-assisted CLI login + profiles + auth/team commands#84
LanusseMorais merged 5 commits into
mainfrom
feature/PD-6071-cli-browser-login

Conversation

@LanusseMorais

@LanusseMorais LanusseMorais commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a browser-assisted login flow to lsh, so users no longer need to manually copy an API token from the dashboard. Adds multi-team support via profiles, plus commands to inspect and switch the active context.

$ lsh login
Opening your browser to authorize this CLI...

  URL:
    https://www.latitude.sh/dashboard/cli/authorize?session=<id>

  Confirm this code matches what your browser shows:
    WDJB-MJHT

Waiting for approval... press Ctrl+C to cancel.

✅ Logged in as you@example.com on team Acme (profile: acme)

New commands

Command Description
lsh login Browser-assisted login (default). Falls back to manual URL on headless.
lsh login --with-token <T> Validates the token against /user/profile and stores it under a profile.
lsh auth status Show the active profile (email, team, key name, source).
lsh auth logout [--profile X | --all] Removes a profile. Revokes the API key remotely when source is browser.
lsh profile use <name> Set the active profile (accepts the profile name or the slug/id of the team it is bound to).
lsh profile list Lists the profiles you are logged into.

New global flag and env vars

Name Purpose
--profile <name> One-shot override of the default profile for a single invocation.
LATITUDESH_TOKEN Use this token directly, bypassing any stored profile. Useful in CI.
LSH_PROFILE Override the default profile via environment.
LSH_PROJECT Pre-fill the --project flag (skips the interactive prompt).

Interactive project picker

Commands that take --project (e.g. lsh servers list, lsh servers create) now prompt interactively when the flag is missing and stdin is a TTY:

? Select a project
  ▸ acme-prod        Acme Prod — proj_xxx
    acme-staging     Acme Staging — proj_yyy
    All projects     Run across every project in this team

For list commands, picking All projects (or passing --all-projects) lists across every project.

In non-interactive contexts the command fails with an actionable message:

Error: --project is required (pass --project=<id>, --all-projects, or set LSH_PROJECT)

Config file

The config at ~/.config/lsh/config.json now supports multiple profiles. Existing single-token configs are migrated automatically on first run.

Before:

{ "Authorization": "ak_xxx", "API-Version": "2023-06-01" }

After (auto-migrated as profile default):

{
  "default_profile": "acme",
  "profiles": {
    "acme": {
      "authorization": "ak_xxx",
      "team_id": "...", "team_name": "Acme", "team_slug": "acme",
      "email": "you@example.com",
      "source": "with-token",
      "api_version": "2023-06-01"
    }
  }
}

Permissions: 0700 directory, 0600 file.

Backward compatibility

  • lsh login <token> (positional) still works but prints a deprecation warning.
  • Old config layout is migrated transparently.
  • Generated command flags (--project=<id> etc.) unchanged.

Breaking change

lsh servers list and lsh virtual-networks list no longer silently list across all projects when --project is missing. Behaviour now:

  • TTY: shows the picker.
  • Non-TTY: fails with the message above.

Existing scripts can opt back into the old behaviour with --all-projects, --project=<id>, or LSH_PROJECT=<id>.

Notes

  • Two generated files (cli/get_servers_operation.go, cli/get_virtual_networks_operation.go) gained a manually-added --all-projects flag. Lines are tagged // MANUAL — keep when regenerating to survive future swagger regenerations.

How to test

go build -o ./lsh-dev .

1. Token login (covers most of the flow without needing the browser page)

./lsh-dev login --with-token <YOUR_TOKEN>
./lsh-dev auth status
./lsh-dev profile list
cat ~/.config/lsh/config.json | jq

Expected:

  • auth status shows Email, Team, Source: with-token.
  • profile list shows the profile with a * marker.
  • Config has a profiles map with the team slug as key.

2. Switch between profiles

Run login --with-token twice with tokens from different teams (or pass --profile=<name> to override the auto-naming).

./lsh-dev profile list
./lsh-dev profile use <other-team-slug>
./lsh-dev auth status                              # default switched
./lsh-dev --profile <slug> auth status             # one-shot override
LSH_PROFILE=<slug> ./lsh-dev auth status           # env override
LATITUDESH_TOKEN=ak_xxx ./lsh-dev auth status      # bypass any profile

3. Logout

./lsh-dev auth logout                              # active profile
./lsh-dev auth logout --profile <slug>             # specific
./lsh-dev auth logout --all                        # everything

Logout on a profile with source: browser revokes the API key remotely (you can verify under Settings → API Keys); logout on a --with-token profile only clears it locally.

4. Project picker

After logging in, run a command that needs a project:

./lsh-dev servers list

Expected: an interactive picker listing projects + an All projects entry. Selecting a project filters results; selecting All projects lists everything.

Other invocations to test:

./lsh-dev servers list --project=<id>              # filter, no prompt
./lsh-dev servers list --all-projects              # all, no prompt
LSH_PROJECT=<id> ./lsh-dev servers list            # env, no prompt
./lsh-dev servers list < /dev/null                 # non-TTY, fails with an actionable error

5. Browser-assisted login

Requires the matching dashboard page deployed (the page lives in a separate repo and is being shipped in parallel — you'll see the URL the CLI prints).

./lsh-dev login

Expected: prints the URL + a user_code; opens the browser if available; polls until you approve. After approval, prints ✅ Logged in as .... If the browser cannot be opened (SSH session, no DISPLAY, piped stdin), the CLI prints the URL and waits — you open it from another machine.

6. Config migration

Manually drop a legacy config and verify it gets migrated:

mkdir -p ~/.config/lsh
cat > ~/.config/lsh/config.json <<'EOF2'
{ "Authorization": "ak_old_token", "API-Version": "2023-06-01" }
EOF2

./lsh-dev auth status
cat ~/.config/lsh/config.json | jq

Expected: a default profile materialized from the legacy field, source: with-token. Subsequent runs are no-ops.

Greptile Summary

This PR adds a complete browser-assisted login flow (lsh login), multi-team profile support, and three new command groups (auth, profile, login) to the CLI. It migrates the existing single-token config format to a multi-profile store, adds an interactive project picker for commands that need --project, and introduces LATITUDESH_TOKEN / LSH_PROFILE / LSH_PROJECT environment overrides.

  • Auth flow: POST /auth/cli_sessions creates a session; the CLI polls with X-CLI-Secret until status=approved, extracts the returned API key/team/user, and saves a named profile. --with-token takes the fast path via GET /user/profile + GET /team.
  • Config: ~/.config/lsh/config.json is rewritten as a {default_profile, profiles:{…}} map with an atomic temp-file rename and 0600 perms; old single-token files are auto-migrated on first load and the migration is persisted immediately.
  • Project picker: resolveProjectFlag runs in PersistentPreRunE; on an interactive TTY it fetches projects and launches a bubbletea list; on a non-TTY it returns an actionable error. --all-projects (manually added to two generated files) bypasses the requirement.

Confidence Score: 4/5

The core auth and config logic is well-tested and covers migration, atomic writes, profile resolution order, and both login flows. The known previous issues are all addressed in this diff.

The implementation is thorough and previously raised blocking issues have been fixed. Two UX concerns remain: the headless banner is misleading before the headless note appears, and in the --all logout path API keys are permanently revoked server-side before the local config write succeeds, which can leave profiles on disk pointing to revoked tokens if the write fails. Neither breaks correctness in the common path.

cli/auth_login_browser.go (headless banner wording) and cli/auth_logout.go (revoke ordering before Save in both single and --all paths)

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as lsh CLI
    participant API as Latitude API
    participant Browser

    Note over CLI: InitViperConfigs → HydrateFromActiveProfile("")
    Note over CLI: PersistentPreRunE runs after flag parse

    alt lsh login (browser flow)
        User->>CLI: lsh login
        CLI->>API: POST /auth/cli_sessions
        API-->>CLI: "{id, secret, user_code, authorize_url}"
        CLI->>Browser: Open authorize_url (if not headless)
        CLI->>User: Print URL + user_code
        loop Poll every 2s (max 5m30s)
            CLI->>API: "GET /auth/cli_sessions/{id} [X-CLI-Secret: secret]"
            API-->>CLI: "{status: pending} or {status: approved, api_key, team, user}"
        end
        CLI->>CLI: saveProfile(team_slug, profile)
        CLI->>User: Logged in
    else lsh login --with-token T
        User->>CLI: lsh login --with-token ak_xxx
        CLI->>API: GET /user/profile [Authorization: ak_xxx]
        API-->>CLI: "{email}"
        CLI->>API: GET /team [Authorization: ak_xxx]
        API-->>CLI: "[{id, name, slug}]"
        CLI->>CLI: "saveProfile(team_slug, profile source=with-token)"
        CLI->>User: Logged in
    end

    alt lsh auth logout
        User->>CLI: lsh auth logout
        CLI->>CLI: config.Load()
        opt "source == browser"
            CLI->>API: "DELETE /auth/api_keys/{key_id}"
        end
        CLI->>CLI: config.Save() atomic rename
        CLI->>User: Removed profile
    end

    alt lsh servers list
        User->>CLI: lsh servers list
        Note over CLI: PersistentPreRunE: HydrateFromActiveProfile
        alt --project set or LSH_PROJECT
            CLI->>API: "GET /servers?filter[project]=X"
        else interactive TTY
            CLI->>API: GET /projects paginated
            API-->>CLI: "[{id, name, slug}]"
            CLI->>User: Interactive project picker
            User-->>CLI: select project or All projects
            CLI->>API: GET /servers with or without project filter
        else non-TTY no --project
            CLI->>User: Error --project is required
        end
    end
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
cli/auth_login_browser.go:207-221
**Misleading banner in headless environments**

`printAuthorizePrompt` unconditionally prints "Opening your browser to authorize this CLI..." regardless of the `headless` parameter, so users in SSH sessions or CI see that line before the "(detected headless environment — open the URL above on a machine with a browser)" notice. Someone watching output scroll by is likely to wait for a browser that never opens, then scroll back to find the headless note. The first line should vary based on `headless`.

### Issue 2 of 2
cli/auth_logout.go:415-430
**Remote revoke before local persist in `--all` path**

`revokeIfBrowserSourced` is called for each profile inside the loop before `config.Save`. If `Save` subsequently fails (e.g. full disk), all browser-sourced API keys will have been permanently revoked at the server but every profile remains on disk with its now-invalid token. The same ordering issue exists in the single-profile path (line 440). A subsequent `lsh auth logout --all` will trigger warnings ("could not revoke API key") for each entry but local cleanup will eventually succeed. If this sequence is acceptable as a best-effort trade-off, adding a comment explaining the intentional choice would help future readers.

Reviews (4): Last reviewed commit: "fix(auth): review fixes + tests for PD-6..." | Re-trigger Greptile

@LanusseMorais

Copy link
Copy Markdown
Collaborator Author

@greptile review please

Comment thread cli/auth_login_browser.go
Comment thread internal/config/config.go Outdated
Comment thread cli/auth_login_token.go
Comment thread internal/config/config.go Outdated
…sist legacy migration, don't overwrite default profile on login, reset poll backoff + fail-fast on approved-without-key, honor LATITUDESH_TOKEN in auth status, env api-version for --with-token, stderr warnings, filepath.Dir)
@LanusseMorais

Copy link
Copy Markdown
Collaborator Author

@greptile review

@LanusseMorais

Copy link
Copy Markdown
Collaborator Author

@greptile review please

…le UX, atomic config, project-picker pagination, hydration exemption; config/authclient/cli tests)
@LanusseMorais

Copy link
Copy Markdown
Collaborator Author

@greptile review please

@LanusseMorais LanusseMorais merged commit 4b8f4b0 into main Jun 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants