Skip to content

Commit 4fcfca0

Browse files
authored
Errors are all handled now (#48)
1 parent 3ba3dac commit 4fcfca0

32 files changed

Lines changed: 786 additions & 670 deletions

.goreleaser.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ builds:
1313
flags: [-trimpath]
1414
ldflags:
1515
- -s -w
16-
- -X github.com/major-technology/cli/cmd.version={{.Version}}
16+
- -X github.com/major-technology/cli/cmd.Version={{.Version}}
1717
- -X github.com/major-technology/cli/cmd.configFile=configs/prod.json
1818
goos: [darwin, linux]
1919
goarch: [amd64, arm64]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ brew install major-technology/tap/major
1616

1717
### Direct Install
1818
```bash
19-
curl -fsSL https://raw.githubusercontent.com/major-technology/cli/main/install.sh | bash
19+
curl -fsSL https://install.major.build | bash
2020
```
2121

2222
### Updating

clients/api/client.go

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
mjrToken "github.com/major-technology/cli/clients/token"
12+
clierrors "github.com/major-technology/cli/errors"
1213
"github.com/pkg/errors"
1314
)
1415

@@ -46,7 +47,8 @@ func (c *Client) doRequestInternal(method, path string, body interface{}, respon
4647
// Get token from keyring for this request
4748
t, err := mjrToken.GetToken()
4849
if err != nil {
49-
return &NoTokenError{OriginalError: err}
50+
// User is not logged in - return appropriate CLIError
51+
return clierrors.ErrorNotLoggedIn
5052
}
5153
token = t
5254
}
@@ -84,20 +86,19 @@ func (c *Client) doRequestInternal(method, path string, body interface{}, respon
8486

8587
// Handle error responses
8688
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
87-
var errResp ErrorResponse
89+
var errResp *ErrorResponse
8890
if err := json.Unmarshal(respBody, &errResp); err == nil && errResp.Error != nil {
89-
return &APIError{
90-
StatusCode: errResp.Error.StatusCode,
91-
InternalCode: errResp.Error.InternalCode,
92-
Message: errResp.Error.ErrorString,
93-
ErrorType: errResp.Error.ErrorString,
94-
}
91+
return ToCLIError(errResp)
9592
}
9693
// Fallback for unexpected error format
97-
return &APIError{
98-
StatusCode: resp.StatusCode,
99-
Message: string(respBody),
94+
errResp = &ErrorResponse{
95+
Error: &AppErrorDetail{
96+
InternalCode: 9999,
97+
ErrorString: string(respBody),
98+
StatusCode: resp.StatusCode,
99+
},
100100
}
101+
return ToCLIError(errResp)
101102
}
102103

103104
// Parse successful response if a response struct is provided
@@ -123,22 +124,12 @@ func (c *Client) StartLogin() (*LoginStartResponse, error) {
123124
}
124125

125126
// PollLogin polls the login endpoint to check if the user has authorized the device
126-
// Returns the response and error. For authorization pending state, returns a specific error.
127127
func (c *Client) PollLogin(deviceCode string) (*LoginPollResponse, error) {
128128
req := LoginPollRequest{DeviceCode: deviceCode}
129129

130130
var resp LoginPollResponse
131131
err := c.doRequestWithoutAuth("POST", "/login/poll", req, &resp)
132132
if err != nil {
133-
// Check if authorization is pending (expected error state)
134-
if IsAuthorizationPending(err) {
135-
return nil, err // Return the error so caller can check with IsAuthorizationPending
136-
}
137-
// Check if it's an invalid device code
138-
if IsInvalidDeviceCode(err) {
139-
return nil, fmt.Errorf("invalid or expired device code")
140-
}
141-
// Other errors
142133
return nil, err
143134
}
144135

@@ -150,14 +141,11 @@ func (c *Client) VerifyToken() (*VerifyTokenResponse, error) {
150141
var resp VerifyTokenResponse
151142
err := c.doRequest("GET", "/verify", nil, &resp)
152143
if err != nil {
153-
if IsUnauthorized(err) {
154-
return nil, fmt.Errorf("invalid or expired token - please login again")
155-
}
156144
return nil, err
157145
}
158146

159147
if !resp.Active {
160-
return nil, fmt.Errorf("token is not active - please login again")
148+
return nil, clierrors.ErrorTokenNotActive
161149
}
162150

163151
return &resp, nil
@@ -363,6 +351,7 @@ func (c *Client) SetApplicationTemplate(applicationID, templateID string) (*SetA
363351
// CheckVersion checks if the CLI version is up to date
364352
func (c *Client) CheckVersion(currentVersion string) (*CheckVersionResponse, error) {
365353
req := VersionCheckRequest{Version: currentVersion}
354+
366355
var resp CheckVersionResponse
367356
err := c.doRequestWithoutAuth("POST", "/version/check", req, &resp)
368357
if err != nil {

clients/api/errors.go

Lines changed: 36 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
package api
22

33
import (
4-
"errors"
54
"fmt"
6-
"net/http"
75

8-
"github.com/major-technology/cli/ui"
9-
"github.com/spf13/cobra"
6+
clierrors "github.com/major-technology/cli/errors"
107
)
118

129
// Error code constants from @repo/errors
@@ -47,145 +44,45 @@ type ErrorResponse struct {
4744
Error *AppErrorDetail `json:"error,omitempty"`
4845
}
4946

50-
// APIError represents an API error with status code and message
51-
type APIError struct {
52-
StatusCode int
53-
InternalCode int // Internal error code from the API
54-
Message string
55-
ErrorType string
56-
}
57-
58-
func (e *APIError) Error() string {
59-
if e.ErrorType != "" {
60-
return fmt.Sprintf("API error (status %d): %s - %s", e.StatusCode, e.ErrorType, e.Message)
61-
}
62-
return fmt.Sprintf("API error (status %d): %s", e.StatusCode, e.Message)
63-
}
64-
65-
// NoTokenError represents an error when no token is available
66-
type NoTokenError struct {
67-
OriginalError error
68-
}
69-
70-
func (e *NoTokenError) Error() string {
71-
return fmt.Sprintf("not logged in: %v", e.OriginalError)
72-
}
73-
74-
// ForceUpgradeError represents an error when the CLI version is too old and must be upgraded
75-
type ForceUpgradeError struct {
76-
LatestVersion string
77-
}
78-
79-
func (e *ForceUpgradeError) Error() string {
80-
return "CLI version is out of date and must be upgraded"
81-
}
82-
83-
// IsUnauthorized checks if the error is an unauthorized error
84-
func IsUnauthorized(err error) bool {
85-
var apiErr *APIError
86-
if errors.As(err, &apiErr) {
87-
return apiErr.StatusCode == http.StatusUnauthorized
88-
}
89-
return false
90-
}
91-
92-
// IsNotFound checks if the error is a not found error
93-
func IsNotFound(err error) bool {
94-
var apiErr *APIError
95-
if errors.As(err, &apiErr) {
96-
return apiErr.StatusCode == http.StatusNotFound
97-
}
98-
return false
99-
}
100-
101-
// IsBadRequest checks if the error is a bad request error
102-
func IsBadRequest(err error) bool {
103-
var apiErr *APIError
104-
if errors.As(err, &apiErr) {
105-
return apiErr.StatusCode == http.StatusBadRequest
106-
}
107-
return false
108-
}
109-
110-
// IsNoToken checks if the error is a no token error
111-
func IsNoToken(err error) bool {
112-
var noTokenErr *NoTokenError
113-
return errors.As(err, &noTokenErr)
114-
}
115-
116-
// HasErrorCode checks if the error has a specific internal error code
117-
func HasErrorCode(err error, code int) bool {
118-
var apiErr *APIError
119-
if errors.As(err, &apiErr) {
120-
return apiErr.InternalCode == code
121-
}
122-
return false
123-
}
124-
125-
// IsAuthorizationPending checks if the error is an authorization pending error
126-
func IsAuthorizationPending(err error) bool {
127-
return HasErrorCode(err, ErrorCodeAuthorizationPending)
128-
}
129-
130-
// IsInvalidDeviceCode checks if the error is an invalid device code error
131-
func IsInvalidDeviceCode(err error) bool {
132-
return HasErrorCode(err, ErrorCodeInvalidDeviceCode)
133-
}
134-
135-
// GetErrorCode returns the internal error code from an error, or 0 if not an APIError
136-
func GetErrorCode(err error) int {
137-
var apiErr *APIError
138-
if errors.As(err, &apiErr) {
139-
return apiErr.InternalCode
140-
}
141-
return 0
142-
}
143-
144-
// IsForceUpgrade checks if the error is a force upgrade error
145-
func IsForceUpgrade(err error) bool {
146-
var forceUpgradeErr *ForceUpgradeError
147-
return errors.As(err, &forceUpgradeErr)
148-
}
149-
150-
// IsTokenExpired checks if the error is a token expiration error
151-
func IsTokenExpired(err error) bool {
152-
return HasErrorCode(err, ErrorCodeInvalidToken)
153-
}
154-
155-
// CheckErr checks for errors and prints appropriate messages using the command's output
156-
// Returns true if no error (ok to continue), false if there was an error
157-
func CheckErr(cmd *cobra.Command, err error) bool {
158-
if err == nil {
159-
return true
160-
}
47+
// errorCodeToCLIError maps API error codes to CLIError instances
48+
var errorCodeToCLIError = map[int]*clierrors.CLIError{
49+
// Authentication & Authorization Errors (2000-2099)
50+
ErrorCodeUnauthorized: clierrors.ErrorUnauthorized,
51+
ErrorCodeInvalidToken: clierrors.ErrorInvalidToken,
52+
ErrorCodeInvalidUserCode: clierrors.ErrorInvalidUserCode,
53+
ErrorCodeTokenNotFound: clierrors.ErrorTokenNotFound,
54+
ErrorCodeInvalidDeviceCode: clierrors.ErrorInvalidDeviceCode,
55+
ErrorCodeAuthorizationPending: clierrors.ErrorAuthorizationPending,
16156

162-
// Check if it's a force upgrade error
163-
if IsForceUpgrade(err) {
164-
ui.PrintError(cmd, "Your CLI version is out of date and must be upgraded.", "brew update && brew upgrade major")
165-
return false
166-
}
57+
// Organization Errors (3000-3099)
58+
ErrorCodeOrganizationNotFound: clierrors.ErrorOrganizationNotFoundAPI,
59+
ErrorCodeNotOrgMember: clierrors.ErrorNotOrgMember,
60+
ErrorCodeNoCreatePermission: clierrors.ErrorNoCreatePermission,
16761

168-
// Check if it's a token expiration error
169-
if IsTokenExpired(err) {
170-
ui.PrintError(cmd, "Your session has expired!", "major user login")
171-
return false
172-
}
62+
// Application Errors (4000-4099)
63+
ErrorCodeApplicationNotFound: clierrors.ErrorApplicationNotFoundAPI,
64+
ErrorCodeNoApplicationAccess: clierrors.ErrorNoApplicationAccess,
65+
ErrorCodeDuplicateAppName: clierrors.ErrorDuplicateAppName,
17366

174-
// Check if it's a no token error
175-
if IsNoToken(err) {
176-
ui.PrintError(cmd, "Not logged in!", "major user login")
177-
return false
67+
// GitHub Integration Errors (5000-5099)
68+
ErrorCodeGitHubRepoNotFound: clierrors.ErrorGitHubRepoNotFound,
69+
ErrorCodeGitHubRepoAccessDenied: clierrors.ErrorGitHubRepoAccessDenied,
70+
ErrorCodeGitHubCollaboratorAddFailed: clierrors.ErrorGitHubCollaboratorAddFailed,
71+
}
72+
73+
// ToCLIError converts an APIError to a CLIError
74+
// If a specific error code mapping exists, it returns that CLIError
75+
// Otherwise, it creates a generic CLIError with the API error details
76+
func ToCLIError(errResp *ErrorResponse) error {
77+
// Check if we have a specific mapping for this error code
78+
if cliErr, exists := errorCodeToCLIError[errResp.Error.InternalCode]; exists {
79+
return cliErr
17880
}
17981

180-
// Check if it's an API error
181-
var apiErr *APIError
182-
if errors.As(err, &apiErr) {
183-
// Just print the error description/message, nothing else
184-
cmd.Printf("Error: %s\n", apiErr.Message)
185-
return false
82+
// No specific mapping - create a generic CLIError with API details
83+
return &clierrors.CLIError{
84+
Title: fmt.Sprintf("API Error (Code: %d)", errResp.Error.InternalCode),
85+
Suggestion: "Please try again or contact support if the issue persists.",
86+
Err: fmt.Errorf("%s", errResp.Error.ErrorString),
18687
}
187-
188-
// Generic error
189-
cmd.Printf("Error: %v\n", err)
190-
return false
19188
}

clients/git/client.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import (
66
"os/exec"
77
"regexp"
88
"strings"
9+
10+
"github.com/pkg/errors"
11+
12+
clierrors "github.com/major-technology/cli/errors"
913
)
1014

1115
// RemoteInfo contains parsed information from a git remote URL
@@ -32,7 +36,7 @@ func GetRemoteURLFromDir(dir string) (string, error) {
3236
if exitErr, ok := err.(*exec.ExitError); ok {
3337
stderr := string(exitErr.Stderr)
3438
if strings.Contains(stderr, "not a git repository") {
35-
return "", fmt.Errorf("you currently are not in a git repo")
39+
return "", clierrors.ErrorNotGitRepository
3640
}
3741
}
3842
return "", err
@@ -47,7 +51,7 @@ func Clone(url, targetDir string) error {
4751
output, err := cmd.CombinedOutput()
4852
if err != nil {
4953
// Include the git output in the error message
50-
return fmt.Errorf("%w: %s", err, string(output))
54+
return errors.Wrap(err, "git clone failed: "+string(output))
5155
}
5256
// Print output on success
5357
fmt.Print(string(output))
@@ -107,7 +111,7 @@ func ParseRemoteURL(remoteURL string) (*RemoteInfo, error) {
107111
}, nil
108112
}
109113

110-
return nil, fmt.Errorf("unsupported remote URL format: %s", remoteURL)
114+
return nil, clierrors.ErrorUnsupportedGitRemoteURLWithFormat(remoteURL)
111115
}
112116

113117
// GetRepoRoot returns the root directory of the git repository
@@ -173,7 +177,7 @@ func Pull(repoDir string) error {
173177
output, err := cmd.CombinedOutput()
174178
if err != nil {
175179
// Include the git output in the error message
176-
return fmt.Errorf("%w: %s", err, string(output))
180+
return errors.Wrap(err, "git pull failed: "+string(output))
177181
}
178182
return nil
179183
}

0 commit comments

Comments
 (0)