Skip to content

Commit e4d313d

Browse files
authored
Better error messaging, error messaging middleware (#38)
1 parent 0c6d731 commit e4d313d

4 files changed

Lines changed: 213 additions & 49 deletions

File tree

clients/api/errors.go

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"fmt"
66
"net/http"
77

8-
"github.com/charmbracelet/lipgloss"
8+
"github.com/major-technology/cli/ui"
99
"github.com/spf13/cobra"
1010
)
1111

@@ -161,64 +161,19 @@ func CheckErr(cmd *cobra.Command, err error) bool {
161161

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

183168
// Check if it's a token expiration error
184169
if IsTokenExpired(err) {
185-
// Create styled error message box
186-
errorStyle := lipgloss.NewStyle().
187-
Bold(true).
188-
Foreground(lipgloss.Color("#FF5F87")).
189-
Padding(1, 2).
190-
Border(lipgloss.RoundedBorder()).
191-
BorderForeground(lipgloss.Color("#FF5F87"))
192-
193-
commandStyle := lipgloss.NewStyle().
194-
Bold(true).
195-
Foreground(lipgloss.Color("#87D7FF"))
196-
197-
message := fmt.Sprintf("Your session has expired!\n\nRun %s to login again.",
198-
commandStyle.Render("major user login"))
199-
200-
cmd.Println(errorStyle.Render(message))
170+
ui.PrintError(cmd, "Your session has expired!", "major user login")
201171
return false
202172
}
203173

204174
// Check if it's a no token error
205175
if IsNoToken(err) {
206-
// Create styled error message box
207-
errorStyle := lipgloss.NewStyle().
208-
Bold(true).
209-
Foreground(lipgloss.Color("#FF5F87")).
210-
Padding(1, 2).
211-
Border(lipgloss.RoundedBorder()).
212-
BorderForeground(lipgloss.Color("#FF5F87"))
213-
214-
commandStyle := lipgloss.NewStyle().
215-
Bold(true).
216-
Foreground(lipgloss.Color("#87D7FF"))
217-
218-
message := fmt.Sprintf("Not logged in!\n\nRun %s to get started.",
219-
commandStyle.Render("major user login"))
220-
221-
cmd.Println(errorStyle.Render(message))
176+
ui.PrintError(cmd, "Not logged in!", "major user login")
222177
return false
223178
}
224179

cmd/app/create.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/major-technology/cli/clients/api"
1414
"github.com/major-technology/cli/clients/git"
1515
mjrToken "github.com/major-technology/cli/clients/token"
16+
"github.com/major-technology/cli/middleware"
1617
"github.com/major-technology/cli/singletons"
1718
"github.com/spf13/cobra"
1819
)
@@ -22,6 +23,9 @@ var createCmd = &cobra.Command{
2223
Use: "create",
2324
Short: "Create a new application",
2425
Long: `Create a new application with a GitHub repository and sets up the basic template.`,
26+
PreRunE: middleware.Compose(
27+
middleware.CheckLogin,
28+
),
2529
Run: func(cobraCmd *cobra.Command, args []string) {
2630
cobra.CheckErr(runCreate(cobraCmd))
2731
},

middleware/middleware.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package middleware
2+
3+
import (
4+
"fmt"
5+
"os/exec"
6+
"regexp"
7+
"strconv"
8+
"strings"
9+
10+
"github.com/major-technology/cli/clients/api"
11+
"github.com/major-technology/cli/singletons"
12+
"github.com/major-technology/cli/ui"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
// MiddlewareError is a custom error type that includes a title and suggestion
17+
type MiddlewareError struct {
18+
Title string
19+
Suggestion string
20+
Err error
21+
}
22+
23+
func (e *MiddlewareError) Error() string {
24+
return e.Title
25+
}
26+
27+
func (e *MiddlewareError) Unwrap() error {
28+
return e.Err
29+
}
30+
31+
// CommandCheck is a function that performs a check before a command is run
32+
type CommandCheck func(cmd *cobra.Command, args []string) error
33+
34+
// Compose combines multiple checks into a single function compatible with Cobra's PreRunE
35+
func Compose(checks ...CommandCheck) func(cmd *cobra.Command, args []string) error {
36+
return func(cmd *cobra.Command, args []string) error {
37+
for _, check := range checks {
38+
if err := check(cmd, args); err != nil {
39+
// If it's our custom MiddlewareError, print nicely
40+
if mwErr, ok := err.(*MiddlewareError); ok {
41+
ui.PrintError(cmd, mwErr.Title, mwErr.Suggestion)
42+
cmd.SilenceErrors = true
43+
cmd.SilenceUsage = true
44+
return err
45+
}
46+
47+
// Fallback: print using ui.PrintError for consistency or rely on CheckErr if relevant
48+
// For checks that didn't return MiddlewareError but are simple errors:
49+
ui.PrintError(cmd, err.Error(), "")
50+
cmd.SilenceErrors = true
51+
cmd.SilenceUsage = true
52+
return err
53+
}
54+
}
55+
return nil
56+
}
57+
}
58+
59+
// CheckLogin checks if the user is logged in and the session is valid
60+
func CheckLogin(cmd *cobra.Command, args []string) error {
61+
client := singletons.GetAPIClient()
62+
63+
// VerifyToken checks if the token exists and is valid by calling the API
64+
_, err := client.VerifyToken()
65+
if err != nil {
66+
// Try to determine the specific error
67+
title := "Authentication failed"
68+
suggestion := "Run major user login to authenticate"
69+
70+
if api.IsTokenExpired(err) || strings.Contains(err.Error(), "expired") {
71+
title = "Your session has expired!"
72+
suggestion = "Run major user login to login again."
73+
} else if api.IsNoToken(err) || strings.Contains(err.Error(), "not logged in") || strings.Contains(err.Error(), "invalid") {
74+
title = "Not logged in!"
75+
suggestion = "Run major user login to get started."
76+
}
77+
78+
return &MiddlewareError{
79+
Title: title,
80+
Suggestion: suggestion,
81+
Err: err,
82+
}
83+
}
84+
85+
return nil
86+
}
87+
88+
// CheckPnpmInstalled checks if pnpm is installed in the system path
89+
func CheckPnpmInstalled(cmd *cobra.Command, args []string) error {
90+
_, err := exec.LookPath("pnpm")
91+
if err != nil {
92+
return &MiddlewareError{
93+
Title: "pnpm not found",
94+
Suggestion: "pnpm is required. Please install it: npm install -g pnpm",
95+
Err: err,
96+
}
97+
}
98+
return nil
99+
}
100+
101+
// CheckNodeVersion checks if node is installed and meets the minimum version requirement
102+
func CheckNodeVersion(minVersion string) CommandCheck {
103+
return func(cmd *cobra.Command, args []string) error {
104+
path, err := exec.LookPath("node")
105+
if err != nil {
106+
return &MiddlewareError{
107+
Title: "Node.js not found",
108+
Suggestion: "Node.js is required. Please install it.",
109+
Err: err,
110+
}
111+
}
112+
113+
cmdOut := exec.Command(path, "--version")
114+
output, err := cmdOut.Output()
115+
if err != nil {
116+
return fmt.Errorf("failed to check node version: %w", err)
117+
}
118+
119+
versionStr := strings.TrimSpace(string(output))
120+
// Remove 'v' prefix if present
121+
versionStr = strings.TrimPrefix(versionStr, "v")
122+
123+
if !isVersionGTE(versionStr, minVersion) {
124+
return &MiddlewareError{
125+
Title: fmt.Sprintf("Node.js version %s required", minVersion),
126+
Suggestion: fmt.Sprintf("You are running version %s. Please upgrade Node.js.", versionStr),
127+
Err: fmt.Errorf("node version %s is required, but found %s", minVersion, versionStr),
128+
}
129+
}
130+
131+
return nil
132+
}
133+
}
134+
135+
// isVersionGTE returns true if v1 >= v2
136+
// This is a simple implementation assuming semver format x.y.z
137+
func isVersionGTE(v1, v2 string) bool {
138+
parts1 := parseVersion(v1)
139+
parts2 := parseVersion(v2)
140+
141+
for i := 0; i < 3; i++ {
142+
if parts1[i] > parts2[i] {
143+
return true
144+
}
145+
if parts1[i] < parts2[i] {
146+
return false
147+
}
148+
}
149+
return true
150+
}
151+
152+
func parseVersion(v string) [3]int {
153+
var parts [3]int
154+
155+
// Use regex to extract numbers
156+
re := regexp.MustCompile(`(\d+)\.(\d+)\.(\d+)`)
157+
matches := re.FindStringSubmatch(v)
158+
159+
if len(matches) == 4 {
160+
for i := 1; i <= 3; i++ {
161+
val, _ := strconv.Atoi(matches[i])
162+
parts[i-1] = val
163+
}
164+
} else {
165+
// Fallback for simpler versions like "18" or "18.1"
166+
split := strings.Split(v, ".")
167+
for i := 0; i < len(split) && i < 3; i++ {
168+
val, _ := strconv.Atoi(split[i])
169+
parts[i] = val
170+
}
171+
}
172+
173+
return parts
174+
}

ui/ui.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package ui
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/charmbracelet/lipgloss"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
// PrintError prints a styled error message to the command output
11+
func PrintError(cmd *cobra.Command, title string, suggestion string) {
12+
errorStyle := lipgloss.NewStyle().
13+
Bold(true).
14+
Foreground(lipgloss.Color("#FF5F87")).
15+
Padding(1, 2).
16+
Border(lipgloss.RoundedBorder()).
17+
BorderForeground(lipgloss.Color("#FF5F87"))
18+
19+
commandStyle := lipgloss.NewStyle().
20+
Bold(true).
21+
Foreground(lipgloss.Color("#87D7FF"))
22+
23+
var message string
24+
if suggestion != "" {
25+
message = fmt.Sprintf("%s\n\n%s", title, commandStyle.Render(suggestion))
26+
} else {
27+
message = title
28+
}
29+
30+
cmd.Println(errorStyle.Render(message))
31+
}

0 commit comments

Comments
 (0)