Skip to content

feat(cli): login progress spinner, auth status --check, client status --wait (#138)#146

Merged
saadqbal merged 7 commits into
developfrom
feat/138-login-progress-authcheck
Jul 7, 2026
Merged

feat(cli): login progress spinner, auth status --check, client status --wait (#138)#146
saadqbal merged 7 commits into
developfrom
feat/138-login-progress-authcheck

Conversation

@saadqbal

@saadqbal saadqbal commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #138. Part of the installer first-run UX v2 spec (umbrella backend#992; interim copy fixes shipped in client#307). The installer shells out to the CLI for sign-in, session state, and connect verification — these three output/contract changes make the approved installer copy renderable and honest.

1. login — progress display

  • Poll loop extracted into pollForToken, wrapped in a live ui.Spinner (braille frame + mm:ss elapsed + (Ctrl-C to cancel)).
  • TTY-gated: piped / --plain / NO_COLOR output prints one static line — no \r, no ANSI. The spinner line clears on exit so the prints in its place.
  • Sign-in fields relabelled open:/code:Open/Enter (imperative ui.Action rows).
  • "Token saved to ~/.tracebloc (0600)." demoted to a dim, verbose-only detail — the happy path is just ✔ Signed in as ….

2. auth status --check

Machine-readable session probe. Exit 0 only when a token is present and the backend accepts it (live WhoAmI); exit 1 when signed out or the token is rejected. Silent by default (exit-1 is a nil-inner exitError, so main prints nothing); --verbose narrates the verdict. The installer's session-probe step stops grepping prose.

3. client status [--wait] [--timeout]

Reports the backend's view of this machine's active client (online/offline/pending). --wait polls the same source client list renders until the backend reports it online (exit 0) or the timeout elapses (non-zero, with a plain-language last-state line). This is the honest 🟢 Online signal (RFC-0001 §8.5) — the installer's closing claim was inferred only from a local kubectl rollout check, which can't tell whether tracebloc can actually see the client.

Acceptance (all covered by tests)

  • login on a TTY shows spinner + elapsed; --plain/piped stays static (no raw ANSI) — verified by a static-path test + a race-free direct draw test of the mm:ss format.
  • auth status --check: 0 signed-in-valid / 1 signed-out or rejected; no output unless --verbose.
  • client status --wait: 0 only on backend-reported online; non-zero on timeout with a last-state line.

New ui: Spinner (animate-vs-static contract) + Action rows. Polling reuses the existing pollAfter seam plus a new clientStatusPollInterval var so tests drive many iterations with no real delay.

Local: full go test ./... -race green; go vet, gofmt, errcheck, goimports clean. Built the binary and smoke-tested --check exit codes + help wiring.

Coordination notes

🤖 Generated with Claude Code


Note

Medium Risk
Touches authentication probing (WhoAmI, env matching) and installer exit-code contracts; mistakes could skip login or mis-report session validity, though behavior is heavily tested.

Overview
Improves installer first-run UX with three CLI contract/output changes and shared terminal helpers.

tracebloc login pulls device-token polling into pollForToken, shown behind a ui.Spinner (animated on TTY, one static line when piped/--plain/NO_COLOR). Sign-in steps use bold Open / Enter Action rows instead of dim open:/code: fields. After approval, success is ✔ Signed in as …; “token saved…” moves to verbose-only Detailf. Login poll Ctrl-C exits 130 silently.

tracebloc auth status --check (optional --env) is a silent, exit-code probe: 0 only when signed in to the target environment and WhoAmI succeeds; 1 when signed out, env mismatch (no backend call), or invalid/unverifiable session. 426 shows upgrade text even without --verbose; verbose mode distinguishes rejected tokens (401/403) from outages.

tracebloc client status reports the active client’s backend state; --wait polls until online (or --timeout, default 120s) with a spinner, fail-fast on 426, 401/403, or missing active client, and timeout messages that include last state or list errors. --timeout without --wait is rejected.

internal/ui: new Action and Spinner (Windows uses static spinner to avoid raw ANSI redraw).

Broad test coverage for --check, --wait, spinner/static paths, and edge cases from review/Bugbot.

Reviewed by Cursor Bugbot for commit f70e13c. Bugbot is set up for automated code reviews on this repo. Configure here.

… --wait (#138)

Three installer-facing contract changes so the approved installer copy is
renderable and honest (installer UX v2 spec):

- login progress: extract the RFC 8628 poll loop into pollForToken, wrapped in
  a live ui.Spinner (braille frame + mm:ss elapsed + "(Ctrl-C to cancel)").
  TTY-gated — piped/--plain/NO_COLOR output prints one static line, no \r/ANSI.
  Sign-in fields relabelled open:/code: → Open/Enter (imperative ui.Action
  rows), and "Token saved…" demoted to a dim verbose-only detail so the happy
  path is just "✔ Signed in as …".
- auth status --check: a machine-readable session probe. Exit 0 only when a
  token is present AND the backend accepts it (live WhoAmI); exit 1 (silent,
  nil-inner exitError) when signed out or the token is rejected. --verbose
  narrates the verdict. Installer's session-probe step stops grepping prose.
- client status [--wait] [--timeout]: reports the backend's view of this
  machine's active client (online/offline/pending). --wait polls the same
  source `client list` renders until online (exit 0) or timeout (non-zero, with
  a plain-language last-state line) — the honest 🟢 Online signal (§8.5),
  replacing the local-rollout-only inference.

New ui: Spinner (animate-vs-static contract) + Action rows. Poll/interval go
through the existing pollAfter seam + a new clientStatusPollInterval var so
tests drive many iterations with no real delay. Tests cover all three
(including a race-free direct draw test and the timeout/last-state path); full
suite green under -race, errcheck/goimports/gofmt/vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread internal/cli/auth.go
Comment thread internal/cli/client.go
@LukasWodka

Copy link
Copy Markdown
Contributor

👋 Heads-up — Code review queue is at 35 / 30

Above the WIP limit. The team convention is to review existing PRs before opening new work.

Open PRs currently in Code review (oldest first):

Pull from review before opening new work. (This is a nudge from the kanban WIP check, not a block.)

@saadqbal saadqbal self-assigned this Jul 7, 2026

@LukasWodka LukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review (multi-agent: 8 finder angles → independent verification per finding; the top two confirmed empirically with throwaway tests against this branch). Overall: solid craftsmanship — the spinner's animate-vs-static contract is explicit and race-free by design, --check's silence reuses the nil-inner exitError pattern, --wait polls the same source client list renders. But the probe has an environment-scoping hole that defeats its purpose in its primary consumer, and the --wait loop has no terminal state other than success.

Findings, most severe first

1. internal/cli/auth.go:285--check answers for the wrong environment, silently skipping the sign-in that would have fixed it.
The probe validates the stored CurrentEnv session (sessionEnv(cfg)), while login resolves its target from --env/$CLIENT_ENV (api.ResolveEnv). Reproduced empirically: prod session on the machine + installer running CLIENT_ENV=dev--check WhoAmIs prod, exits 0, prints nothing → the installer skips tracebloc login (the exact step that would have switched CurrentEnv to dev) → client create provisions into the prod account while the chart installs pointed at dev, and client status --wait polls a backend that will never see the client. There is no --env flag on auth status to correct it. Suggest: give --check login's env resolution (an --env flag defaulting to ResolveEnv("")), or exit 1 whenever ResolveEnv(target) != cfg.CurrentEnv.

2. client.go:665–687 — the --wait loop treats every failure as transient; terminal states burn the full timeout, then misdiagnose.
Two confirmed facets, one fix site:

  • Revoked/expired token → every poll 401s (measured: 6,254 polls against a hard-401 backend), full 120s stall, then "last state: unreachable… Check the client is running, then retry." — the client may be perfectly online; the credential is dead, and a 426's actionable "upgrade your CLI" message is swallowed.
  • Deleted active client → found=false forever, same full timeout, same wrong "unreachable" label — while the one-shot path fast-fails this exact state with the correct message (line 652).
    Suggest: inside the loop, treat *api.APIError 401/403, *api.UpgradeRequiredError, and persistent found=false as terminal.

3. internal/ui/ui.go:289 — the animated spinner writes raw \r\033[K + SGR straight to the writer; legacy Windows consoles render escape garbage every 120 ms.
fatih/color's own output path enables VT processing on Windows; Spinner.draw's direct Fprintf bypasses it, and install.ps1 makes the Windows path real. Suggest: route the redraw through the color-aware writer, or fall back to the static line on Windows without VT.

4. Design split — the same display-vs-probe problem is solved two different ways in one PR.
auth status grows a --check flag that flips a human display (always exit 0) into an exit-code contract — one forgotten flag from a script silently passing (auth.go:236). client status has no probe mode at all: one-shot exits 0 even for offline, and --timeout without --wait is accepted as a silent no-op (client.go:645). Suggest one consistent shape: --check semantics on both, and reject --timeout without --wait.

5. client.go:617--help leaks internal references.
The client status Long text ships "(RFC-0001 §8.5)" plus installer-implementation narration ("which today is inferred only from a local rollout check") to end users — the same class swept out of the rest of the CLI surface recently.

6. Failure copy names no tracebloc fix.
client.go:652: "…isn't in your account list — it may have been deleted" — reason + speculation, no next step. client.go:679: "Check the client is running, then retry." — the only obvious way a user "checks the client is running" is kubectl, which our copy must never steer toward. Point both at concrete verbs: tracebloc client list, tracebloc cluster doctor, or re-running the installer.

7. auth.go:278runAuthCheck re-implements authedClient() byte-for-byte (config.Load + SignedIn() + newAPIClient(sessionEnv(cfg)) + token assignment), duplicated only to discard the error message. Call the helper and drop its error.

8. client.go:700 — third hand-rolled copy of the active-client match rule (strconv.Itoa(c.ID) == active, after runClientList's marker loop and runClientUse's find loop) — worth a findClientByID helper. Rider: clientStatusPollInterval (line 631) is a dead second test seam — no test overrides it, and the stubbed pollAfter is already the injection point.

Coordination note (not a finding): this branch and open #142 touch adjacent lines of client.go (the AddCommand list vs the command Short two lines above) — whichever merges second will need a trivial conflict resolution.

Finding 1 is the blocker-grade one: it produces exactly the cross-env pollution RFC-0001's design works to prevent, and it fires the moment the installer adopts --check. Findings 2–3 are cheap; 4–8 are polish.

🤖 Review generated with Claude Code

Two spots swallowed HTTP 426 (UpgradeRequiredError, "CLI too old") as if it were
an ordinary failure; both now surface the upgrade instruction:

- `auth status --check`: a 426 from WhoAmI is surfaced non-silently (prints even
  without --verbose) instead of exiting 1 like a rejected token with "re-login"
  advice, which wouldn't help. (Bugbot #146-C)
- `client status --wait`: a 426 from the poll fails fast with the upgrade signal
  instead of retrying until timeout and reporting a generic "unreachable" wait
  timeout. (Bugbot #146-D)

Regression tests for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread internal/cli/client.go
Comment thread internal/cli/client.go Outdated
saadqbal and others added 2 commits July 7, 2026 13:42
…timeout (#138, Bugbot follow-up)

- --wait now fails fast when the active client isn't in the account (deleted /
  wrong account), matching the one-shot path, instead of polling to the timeout.
  (Bugbot #146-E)
- The timeout message surfaces the last real list error when every check failed,
  instead of a bare "(last state: unreachable)" that hid persistent API/network
  failures behind the missing-client wording. (Bugbot #146-F)

Loop restructured into a single switch (426 fail-fast / transient-remember /
missing / online); regression tests for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-wait + polish (#138)

- auth status --check now resolves its target env like login (--env, then
  $CLIENT_ENV, then prod) and requires it to match the signed-in CurrentEnv;
  otherwise it exits 1 so the installer runs the login that switches env. Fixes
  the cross-env hole where a stale prod session made --check pass while the
  installer targeted dev, provisioning into prod. Added an --env flag. The probe
  now reuses authedClient() instead of re-implementing it. (findings 1, 7)
- client status --wait treats 401/403 (revoked/expired token) as terminal —
  fail fast pointing at sign-in rather than polling to timeout. (finding 2;
  426 + missing-client were already terminal from the Bugbot round)
- The animated spinner falls back to the static line on Windows: its raw
  \r\033[K + SGR redraw bypasses fatih/color's Windows VT-enable path and would
  render as escape garbage on legacy consoles. (finding 3)
- Reject --timeout without --wait instead of silently ignoring it. (finding 4)
- client status --help/Long no longer leaks "(RFC-0001 §8.5)" or
  installer-implementation narration. (finding 5)
- Failure copy points at tracebloc verbs (`client list`, `cluster doctor`,
  re-run installer) instead of speculation / implicit kubectl. (finding 6)
- findClientByID helper replaces the hand-rolled active-client match in the two
  find-loops; clientStatusPollInterval is now a const (tests inject via
  pollAfter). (finding 8)

Deferred finding 4a (adding --check to client status) — the probe already exists
via --wait --timeout; not expanding the surface without a call. Regression tests
for the env mismatch, 401 fail-fast, and --timeout guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saadqbal

saadqbal commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Great review — #1 is a real cross-env trap, thanks for catching it. Pushed c4a7459 addressing all but 4a:

  • 1 / 7: --check now resolves its target env like login (added an --env flag; default $CLIENT_ENV → prod) and requires it to match the signed-in CurrentEnv — else exit 1 so the installer runs the login that switches env. Probe now reuses authedClient() instead of re-implementing it.
  • 2: --wait treats 401/403 as terminal (fail fast → sign in). 426 and missing-client were already terminal from the Bugbot round; the timeout message already surfaces the real error rather than "unreachable".
  • 3: animated spinner falls back to the static line on Windows (the raw \r\033[K+SGR redraw bypasses fatih/color's VT-enable path).
  • 4b: reject --timeout without --wait instead of silently ignoring it.
  • 5: dropped "(RFC-0001 §8.5)" + the installer-impl narration from --help.
  • 6: failure copy points at tracebloc client list / cluster doctor / re-run installer — no implicit kubectl.
  • 8: findClientByID helper replaces the hand-rolled match in the two find-loops; clientStatusPollInterval is now a const (tests inject via pollAfter).

Deferred 4a (adding --check to client status): the probe already exists via --wait --timeout, so I didn't want to expand the surface without your call — happy to if you'd prefer the symmetry.

Regression tests for the env mismatch, 401 fast-fail, and the --timeout guard. Full suite green under -race.

…gress-authcheck

# Conflicts:
#	internal/cli/auth.go
#	internal/cli/auth_test.go
Comment thread internal/cli/client.go
…138, Bugbot)

In client status --wait, a transient ListClients failure set lastErr, but a later
successful poll showing offline/pending never cleared it — so a timeout reported
"the last status check failed: <old error>" instead of the real last state. Add a
default switch case (successful poll, not yet online) that resets lastErr, so the
timeout message reflects what actually happened last. Regression test added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 96a28f3. Configure here.

Comment thread internal/cli/client.go
Comprehensive pass (independent review + Bugbot) to stop the one-at-a-time drip,
redesigning the --wait loop to be correct-by-construction:

- --timeout is now a real ceiling: the sleep is clamped to the remaining budget
  and the timeout is decided post-poll, so total runtime can't overshoot by a
  whole poll cycle. A poll begun within budget that returns online is still
  honored. (Bugbot "wait ignores elapsed timeout")
- defer sp.Stop() as a leak-proof net so no return path can leak the spinner
  goroutine; the online path still Stops explicitly before printing its ✔.
- Ctrl-C during --wait (and login's poll) exits quietly via a silent exit 130
  instead of printing "Error: context canceled".
- auth status --check --verbose distinguishes a rejected token (401/403 → run
  login) from an unreachable/erroring backend ("couldn't verify your session"),
  so an outage isn't mislabelled a bad credential.
- Timeout message uses a hoisted lastState so the last real state (offline/
  pending) survives the restructure; stale-error clearing preserved.

Deliberately kept the fail-fast set at 401/403 (+426): 429 and 5xx are retryable,
so "all 4xx terminal" would regress rate-limit handling. Regression tests for the
silent Ctrl-C and the verbose unreachable-vs-rejected copy; full suite green under
-race, errcheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saadqbal saadqbal requested a review from LukasWodka July 7, 2026 10:24
@saadqbal saadqbal merged commit 37f88a8 into develop Jul 7, 2026
16 checks 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