Skip to content

Commit f3fd554

Browse files
authored
Fix auto-complete + use the new errors (#26)
* Fix auto-complete + use the new errors * Make sure create works with new github permissioning * Add version check stuff
1 parent c0c1987 commit f3fd554

15 files changed

Lines changed: 400 additions & 140 deletions

File tree

clients/api/client.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,29 @@ func (c *Client) doRequestInternal(method, path string, body interface{}, respon
8686
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
8787
var errResp ErrorResponse
8888
if err := json.Unmarshal(respBody, &errResp); err == nil {
89-
// Prefer error_description if available, otherwise use message
90-
message := errResp.ErrorDescription
89+
// Check for new error format first
90+
if errResp.Error != nil {
91+
return &APIError{
92+
StatusCode: errResp.Error.StatusCode,
93+
InternalCode: errResp.Error.InternalCode,
94+
Message: errResp.Error.ErrorString,
95+
ErrorType: errResp.Error.ErrorString,
96+
}
97+
}
98+
99+
// Fall back to legacy format
100+
message := errResp.ErrorString
91101
if message == "" {
92102
message = errResp.Message
93103
}
94104
if message == "" {
95105
message = string(respBody)
96106
}
97107
return &APIError{
98-
StatusCode: resp.StatusCode,
99-
Message: message,
100-
ErrorType: errResp.Error,
108+
StatusCode: resp.StatusCode,
109+
Message: message,
110+
ErrorType: message,
111+
InternalCode: 0, // Legacy errors don't have internal codes
101112
}
102113
}
103114
return &APIError{
@@ -129,16 +140,22 @@ func (c *Client) StartLogin() (*LoginStartResponse, error) {
129140
}
130141

131142
// PollLogin polls the login endpoint to check if the user has authorized the device
143+
// Returns the response and error. For authorization pending state, returns a specific error.
132144
func (c *Client) PollLogin(deviceCode string) (*LoginPollResponse, error) {
133145
req := LoginPollRequest{DeviceCode: deviceCode}
134146

135147
var resp LoginPollResponse
136148
err := c.doRequestWithoutAuth("POST", "/login/poll", req, &resp)
137149
if err != nil {
138-
// Check if it's a bad request (expired/invalid codes)
139-
if IsBadRequest(err) {
150+
// Check if authorization is pending (expected error state)
151+
if IsAuthorizationPending(err) {
152+
return nil, err // Return the error so caller can check with IsAuthorizationPending
153+
}
154+
// Check if it's an invalid device code
155+
if IsInvalidDeviceCode(err) {
140156
return nil, fmt.Errorf("invalid or expired device code")
141157
}
158+
// Other errors
142159
return nil, err
143160
}
144161

@@ -282,3 +299,16 @@ func (c *Client) AddGithubCollaborators(applicationID, githubUsername string) (*
282299
}
283300
return &resp, nil
284301
}
302+
303+
// --- Version Check endpoints ---
304+
305+
// CheckVersion checks if the CLI version is up to date
306+
func (c *Client) CheckVersion(currentVersion string) (*CheckVersionResponse, error) {
307+
req := VersionCheckRequest{Version: currentVersion}
308+
var resp CheckVersionResponse
309+
err := c.doRequestWithoutAuth("POST", "/version/check", req, &resp)
310+
if err != nil {
311+
return nil, err
312+
}
313+
return &resp, nil
314+
}

clients/api/errors.go

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,56 @@ import (
99
"github.com/spf13/cobra"
1010
)
1111

12+
// Error code constants from @repo/errors
13+
const (
14+
// Authentication & Authorization Errors (2000-2099)
15+
ErrorCodeUnauthorized = 2000
16+
ErrorCodeInvalidToken = 2001
17+
ErrorCodeInvalidUserCode = 2002
18+
ErrorCodeTokenNotFound = 2003
19+
ErrorCodeInvalidDeviceCode = 2004
20+
ErrorCodeAuthorizationPending = 2005
21+
22+
// Organization Errors (3000-3099)
23+
ErrorCodeOrganizationNotFound = 3000
24+
ErrorCodeNotOrgMember = 3001
25+
ErrorCodeNoCreatePermission = 3002
26+
27+
// Application Errors (4000-4099)
28+
ErrorCodeApplicationNotFound = 4000
29+
ErrorCodeNoApplicationAccess = 4001
30+
ErrorCodeDuplicateAppName = 4002
31+
32+
// GitHub Integration Errors (5000-5099)
33+
ErrorCodeGitHubRepoNotFound = 5000
34+
ErrorCodeGitHubRepoAccessDenied = 5001
35+
ErrorCodeGitHubCollaboratorAddFailed = 5002
36+
)
37+
38+
// AppErrorDetail represents the error detail from the API (new format)
39+
type AppErrorDetail struct {
40+
InternalCode int `json:"internal_code"`
41+
ErrorString string `json:"error_string"`
42+
StatusCode int `json:"status_code"`
43+
}
44+
1245
// ErrorResponse represents an error response from the API
46+
// Supports both new format (error object) and legacy format (error string)
1347
type ErrorResponse struct {
14-
Error string `json:"error"`
15-
ErrorDescription string `json:"error_description"`
16-
Message string `json:"message"`
48+
// New format
49+
Error *AppErrorDetail `json:"error,omitempty"`
50+
51+
// Legacy format (for backward compatibility)
52+
ErrorString string `json:"error_description,omitempty"`
53+
Message string `json:"message,omitempty"`
1754
}
1855

1956
// APIError represents an API error with status code and message
2057
type APIError struct {
21-
StatusCode int
22-
Message string
23-
ErrorType string
58+
StatusCode int
59+
InternalCode int // Internal error code from the API
60+
Message string
61+
ErrorType string
2462
}
2563

2664
func (e *APIError) Error() string {
@@ -39,6 +77,15 @@ func (e *NoTokenError) Error() string {
3977
return fmt.Sprintf("not logged in: %v", e.OriginalError)
4078
}
4179

80+
// ForceUpgradeError represents an error when the CLI version is too old and must be upgraded
81+
type ForceUpgradeError struct {
82+
LatestVersion string
83+
}
84+
85+
func (e *ForceUpgradeError) Error() string {
86+
return "CLI version is out of date and must be upgraded"
87+
}
88+
4289
// IsUnauthorized checks if the error is an unauthorized error
4390
func IsUnauthorized(err error) bool {
4491
var apiErr *APIError
@@ -72,13 +119,68 @@ func IsNoToken(err error) bool {
72119
return errors.As(err, &noTokenErr)
73120
}
74121

122+
// HasErrorCode checks if the error has a specific internal error code
123+
func HasErrorCode(err error, code int) bool {
124+
var apiErr *APIError
125+
if errors.As(err, &apiErr) {
126+
return apiErr.InternalCode == code
127+
}
128+
return false
129+
}
130+
131+
// IsAuthorizationPending checks if the error is an authorization pending error
132+
func IsAuthorizationPending(err error) bool {
133+
return HasErrorCode(err, ErrorCodeAuthorizationPending)
134+
}
135+
136+
// IsInvalidDeviceCode checks if the error is an invalid device code error
137+
func IsInvalidDeviceCode(err error) bool {
138+
return HasErrorCode(err, ErrorCodeInvalidDeviceCode)
139+
}
140+
141+
// GetErrorCode returns the internal error code from an error, or 0 if not an APIError
142+
func GetErrorCode(err error) int {
143+
var apiErr *APIError
144+
if errors.As(err, &apiErr) {
145+
return apiErr.InternalCode
146+
}
147+
return 0
148+
}
149+
150+
// IsForceUpgrade checks if the error is a force upgrade error
151+
func IsForceUpgrade(err error) bool {
152+
var forceUpgradeErr *ForceUpgradeError
153+
return errors.As(err, &forceUpgradeErr)
154+
}
155+
75156
// CheckErr checks for errors and prints appropriate messages using the command's output
76157
// Returns true if no error (ok to continue), false if there was an error
77158
func CheckErr(cmd *cobra.Command, err error) bool {
78159
if err == nil {
79160
return true
80161
}
81162

163+
// Check if it's a force upgrade error
164+
if IsForceUpgrade(err) {
165+
// Create styled error message box
166+
errorStyle := lipgloss.NewStyle().
167+
Bold(true).
168+
Foreground(lipgloss.Color("#FF5F87")).
169+
Padding(1, 2).
170+
Border(lipgloss.RoundedBorder()).
171+
BorderForeground(lipgloss.Color("#FF5F87"))
172+
173+
commandStyle := lipgloss.NewStyle().
174+
Bold(true).
175+
Foreground(lipgloss.Color("#87D7FF"))
176+
177+
message := fmt.Sprintf("Your CLI version is out of date and must be upgraded.\n\nRun:\n%s",
178+
commandStyle.Render("brew update && brew upgrade major"))
179+
180+
cmd.Println(errorStyle.Render(message))
181+
return false
182+
}
183+
82184
// Check if it's a no token error
83185
if IsNoToken(err) {
84186
// Create styled error message box

clients/api/structs.go

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ package api
44

55
// LoginStartResponse represents the response from POST /login/start
66
type LoginStartResponse struct {
7-
DeviceCode string `json:"device_code"`
8-
UserCode string `json:"user_code"`
9-
VerificationURI string `json:"verification_uri"`
10-
ExpiresIn int `json:"expires_in"`
11-
Interval int `json:"interval"`
7+
Error *AppErrorDetail `json:"error,omitempty"`
8+
DeviceCode string `json:"device_code,omitempty"`
9+
UserCode string `json:"user_code,omitempty"`
10+
VerificationURI string `json:"verification_uri,omitempty"`
11+
ExpiresIn int `json:"expires_in,omitempty"`
12+
Interval int `json:"interval,omitempty"`
1213
}
1314

1415
// LoginPollRequest represents the request body for POST /login/poll
@@ -18,22 +19,19 @@ type LoginPollRequest struct {
1819

1920
// LoginPollResponse represents the response from POST /login/poll
2021
type LoginPollResponse struct {
21-
// Pending state
22-
Error string `json:"error,omitempty"`
23-
ErrorDescription string `json:"error_description,omitempty"`
24-
25-
// Success state
26-
AccessToken string `json:"access_token,omitempty"`
27-
TokenType string `json:"token_type,omitempty"`
28-
ExpiresIn int `json:"expires_in,omitempty"`
22+
Error *AppErrorDetail `json:"error,omitempty"`
23+
AccessToken string `json:"access_token,omitempty"`
24+
TokenType string `json:"token_type,omitempty"`
25+
ExpiresIn int `json:"expires_in,omitempty"`
2926
}
3027

3128
// VerifyTokenResponse represents the response from GET /verify
3229
type VerifyTokenResponse struct {
33-
Active bool `json:"active"`
34-
UserID string `json:"user_id"`
35-
Email string `json:"email"`
36-
Exp int64 `json:"exp"`
30+
Error *AppErrorDetail `json:"error,omitempty"`
31+
Active bool `json:"active,omitempty"`
32+
UserID string `json:"user_id,omitempty"`
33+
Email string `json:"email,omitempty"`
34+
Exp int64 `json:"exp,omitempty"`
3735
}
3836

3937
// --- Organization structs ---
@@ -46,7 +44,8 @@ type Organization struct {
4644

4745
// OrganizationsResponse represents the response from GET /organizations
4846
type OrganizationsResponse struct {
49-
Organizations []Organization `json:"organizations"`
47+
Error *AppErrorDetail `json:"error,omitempty"`
48+
Organizations []Organization `json:"organizations,omitempty"`
5049
}
5150

5251
// --- Application structs ---
@@ -60,10 +59,11 @@ type CreateApplicationRequest struct {
6059

6160
// CreateApplicationResponse represents the response from POST /applications
6261
type CreateApplicationResponse struct {
63-
ApplicationID string `json:"applicationId"`
64-
RepositoryName string `json:"repositoryName"`
65-
CloneURLSSH string `json:"cloneUrlSsh"`
66-
CloneURLHTTPS string `json:"cloneUrlHttps"`
62+
Error *AppErrorDetail `json:"error,omitempty"`
63+
ApplicationID string `json:"applicationId,omitempty"`
64+
RepositoryName string `json:"repositoryName,omitempty"`
65+
CloneURLSSH string `json:"cloneUrlSsh,omitempty"`
66+
CloneURLHTTPS string `json:"cloneUrlHttps,omitempty"`
6767
}
6868

6969
// GetApplicationByRepoRequest represents the request body for GET /application/from-repo
@@ -74,7 +74,8 @@ type GetApplicationByRepoRequest struct {
7474

7575
// GetApplicationByRepoResponse represents the response from GET /application/from-repo
7676
type GetApplicationByRepoResponse struct {
77-
ApplicationID string `json:"applicationId"`
77+
Error *AppErrorDetail `json:"error,omitempty"`
78+
ApplicationID string `json:"applicationId,omitempty"`
7879
}
7980

8081
// GetApplicationEnvRequest represents the request body for POST /application/env
@@ -85,7 +86,8 @@ type GetApplicationEnvRequest struct {
8586

8687
// GetApplicationEnvResponse represents the response from POST /application/env
8788
type GetApplicationEnvResponse struct {
88-
EnvVars map[string]string `json:"envVars"`
89+
Error *AppErrorDetail `json:"error,omitempty"`
90+
EnvVars map[string]string `json:"envVars,omitempty"`
8991
}
9092

9193
// ResourceItem represents a single resource
@@ -97,7 +99,8 @@ type ResourceItem struct {
9799

98100
// GetApplicationResourcesResponse represents the response from GET /applications/:applicationId/resources
99101
type GetApplicationResourcesResponse struct {
100-
Resources []ResourceItem `json:"resources"`
102+
Error *AppErrorDetail `json:"error,omitempty"`
103+
Resources []ResourceItem `json:"resources,omitempty"`
101104
}
102105

103106
// CreateApplicationVersionRequest represents the request body for POST /applications/versions
@@ -107,7 +110,8 @@ type CreateApplicationVersionRequest struct {
107110

108111
// CreateApplicationVersionResponse represents the response from POST /applications/versions
109112
type CreateApplicationVersionResponse struct {
110-
VersionID string `json:"versionId"`
113+
Error *AppErrorDetail `json:"error,omitempty"`
114+
VersionID string `json:"versionId,omitempty"`
111115
}
112116

113117
// ApplicationItem represents a single application in the list
@@ -126,7 +130,8 @@ type GetOrganizationApplicationsRequest struct {
126130

127131
// GetOrganizationApplicationsResponse represents the response from POST /organizations/applications
128132
type GetOrganizationApplicationsResponse struct {
129-
Applications []ApplicationItem `json:"applications"`
133+
Error *AppErrorDetail `json:"error,omitempty"`
134+
Applications []ApplicationItem `json:"applications,omitempty"`
130135
}
131136

132137
// AddGithubCollaboratorsRequest represents the request body for POST /applications/add-gh-collaborators
@@ -137,6 +142,21 @@ type AddGithubCollaboratorsRequest struct {
137142

138143
// AddGithubCollaboratorsResponse represents the response from POST /applications/add-gh-collaborators
139144
type AddGithubCollaboratorsResponse struct {
140-
Success bool `json:"success"`
141-
Message string `json:"message,omitempty"`
145+
Error *AppErrorDetail `json:"error,omitempty"`
146+
Success bool `json:"success,omitempty"`
147+
Message string `json:"message,omitempty"`
148+
}
149+
150+
// --- Version Check structs ---
151+
152+
// CheckVersionResponse represents the response from GET /version/check
153+
type CheckVersionResponse struct {
154+
Error *AppErrorDetail `json:"error,omitempty"`
155+
ForceUpgrade bool `json:"forceUpgrade,omitempty"`
156+
CanUpgrade bool `json:"canUpgrade,omitempty"`
157+
LatestVersion *string `json:"latestVersion,omitempty"`
158+
}
159+
160+
type VersionCheckRequest struct {
161+
Version string `json:"version"`
142162
}

cmd/app/app.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package app
22

33
import (
4+
"github.com/major-technology/cli/utils"
45
"github.com/spf13/cobra"
56
)
67

@@ -9,6 +10,10 @@ var Cmd = &cobra.Command{
910
Use: "app",
1011
Short: "Application management commands",
1112
Long: `Commands for creating and managing applications.`,
13+
Args: utils.NoArgs,
14+
Run: func(cmd *cobra.Command, args []string) {
15+
cmd.Help()
16+
},
1217
}
1318

1419
func init() {

0 commit comments

Comments
 (0)