@@ -133,7 +133,8 @@ func RefreshIndex(ctx context.Context) (*CachedIndex, error) {
133133 }
134134
135135 // Attach GitHub token if available.
136- if token := githubToken (); token != "" {
136+ token , tokenSource := githubTokenWithSource ()
137+ if token != "" {
137138 req .Header .Set ("Authorization" , "Bearer " + token )
138139 }
139140 req .Header .Set ("User-Agent" , "datumctl-plugin-index" )
@@ -145,7 +146,12 @@ func RefreshIndex(ctx context.Context) (*CachedIndex, error) {
145146 defer resp .Body .Close ()
146147
147148 if resp .StatusCode != http .StatusOK {
148- return degradedFallback (fmt .Errorf ("fetch plugin index: HTTP %s" , resp .Status ))
149+ return degradedFallback (& IndexFetchError {
150+ URL : IndexURL ,
151+ StatusCode : resp .StatusCode ,
152+ Status : resp .Status ,
153+ TokenSource : tokenSource ,
154+ })
149155 }
150156
151157 raw , err := io .ReadAll (resp .Body )
@@ -202,10 +208,45 @@ func degradedFallback(origErr error) (*CachedIndex, error) {
202208 return cached , origErr
203209}
204210
205- // githubToken returns a GitHub personal access token from the environment.
206- func githubToken () string {
211+ // githubTokenWithSource returns a GitHub personal access token from the
212+ // environment along with the name of the variable it came from (empty when no
213+ // token is set).
214+ func githubTokenWithSource () (token , source string ) {
207215 if t := os .Getenv ("DATUMCTL_GITHUB_TOKEN" ); t != "" {
208- return t
216+ return t , "DATUMCTL_GITHUB_TOKEN"
209217 }
210- return os .Getenv ("GITHUB_TOKEN" )
218+ if t := os .Getenv ("GITHUB_TOKEN" ); t != "" {
219+ return t , "GITHUB_TOKEN"
220+ }
221+ return "" , ""
222+ }
223+
224+ // IndexFetchError is returned by RefreshIndex when the index host responds with
225+ // a non-OK HTTP status. It carries enough context for the command layer to
226+ // render actionable guidance via Hint.
227+ type IndexFetchError struct {
228+ URL string
229+ StatusCode int
230+ Status string // HTTP status text, e.g. "404 Not Found"
231+ TokenSource string // env var the Authorization token came from, "" if none
232+ }
233+
234+ func (e * IndexFetchError ) Error () string {
235+ return fmt .Sprintf ("the plugin index host returned HTTP %s" , e .Status )
236+ }
237+
238+ // Hint returns actionable guidance for resolving the failure, or "" when none
239+ // applies. The common case: a GitHub token in the environment is sent to the
240+ // public index host, which rejects it with a 401/403/404.
241+ func (e * IndexFetchError ) Hint () string {
242+ switch e .StatusCode {
243+ case http .StatusUnauthorized , http .StatusForbidden , http .StatusNotFound :
244+ if e .TokenSource != "" {
245+ return fmt .Sprintf (
246+ "A GitHub token from $%s is being sent to the index host, which is the likely cause. " +
247+ "The public plugin index needs no authentication; unset that variable and retry." ,
248+ e .TokenSource )
249+ }
250+ }
251+ return ""
211252}
0 commit comments