Skip to content

Commit fb148ea

Browse files
brycelelbachclaude
andcommitted
feat: show login status, org, and credential info in brev status
The hidden `brev status` command only printed the current workspace's name, ID, and machine type. Extend it to also report login state, credential provider (kas/auth0/service account/manual token), JWT issued/expires timestamps with humanized remaining duration, refresh token presence, and the active user and organization. Switch the command's wiring from loginCmdStore to noLoginCmdStore so that running `brev status` with an expired or missing token never triggers an interactive login prompt; remote user/org lookups instead fail gracefully and the local-only credential metadata is still shown. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a295d3a commit fb148ea

2 files changed

Lines changed: 234 additions & 21 deletions

File tree

pkg/cmd/cmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ func createCmdTree(cmd *cobra.Command, t *terminal.Terminal, loginCmdStore *stor
331331
cmd.AddCommand(ollama.NewCmdOllama(t, loginCmdStore))
332332
cmd.AddCommand(agentskill.NewCmdAgentSkill(t, noLoginCmdStore))
333333
cmd.AddCommand(background.NewCmdBackground(t, loginCmdStore))
334-
cmd.AddCommand(status.NewCmdStatus(t, loginCmdStore))
334+
cmd.AddCommand(status.NewCmdStatus(t, noLoginCmdStore))
335335
cmd.AddCommand(sshkeys.NewCmdSSHKeys(t, loginCmdStore))
336336
cmd.AddCommand(start.NewCmdStart(t, loginCmdStore, noLoginCmdStore))
337337
cmd.AddCommand(stop.NewCmdStop(t, loginCmdStore, noLoginCmdStore))

pkg/cmd/status/status.go

Lines changed: 233 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,47 @@
1+
// Package status implements the `brev status` command, which reports the
2+
// caller's login state, current organization, and credential metadata.
13
package status
24

35
import (
6+
"errors"
7+
"fmt"
8+
"strings"
9+
"time"
10+
11+
"github.com/brevdev/brev-cli/pkg/auth"
412
"github.com/brevdev/brev-cli/pkg/cmd/util"
13+
"github.com/brevdev/brev-cli/pkg/config"
514
"github.com/brevdev/brev-cli/pkg/entity"
6-
"github.com/brevdev/brev-cli/pkg/store"
15+
breverrors "github.com/brevdev/brev-cli/pkg/errors"
716
"github.com/brevdev/brev-cli/pkg/terminal"
17+
"github.com/golang-jwt/jwt/v5"
818
"github.com/spf13/cobra"
919
)
1020

11-
var (
12-
createLong = "Create a new Brev machine"
13-
createExample = `
14-
brev create <name>
15-
`
16-
// instanceTypes = []string{"p4d.24xlarge", "p3.2xlarge", "p3.8xlarge", "p3.16xlarge", "p3dn.24xlarge", "p2.xlarge", "p2.8xlarge", "p2.16xlarge", "g5.xlarge", "g5.2xlarge", "g5.4xlarge", "g5.8xlarge", "g5.16xlarge", "g5.12xlarge", "g5.24xlarge", "g5.48xlarge", "g5g.xlarge", "g5g.2xlarge", "g5g.4xlarge", "g5g.8xlarge", "g5g.16xlarge", "g5g.metal", "g4dn.xlarge", "g4dn.2xlarge", "g4dn.4xlarge", "g4dn.8xlarge", "g4dn.16xlarge", "g4dn.12xlarge", "g4dn.metal", "g4ad.xlarge", "g4ad.2xlarge", "g4ad.4xlarge", "g4ad.8xlarge", "g4ad.16xlarge", "g3s.xlarge", "g3.4xlarge", "g3.8xlarge", "g3.16xlarge"}
21+
const (
22+
statusLong = "Show your Brev login status, current organization, and credential metadata."
23+
statusExample = " brev status"
24+
25+
// auth0Issuer mirrors the hard-coded issuer used in auth.StandardLogin.
26+
auth0Issuer = "https://brevdev.us.auth0.com/"
1727
)
1828

1929
type StatusStore interface {
20-
util.GetWorkspaceByNameOrIDErrStore
30+
GetAuthTokens() (*entity.AuthTokens, error)
2131
GetActiveOrganizationOrDefault() (*entity.Organization, error)
2232
GetCurrentUser() (*entity.User, error)
23-
GetWorkspace(workspaceID string) (*entity.Workspace, error)
2433
GetCurrentWorkspaceID() (string, error)
25-
CreateWorkspace(organizationID string, options *store.CreateWorkspacesOptions) (*entity.Workspace, error)
34+
GetWorkspace(workspaceID string) (*entity.Workspace, error)
2635
}
2736

2837
func NewCmdStatus(t *terminal.Terminal, statusStore StatusStore) *cobra.Command {
2938
cmd := &cobra.Command{
30-
Annotations: map[string]string{"hidden": ""},
39+
Annotations: map[string]string{"configuration": ""},
3140
Use: "status",
3241
DisableFlagsInUseLine: true,
33-
Short: "Show instance status",
34-
Long: createLong,
35-
Example: createExample,
42+
Short: "Show login status, organization, and credential info",
43+
Long: statusLong,
44+
Example: statusExample,
3645
RunE: func(cmd *cobra.Command, args []string) error {
3746
runShowStatus(t, statusStore)
3847
return nil
@@ -44,18 +53,222 @@ func NewCmdStatus(t *terminal.Terminal, statusStore StatusStore) *cobra.Command
4453
func runShowStatus(t *terminal.Terminal, statusStore StatusStore) {
4554
terminal.DisplayBrevLogo(t)
4655
t.Vprintf("\n")
47-
wsID, err := statusStore.GetCurrentWorkspaceID()
56+
57+
tokens, loggedIn := showAuthStatus(t, statusStore)
58+
if loggedIn {
59+
showUserAndOrg(t, statusStore, tokens)
60+
}
61+
showWorkspaceStatus(t, statusStore)
62+
}
63+
64+
// showAuthStatus prints the login section. Returns the loaded tokens (may be nil)
65+
// and whether the user is logged in.
66+
func showAuthStatus(t *terminal.Terminal, statusStore StatusStore) (*entity.AuthTokens, bool) {
67+
tokens, err := statusStore.GetAuthTokens()
4868
if err != nil {
49-
t.Vprintf("\n Error: %s", t.Red(err.Error()))
69+
var notFound *breverrors.CredentialsFileNotFound
70+
if errors.As(err, &notFound) {
71+
printLoggedOut(t)
72+
return nil, false
73+
}
74+
t.Vprintf("Status: %s (%s)\n", t.Red("unknown"), err.Error())
75+
return nil, false
76+
}
77+
if tokens == nil || (tokens.AccessToken == "" && tokens.RefreshToken == "") {
78+
printLoggedOut(t)
79+
return nil, false
80+
}
81+
82+
t.Vprintf("Status: %s\n", t.Green("Logged in"))
83+
84+
provider, providerDetail := describeCredential(tokens)
85+
if providerDetail != "" {
86+
t.Vprintf("Provider: %s (%s)\n", t.Yellow(provider), providerDetail)
87+
} else {
88+
t.Vprintf("Provider: %s\n", t.Yellow(provider))
89+
}
90+
91+
issuedAt, expiresAt, gotClaims := tokenIssuedAndExpiry(tokens.AccessToken)
92+
if gotClaims {
93+
if !issuedAt.IsZero() {
94+
t.Vprintf("Issued at: %s\n", t.Yellow(issuedAt.Local().Format(time.RFC3339)))
95+
}
96+
if !expiresAt.IsZero() {
97+
remaining := time.Until(expiresAt)
98+
if remaining > 0 {
99+
t.Vprintf("Expires at: %s (%s remaining)\n",
100+
t.Yellow(expiresAt.Local().Format(time.RFC3339)),
101+
t.Yellow(formatDuration(remaining)))
102+
} else {
103+
t.Vprintf("Expires at: %s (%s; refresh token will be used on next call)\n",
104+
t.Yellow(expiresAt.Local().Format(time.RFC3339)),
105+
t.Red("expired"))
106+
}
107+
}
108+
} else if tokens.AccessToken != "" {
109+
t.Vprintf("Token: %s\n", t.Yellow("opaque (no expiration metadata)"))
110+
}
111+
112+
if tokens.RefreshToken != "" && tokens.RefreshToken != "auto-login" {
113+
t.Vprintf("Refresh: %s\n", t.Yellow("present"))
114+
} else {
115+
t.Vprintf("Refresh: %s\n", t.Yellow("absent"))
116+
}
117+
118+
return tokens, true
119+
}
120+
121+
func showUserAndOrg(t *terminal.Terminal, statusStore StatusStore, tokens *entity.AuthTokens) {
122+
tokenEmail := ""
123+
if tokens != nil {
124+
tokenEmail = auth.GetEmailFromToken(tokens.AccessToken)
125+
}
126+
127+
user, userErr := statusStore.GetCurrentUser()
128+
if userErr == nil && user != nil {
129+
t.Vprintf("\nUser: %s\n", t.Yellow(coalesce(user.Name, user.Username, user.Email)))
130+
t.Vprintf("\tID: %s\n", t.Yellow(user.ID))
131+
if user.Email != "" {
132+
t.Vprintf("\tEmail: %s\n", t.Yellow(user.Email))
133+
}
134+
if user.Username != "" && user.Username != user.Name {
135+
t.Vprintf("\tUsername: %s\n", t.Yellow(user.Username))
136+
}
137+
} else {
138+
if tokenEmail != "" {
139+
t.Vprintf("\nUser: %s\n", t.Yellow(tokenEmail))
140+
}
141+
if userErr != nil {
142+
t.Vprintf("\t(remote user lookup failed: %s)\n", t.Red(rootErrorMessage(userErr)))
143+
}
144+
}
145+
146+
org, orgErr := statusStore.GetActiveOrganizationOrDefault()
147+
if orgErr == nil && org != nil {
148+
t.Vprintf("\nOrg: %s\n", t.Yellow(org.Name))
149+
t.Vprintf("\tID: %s\n", t.Yellow(org.ID))
150+
} else if orgErr != nil {
151+
t.Vprintf("\nOrg: %s\n", t.Red("unknown"))
152+
t.Vprintf("\t(remote org lookup failed: %s)\n", t.Red(rootErrorMessage(orgErr)))
153+
} else {
154+
t.Vprintf("\nOrg: %s\n", t.Yellow("none set"))
155+
}
156+
}
157+
158+
func showWorkspaceStatus(t *terminal.Terminal, statusStore StatusStore) {
159+
wsID, err := statusStore.GetCurrentWorkspaceID()
160+
if err != nil || wsID == "" {
50161
return
51162
}
52163
ws, err := statusStore.GetWorkspace(wsID)
53164
if err != nil {
54-
t.Vprintf("\n Error: %s", t.Red(err.Error()))
165+
t.Vprintf("\nInstance lookup failed: %s\n", t.Red(rootErrorMessage(err)))
55166
return
56167
}
168+
t.Vprintf("\nInstance: %s\n", t.Yellow(ws.Name))
169+
t.Vprintf("\tID: %s\n", t.Yellow(ws.ID))
170+
t.Vprintf("\tMachine: %s\n", t.Yellow(util.GetInstanceString(*ws)))
171+
}
172+
173+
// rootErrorMessage returns a single-line message from the deepest wrapped
174+
// error, stripping the file/line trace that breverrors.WrapAndTrace prepends.
175+
func rootErrorMessage(err error) string {
176+
if err == nil {
177+
return ""
178+
}
179+
root := breverrors.Root(err)
180+
if root == nil {
181+
return strings.TrimSpace(err.Error())
182+
}
183+
return strings.TrimSpace(root.Error())
184+
}
185+
186+
func printLoggedOut(t *terminal.Terminal) {
187+
t.Vprintf("Status: %s\n", t.Red("Logged out"))
188+
t.Vprintf("Run %s to log in.\n", t.Yellow("brev login"))
189+
}
190+
191+
// describeCredential infers the credential provider from the stored tokens.
192+
// Returns (provider, optional detail string).
193+
func describeCredential(tokens *entity.AuthTokens) (string, string) {
194+
if tokens.AccessToken == "" {
195+
return "unknown", ""
196+
}
197+
// FileStore.GetAuthTokens returns the Kubernetes service-account token
198+
// with an empty refresh token when running inside a Brev workspace pod.
199+
if tokens.RefreshToken == "" {
200+
return "service account", "Kubernetes pod token"
201+
}
202+
if tokens.RefreshToken == "auto-login" {
203+
return "manual access token", "set via `brev login --token`"
204+
}
205+
if tokens.AccessToken == "auto-login" {
206+
return "manual refresh token", "set via `brev login --token`"
207+
}
208+
if auth.IssuerCheck(tokens.AccessToken, auth0Issuer) {
209+
return "auth0", auth0Issuer
210+
}
211+
if kasIssuer := config.GlobalConfig.GetBrevAuthIssuerURL(); kasIssuer != "" && auth.IssuerCheck(tokens.AccessToken, kasIssuer) {
212+
return "kas (NVIDIA NGC)", kasIssuer
213+
}
214+
if iss := tokenIssuer(tokens.AccessToken); iss != "" {
215+
return "unknown", iss
216+
}
217+
return "unknown", ""
218+
}
219+
220+
func tokenIssuer(token string) string {
221+
parser := jwt.Parser{}
222+
claims := jwt.MapClaims{}
223+
_, _, err := parser.ParseUnverified(token, &claims)
224+
if err != nil {
225+
return ""
226+
}
227+
iss, _ := claims["iss"].(string)
228+
return iss
229+
}
57230

58-
t.Vprintf("\nYou're on instance %s", t.Yellow(ws.Name))
59-
t.Vprintf("\n\tID: %s", t.Yellow(ws.ID))
60-
t.Vprintf("\n\tMachine: %s", t.Yellow(util.GetInstanceString(*ws)))
231+
// tokenIssuedAndExpiry parses a JWT and returns iat and exp times. ok=false if
232+
// the token isn't a JWT or has neither claim.
233+
func tokenIssuedAndExpiry(token string) (issuedAt, expiresAt time.Time, ok bool) {
234+
parser := jwt.Parser{}
235+
claims := jwt.MapClaims{}
236+
_, _, err := parser.ParseUnverified(token, &claims)
237+
if err != nil {
238+
return time.Time{}, time.Time{}, false
239+
}
240+
if exp, e := claims.GetExpirationTime(); e == nil && exp != nil {
241+
expiresAt = exp.Time
242+
}
243+
if iat, e := claims.GetIssuedAt(); e == nil && iat != nil {
244+
issuedAt = iat.Time
245+
}
246+
return issuedAt, expiresAt, !expiresAt.IsZero() || !issuedAt.IsZero()
247+
}
248+
249+
func formatDuration(d time.Duration) string {
250+
if d < 0 {
251+
d = -d
252+
}
253+
switch {
254+
case d < time.Minute:
255+
return fmt.Sprintf("%ds", int(d.Seconds()))
256+
case d < time.Hour:
257+
return fmt.Sprintf("%dm %ds", int(d.Minutes()), int(d.Seconds())%60)
258+
case d < 24*time.Hour:
259+
return fmt.Sprintf("%dh %dm", int(d.Hours()), int(d.Minutes())%60)
260+
default:
261+
days := int(d.Hours()) / 24
262+
hours := int(d.Hours()) % 24
263+
return fmt.Sprintf("%dd %dh", days, hours)
264+
}
265+
}
266+
267+
func coalesce(values ...string) string {
268+
for _, v := range values {
269+
if v != "" {
270+
return v
271+
}
272+
}
273+
return ""
61274
}

0 commit comments

Comments
 (0)