From b6fa20784d8afc8e301846abf54a1bc377e808de Mon Sep 17 00:00:00 2001 From: DTTerastar Date: Sat, 25 Apr 2026 18:00:29 -0400 Subject: [PATCH] feat: add 'auth status' subcommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per quantcli shared contract §5, every CLI exposes 'auth status' printing a one-line readiness summary, exit 0 if usable. https://github.com/quantcli/common/blob/main/CONTRACT.md#5-auth liftoff-export auth status → "logged in (token expires 2026-04-25T20:37:21-04:00)" / exit 0 → "not logged in — run: liftoff-export auth login" / exit 1 → "token expired YYYY-MM-DDTHH:MM:SS-04:00 — run: ... auth refresh" / exit 1 Local check only — no network call, no refresh attempted. Adds an exported auth.Load() so cmd/ can read the token store without going through GetToken's auto-refresh path. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/auth.go | 28 ++++++++++++++++++++++++++++ internal/auth/auth.go | 7 +++++++ 2 files changed, 35 insertions(+) diff --git a/cmd/auth.go b/cmd/auth.go index b86c56a..a62f9a3 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strings" + "time" "github.com/quantcli/liftoff-export-cli/internal/auth" "github.com/spf13/cobra" @@ -67,8 +68,35 @@ var logoutCmd = &cobra.Command{ }, } +var statusCmd = &cobra.Command{ + Use: "status", + Short: "Print one-line auth readiness state and exit 0 if usable", + Long: `Print a one-line summary of whether the CLI has a usable token. Exit 0 +if a saved token is present and not yet expired, 1 otherwise. + +This is a local check — no network call and no refresh is attempted, even +when the saved token is expired. Use 'auth refresh' (or any export +subcommand) to actually refresh. + +Per the quantcli shared contract: +https://github.com/quantcli/common/blob/main/CONTRACT.md#5-auth`, + RunE: func(cmd *cobra.Command, args []string) error { + store, err := auth.Load() + if err != nil { + return fmt.Errorf("not logged in — run: liftoff-export auth login") + } + exp := store.ExpiresAt.Local().Format(time.RFC3339) + if time.Now().After(store.ExpiresAt) { + return fmt.Errorf("token expired %s — run: liftoff-export auth refresh", exp) + } + fmt.Fprintf(cmd.OutOrStdout(), "logged in (token expires %s)\n", exp) + return nil + }, +} + func init() { authCmd.AddCommand(loginCmd) authCmd.AddCommand(logoutCmd) authCmd.AddCommand(refreshCmd) + authCmd.AddCommand(statusCmd) } diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 2845d67..946f98b 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -245,6 +245,13 @@ func load() (*TokenStore, error) { return &store, json.Unmarshal(data, &store) } +// Load reads the saved token store without attempting a refresh. Returns +// (nil, error) when no token has been saved yet. Suitable for non-mutating +// status checks; use GetToken when an action needs a usable access token. +func Load() (*TokenStore, error) { + return load() +} + // parseBearer extracts "Bearer " from an Authorization header value. func parseBearer(header string) string { if strings.HasPrefix(header, "Bearer ") {