Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions internal/api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
)
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 9 additions & 2 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading