Skip to content

Commit f2d3b75

Browse files
authored
Merge pull request #39 from alpacax/copilot/fix-38
Enhance CLI error messages with clear, actionable guidance
2 parents 658d719 + 9076d6c commit f2d3b75

File tree

24 files changed

+186
-40
lines changed

24 files changed

+186
-40
lines changed

api/ftp/ftp.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ func DownloadFile(ac *client.AlpaconClient, src, dest, username, groupname strin
255255
for count := 0; count < maxAttempts; count++ {
256256
resp, err = http.Get(downloadResponse.DownloadURL)
257257
if err != nil {
258-
return err
258+
return fmt.Errorf("network error while downloading: %w", err)
259259
}
260260

261261
if resp.StatusCode == http.StatusOK {
@@ -273,7 +273,7 @@ func DownloadFile(ac *client.AlpaconClient, src, dest, username, groupname strin
273273

274274
respBody, err := io.ReadAll(resp.Body)
275275
if err != nil {
276-
return err
276+
return fmt.Errorf("failed to read download response: %w", err)
277277
}
278278

279279
var fileName string
@@ -284,16 +284,16 @@ func DownloadFile(ac *client.AlpaconClient, src, dest, username, groupname strin
284284
}
285285
err = utils.SaveFile(filepath.Join(dest, fileName), respBody)
286286
if err != nil {
287-
return err
287+
return fmt.Errorf("failed to save file locally: %w", err)
288288
}
289289
if recursive {
290290
err = utils.Unzip(filepath.Join(dest, fileName), dest)
291291
if err != nil {
292-
return err
292+
return fmt.Errorf("failed to extract downloaded folder: %w", err)
293293
}
294294
err = utils.DeleteFile(filepath.Join(dest, fileName))
295295
if err != nil {
296-
return err
296+
return fmt.Errorf("failed to clean up temporary zip file: %w", err)
297297
}
298298
}
299299
}

client/client.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const (
2626
func NewAlpaconAPIClient() (*AlpaconClient, error) {
2727
validConfig, err := config.LoadConfig()
2828
if err != nil {
29-
return nil, err
29+
return nil, fmt.Errorf("configuration file not found or invalid: %v. Please run 'alpacon login' to configure your connection", err)
3030
}
3131

3232
httpClient := &http.Client{
@@ -49,7 +49,7 @@ func NewAlpaconAPIClient() (*AlpaconClient, error) {
4949
fmt.Println("Refreshing access token...")
5050
tokenRes, err := auth0.RefreshAccessToken(validConfig.WorkspaceURL, httpClient, validConfig.RefreshToken)
5151
if err != nil {
52-
return nil, fmt.Errorf("failed to refresh access token: %v", err)
52+
return nil, fmt.Errorf("failed to refresh access token: %v. Your session may have expired completely. Please run 'alpacon login' to authenticate again", err)
5353
}
5454

5555
client.AccessToken = tokenRes.AccessToken
@@ -81,7 +81,7 @@ func (ac *AlpaconClient) checkAuth() error {
8181
return err
8282
}
8383
if !checkAuthResponse.Authenticated {
84-
return errors.New("authenticated failed")
84+
return errors.New("authentication failed: your login session has expired or is invalid. Please run 'alpacon login' to authenticate again")
8585
}
8686

8787
return nil

cmd/agent/agent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var AgentCmd = &cobra.Command{
1313
if err != nil {
1414
return err
1515
}
16-
return errors.New("subcommand error")
16+
return errors.New("a subcommand is required. Use 'alpacon agent upgrade', 'alpacon agent restart', or 'alpacon agent shutdown' to manage the server agent. Run 'alpacon agent --help' for more information")
1717
},
1818
}
1919

cmd/authority/authority.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var AuthorityCmd = &cobra.Command{
1313
if err != nil {
1414
return err
1515
}
16-
return errors.New("subcommand error")
16+
return errors.New("a subcommand is required. Use 'alpacon authority create', 'alpacon authority list', 'alpacon authority detail', 'alpacon authority download', or 'alpacon authority delete'. Run 'alpacon authority --help' for more information")
1717
},
1818
}
1919

cmd/cert/cert.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ var CertCmd = &cobra.Command{
1414
if err != nil {
1515
return err
1616
}
17-
return errors.New("subcommand error")
17+
return errors.New("a subcommand is required. Use 'alpacon cert list', 'alpacon cert detail', or 'alpacon cert download' to manage SSL/TLS certificates. Run 'alpacon cert --help' for more information")
1818
},
1919
}
2020

cmd/csr/csr.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var CsrCmd = &cobra.Command{
1313
if err != nil {
1414
return err
1515
}
16-
return errors.New("subcommand error")
16+
return errors.New("a subcommand is required. Use 'alpacon csr create', 'alpacon csr list', 'alpacon csr approve', 'alpacon csr deny', 'alpacon csr delete', or 'alpacon csr detail'. Run 'alpacon csr --help' for more information")
1717
},
1818
}
1919

cmd/ftp/cp.go

Lines changed: 154 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ftp
33
import (
44
"fmt"
55
"github.com/alpacax/alpacon-cli/api/mfa"
6+
"path/filepath"
67
"strings"
78
"time"
89

@@ -44,7 +45,15 @@ var CpCmd = &cobra.Command{
4445
recursive, _ := cmd.Flags().GetBool("recursive")
4546

4647
if len(args) < 2 {
47-
utils.CliError("You must specify at least two arguments.")
48+
utils.CliError("You must specify at least two arguments.\n\n" +
49+
"Usage examples:\n" +
50+
" • Upload file to server:\n" +
51+
" alpacon cp /local/file.txt server:/remote/path/\n" +
52+
" • Download file from server:\n" +
53+
" alpacon cp server:/remote/file.txt /local/path/\n" +
54+
" • Upload folder (recursive):\n" +
55+
" alpacon cp -r /local/folder server:/remote/path/\n\n" +
56+
"Note: Remote paths must include server name (e.g., myserver:/path/)")
4857
return
4958
}
5059

@@ -67,9 +76,19 @@ var CpCmd = &cobra.Command{
6776
sources := args[:len(args)-1]
6877
dest := args[len(args)-1]
6978

79+
// Validate source and destination paths
80+
if err := validatePaths(sources, dest); err != nil {
81+
utils.CliError("%s", err.Error())
82+
return
83+
}
84+
7085
alpaconClient, err := client.NewAlpaconAPIClient()
7186
if err != nil {
72-
utils.CliError("Connection to Alpacon API failed: %s. Consider re-logging.", err)
87+
utils.CliError("Connection to Alpacon API failed: %s.\n\n"+
88+
"Try these solutions:\n"+
89+
" • Re-login with 'alpacon login'\n"+
90+
" • Check your internet connection\n"+
91+
" • Verify the API endpoint is accessible", err)
7392
return
7493
}
7594

@@ -94,7 +113,8 @@ var CpCmd = &cobra.Command{
94113
}
95114
}
96115
} else {
97-
utils.CliError("Failed to upload the file to server: %s.", err)
116+
// Error already handled in uploadObject
117+
return
98118
}
99119
}
100120
} else if isRemotePath(sources[0]) && isLocalPath(dest) {
@@ -118,11 +138,16 @@ var CpCmd = &cobra.Command{
118138
}
119139
}
120140
} else {
121-
utils.CliError("Failed to download the file from server: %s.", err)
141+
// Error already handled in downloadObject
142+
return
122143
}
123144
}
124145
} else {
125-
utils.CliError("Invalid combination of source and destination paths.")
146+
utils.CliError("Invalid combination of source and destination paths.\n\n" +
147+
"Valid operations:\n" +
148+
" • Upload (local → remote): alpacon cp /local/file server:/remote/path/\n" +
149+
" • Download (remote → local): alpacon cp server:/remote/file /local/path/\n\n" +
150+
"Note: Remote paths must be in format 'servername:/path' (e.g., myserver:/tmp/file.txt)")
126151
}
127152
},
128153
}
@@ -154,16 +179,97 @@ func isLocalPaths(paths []string) bool {
154179
return true
155180
}
156181

182+
func validatePaths(sources []string, dest string) error {
183+
// Check for mixed local and remote sources (not allowed)
184+
hasLocal := false
185+
hasRemote := false
186+
187+
for _, src := range sources {
188+
if isRemotePath(src) {
189+
hasRemote = true
190+
} else {
191+
hasLocal = true
192+
}
193+
}
194+
195+
if hasLocal && hasRemote {
196+
return fmt.Errorf("cannot mix local and remote source paths in a single operation.\n\n" +
197+
"Examples of valid operations:\n" +
198+
" • Upload multiple local files: alpacon cp file1.txt file2.txt server:/remote/\n" +
199+
" • Download single remote file: alpacon cp server:/remote/file.txt /local/\n" +
200+
" • Cannot mix: alpacon cp file1.txt server:/file2.txt /dest/ ❌")
201+
}
202+
203+
// Check for invalid remote path format
204+
allPaths := append(sources, dest)
205+
for _, path := range allPaths {
206+
if isRemotePath(path) {
207+
if !strings.Contains(path, ":") {
208+
return fmt.Errorf("invalid remote path format: '%s'\n\n"+
209+
"Remote paths must be in format 'servername:/path'\n"+
210+
"Examples:\n"+
211+
" • myserver:/home/user/file.txt\n"+
212+
" • web-server:/var/www/index.html", path)
213+
}
214+
215+
parts := strings.SplitN(path, ":", 2)
216+
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
217+
return fmt.Errorf("invalid remote path format: '%s'\n\n"+
218+
"Remote paths must include both server name and path:\n"+
219+
" • Correct: myserver:/path/to/file\n"+
220+
" • Incorrect: :myfile (missing server name)\n"+
221+
" • Incorrect: myserver: (missing path)", path)
222+
}
223+
}
224+
}
225+
226+
return nil
227+
}
228+
157229
func uploadObject(client *client.AlpaconClient, src []string, dest, username, groupname string, recursive bool) error {
158230
var result []string
159231
var err error
160232

233+
// Extract server name for better error messages
234+
serverName, remotePath := utils.SplitPath(dest)
235+
161236
if recursive {
162237
result, err = ftp.UploadFolder(client, src, dest, username, groupname)
163238
} else {
164239
result, err = ftp.UploadFile(client, src, dest, username, groupname)
165240
}
166241
if err != nil {
242+
// Parse error and provide specific guidance
243+
errStr := err.Error()
244+
if strings.Contains(errStr, "no such file or directory") {
245+
utils.CliError("Source file(s) not found: %s\n\n"+
246+
"Please check:\n"+
247+
" • File paths are correct and files exist\n"+
248+
" • You have read permissions for the source files\n"+
249+
" • For folders, use -r flag: alpacon cp -r /local/folder %s",
250+
strings.Join(src, ", "), dest)
251+
} else if strings.Contains(errStr, "permission denied") || strings.Contains(errStr, "access denied") {
252+
utils.CliError("Permission denied uploading to '%s' on server '%s'.\n\n"+
253+
"Try these solutions:\n"+
254+
" • Upload as root: alpacon cp -u root %s %s\n"+
255+
" • Upload to writable location: alpacon cp %s %s:/tmp/\n"+
256+
" • Check destination directory permissions\n"+
257+
" • Ensure destination directory exists",
258+
remotePath, serverName, strings.Join(src, " "), dest, strings.Join(src, " "), serverName)
259+
} else if strings.Contains(errStr, "server not found") || strings.Contains(errStr, "unknown host") {
260+
utils.CliError("Server '%s' not found.\n\n"+
261+
"Please check:\n"+
262+
" • Server name is spelled correctly\n"+
263+
" • Server is registered: alpacon server ls\n"+
264+
" • You have access to this server", serverName)
265+
} else {
266+
utils.CliError("Failed to upload to server '%s': %s\n\n"+
267+
"Try these solutions:\n"+
268+
" • Check server connectivity: alpacon exec %s 'echo test'\n"+
269+
" • Verify destination path exists: alpacon exec %s 'ls -la %s'\n"+
270+
" • Check available disk space on server",
271+
serverName, err, serverName, serverName, filepath.Dir(remotePath))
272+
}
167273
return err
168274
}
169275
wrappedSrc := fmt.Sprintf("[%s]", strings.Join(src, ", "))
@@ -173,12 +279,52 @@ func uploadObject(client *client.AlpaconClient, src []string, dest, username, gr
173279
}
174280

175281
func downloadObject(client *client.AlpaconClient, src, dest, username, groupname string, recursive bool) error {
176-
var err error
177-
err = ftp.DownloadFile(client, src, dest, username, groupname, recursive)
282+
// Extract server name for better error messages
283+
serverName, remotePath := utils.SplitPath(src)
178284

285+
err := ftp.DownloadFile(client, src, dest, username, groupname, recursive)
179286
if err != nil {
287+
// Parse error and provide specific guidance
288+
errStr := err.Error()
289+
if strings.Contains(errStr, "no such file or directory") || strings.Contains(errStr, "file not found") {
290+
utils.CliError("Source file not found: '%s' on server '%s'.\n\n"+
291+
"Please check:\n"+
292+
" • File path is correct: %s\n"+
293+
" • File exists: alpacon exec %s 'ls -la %s'\n"+
294+
" • You have read permissions for the file\n"+
295+
" • For folders, use -r flag: alpacon cp -r %s %s",
296+
remotePath, serverName, remotePath, serverName, filepath.Dir(remotePath), src, dest)
297+
} else if strings.Contains(errStr, "permission denied") || strings.Contains(errStr, "access denied") {
298+
utils.CliError("Permission denied downloading '%s' from server '%s'.\n\n"+
299+
"Try these solutions:\n"+
300+
" • Download as root: alpacon cp -u root %s %s\n"+
301+
" • Download as file owner: alpacon cp -u OWNER %s %s\n"+
302+
" • Check file permissions: alpacon exec %s 'ls -la %s'",
303+
remotePath, serverName, src, dest, src, dest, serverName, remotePath)
304+
} else if strings.Contains(errStr, "server not found") || strings.Contains(errStr, "unknown host") {
305+
utils.CliError("Server '%s' not found.\n\n"+
306+
"Please check:\n"+
307+
" • Server name is spelled correctly\n"+
308+
" • Server is registered: alpacon server ls\n"+
309+
" • You have access to this server", serverName)
310+
} else if strings.Contains(errStr, "download failed") {
311+
utils.CliError("Download failed from server '%s': %s\n\n"+
312+
"This might be due to:\n"+
313+
" • Network connectivity issues\n"+
314+
" • Server timeout (file too large)\n"+
315+
" • Insufficient local disk space\n"+
316+
" • Server-side file access issues",
317+
serverName, err)
318+
} else {
319+
utils.CliError("Failed to download from server '%s': %s\n\n"+
320+
"Try these solutions:\n"+
321+
" • Check server connectivity: alpacon exec %s 'echo test'\n"+
322+
" • Verify source file: alpacon exec %s 'file %s'\n"+
323+
" • Check local destination permissions",
324+
serverName, err, serverName, serverName, remotePath)
325+
}
180326
return err
181327
}
182-
utils.CliInfo("Download request for %s to server %s successful.", src, dest)
328+
utils.CliInfo("Download request for %s to %s successful.", src, dest)
183329
return nil
184330
}

cmd/iam/group.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ var GroupCmd = &cobra.Command{
1717
if err != nil {
1818
return err
1919
}
20-
return errors.New("subcommand error")
20+
return errors.New("a subcommand is required. Use 'alpacon group list', 'alpacon group create', 'alpacon group detail', 'alpacon group delete', or 'alpacon group member' to manage groups. Run 'alpacon group --help' for more information")
2121
},
2222
}
2323

cmd/iam/member.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var MemberCmd = &cobra.Command{
1818
if err != nil {
1919
return err
2020
}
21-
return errors.New("subcommand error")
21+
return errors.New("a subcommand is required. Use 'alpacon group member add' or 'alpacon group member delete' to manage group membership. Run 'alpacon group member --help' for more information")
2222
},
2323
}
2424

cmd/iam/member_add.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,6 @@ func promptForRole() string {
6262
if strings.ToLower(role) == "owner" || strings.ToLower(role) == "manager" || strings.ToLower(role) == "member" {
6363
return role
6464
}
65-
fmt.Println("Invalid role. Please choose 'owner', 'manager', or 'member'.")
65+
fmt.Println("Invalid role. Please choose 'owner', 'manager', or 'member'. Role determines the user's permissions within the group")
6666
}
6767
}

0 commit comments

Comments
 (0)