feat(cli): login progress spinner, auth status --check, client status --wait (#138)#146
Conversation
… --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>
|
👋 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.) |
LukasWodka
left a comment
There was a problem hiding this comment.
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=falseforever, 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.APIError401/403,*api.UpgradeRequiredError, and persistentfound=falseas 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:278 — runAuthCheck 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>
…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>
|
Great review — #1 is a real cross-env trap, thanks for catching it. Pushed c4a7459 addressing all but 4a:
Deferred 4a (adding Regression tests for the env mismatch, 401 fast-fail, and the |
…gress-authcheck # Conflicts: # internal/cli/auth.go # internal/cli/auth_test.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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
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>

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 displaypollForToken, wrapped in a liveui.Spinner(braille frame +mm:sselapsed +(Ctrl-C to cancel)).--plain/NO_COLORoutput prints one static line — no\r, no ANSI. The spinner line clears on exit so the✔prints in its place.open:/code:→Open/Enter(imperativeui.Actionrows).✔ Signed in as ….2.
auth status --checkMachine-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-innerexitError, somainprints nothing);--verbosenarrates 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).
--waitpolls the same sourceclient listrenders 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 localkubectl rolloutcheck, which can't tell whether tracebloc can actually see the client.Acceptance (all covered by tests)
loginon a TTY shows spinner + elapsed;--plain/piped stays static (no raw ANSI) — verified by a static-path test + a race-free directdrawtest of themm:ssformat.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) +Actionrows. Polling reuses the existingpollAfterseam plus a newclientStatusPollIntervalvar so tests drive many iterations with no real delay.Local: full
go test ./... -racegreen;go vet,gofmt,errcheck,goimportsclean. Built the binary and smoke-tested--checkexit codes + help wiring.Coordination notes
develop(1de77d2, includes feat(cli): auto-discover the client's namespace, rewrite the first-touch surface, ship atbalias #142). Sibling PR feat(cli): auto-name clients<firstname>-NN+ make--locationoptional (#137) #144 (cli#137) also editsauth.go'srunLogin(a one-lineprof.FirstNameadd in theWhoAmIblock); expect a trivial conflict there — whichever merges second keeps both theFirstNameline and this PR's refactor.🤖 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 loginpulls device-token polling intopollForToken, shown behind aui.Spinner(animated on TTY, one static line when piped/--plain/NO_COLOR). Sign-in steps use boldOpen/EnterActionrows instead of dimopen:/code:fields. After approval, success is✔ Signed in as …; “token saved…” moves to verbose-onlyDetailf. 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 andWhoAmIsucceeds; 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 statusreports the active client’s backend state;--waitpolls 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.--timeoutwithout--waitis rejected.internal/ui: newActionandSpinner(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.