diff --git a/internal/api/client.go b/internal/api/client.go index 9127b07..938c1be 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -5,13 +5,30 @@ import ( "crypto/tls" "fmt" "net/http" + "sync" "time" notteErrors "github.com/nottelabs/notte-cli/internal/errors" + "github.com/nottelabs/notte-cli/internal/update" ) const DefaultBaseURL = "https://api.notte.cc" +// MinCLIVersionHeader is the response header the API uses to advertise the +// minimum CLI version required for full compatibility. +const MinCLIVersionHeader = "x-notte-min-cli-version" + +// detectedMinVersion stores the minimum CLI version returned by the API. +// Set once by checkMinVersion; read by root.go to trigger the upgrade prompt. +var detectedMinVersion string +var detectOnce sync.Once + +// DetectedMinVersion returns the API-required minimum CLI version, or "" if +// the header was not seen or the CLI is already up to date. +func DetectedMinVersion() string { + return detectedMinVersion +} + // NotteClient wraps the generated client with auth and resilience type NotteClient struct { client *ClientWithResponses @@ -142,9 +159,29 @@ func (t *resilientTransport) RoundTrip(req *http.Request) (*http.Response, error t.circuitBreaker.RecordSuccess() } + // Check if the API requires a newer CLI version + t.checkMinVersion(resp) + return resp, nil } +// checkMinVersion inspects the x-notte-min-cli-version response header and +// stores the required version so the root command can prompt for upgrade. +func (t *resilientTransport) checkMinVersion(resp *http.Response) { + minVersion := resp.Header.Get(MinCLIVersionHeader) + if minVersion == "" || t.version == "" || t.version == "dev" { + return + } + + detectOnce.Do(func() { + outdated, err := update.IsNewer(t.version, minVersion) + if err != nil || !outdated { + return + } + detectedMinVersion = minVersion + }) +} + func (t *resilientTransport) doWithRetry(req *http.Request) (*http.Response, error) { var resp *http.Response var err error diff --git a/internal/api/client_test.go b/internal/api/client_test.go index 121ecc7..afc1e8d 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "strings" + "sync" "testing" "time" ) @@ -389,6 +390,69 @@ func TestNotteClient_APIKey(t *testing.T) { } } +func resetDetectOnce() { + detectOnce = sync.Once{} + detectedMinVersion = "" +} + +func TestCheckMinVersion_StoresWhenOutdated(t *testing.T) { + resetDetectOnce() + t.Cleanup(resetDetectOnce) + + rt := &resilientTransport{version: "0.0.10"} + resp := &http.Response{ + Header: http.Header{"X-Notte-Min-Cli-Version": []string{"0.0.12"}}, + } + rt.checkMinVersion(resp) + + if got := DetectedMinVersion(); got != "0.0.12" { + t.Errorf("DetectedMinVersion() = %q, want %q", got, "0.0.12") + } +} + +func TestCheckMinVersion_EmptyWhenCurrent(t *testing.T) { + resetDetectOnce() + t.Cleanup(resetDetectOnce) + + rt := &resilientTransport{version: "0.0.12"} + resp := &http.Response{ + Header: http.Header{"X-Notte-Min-Cli-Version": []string{"0.0.12"}}, + } + rt.checkMinVersion(resp) + + if got := DetectedMinVersion(); got != "" { + t.Errorf("DetectedMinVersion() = %q, want empty", got) + } +} + +func TestCheckMinVersion_EmptyWhenNoHeader(t *testing.T) { + resetDetectOnce() + t.Cleanup(resetDetectOnce) + + rt := &resilientTransport{version: "0.0.10"} + resp := &http.Response{Header: http.Header{}} + rt.checkMinVersion(resp) + + if got := DetectedMinVersion(); got != "" { + t.Errorf("DetectedMinVersion() = %q, want empty", got) + } +} + +func TestCheckMinVersion_EmptyForDevVersion(t *testing.T) { + resetDetectOnce() + t.Cleanup(resetDetectOnce) + + rt := &resilientTransport{version: "dev"} + resp := &http.Response{ + Header: http.Header{"X-Notte-Min-Cli-Version": []string{"0.0.12"}}, + } + rt.checkMinVersion(resp) + + if got := DetectedMinVersion(); got != "" { + t.Errorf("DetectedMinVersion() = %q, want empty", got) + } +} + func TestDefaultContext(t *testing.T) { ctx := DefaultContext() if ctx == nil { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index f34b081..37ee2fa 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -54,8 +54,15 @@ func Execute() { err := rootCmd.Execute() - // Show update notification after command output - if checker != nil { + // If the API told us our CLI is too old, prompt to upgrade (takes priority) + if minVer := api.DetectedMinVersion(); minVer != "" { + update.PrintUpdateNotification(&update.Result{ + CurrentVersion: Version, + LatestVersion: minVer, + UpdateAvailable: true, + }, os.Stderr, os.Stdin, yesFlag, IsJSONOutput(), noColor) + } else if checker != nil { + // Otherwise fall back to the GitHub-based update check if result := checker.GetResult(); result != nil { update.PrintUpdateNotification(result, os.Stderr, os.Stdin, yesFlag, IsJSONOutput(), noColor) }