• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mindersec / minder / 16185345365

10 Jul 2025 03:21AM UTC coverage: 57.567% (+0.2%) from 57.402%
16185345365

push

github

web-flow
Polish default CLI behavior (#5745)

Quiet default CLI behavior: auto-log-in if needed, and create empty config if not present

6 of 20 new or added lines in 2 files covered. (30.0%)

1 existing line in 1 file now uncovered.

18642 of 32383 relevant lines covered (57.57%)

37.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

13.04
/internal/util/cli/cli.go
1
// SPDX-FileCopyrightText: Copyright 2023 The Minder Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
// Package cli contains utility for the cli
5
package cli
6

7
import (
8
        "context"
9
        "errors"
10
        "fmt"
11
        "os"
12
        "path/filepath"
13
        "regexp"
14
        "strings"
15
        "time"
16

17
        "github.com/erikgeiser/promptkit/confirmation"
18
        "github.com/spf13/cobra"
19
        "github.com/spf13/viper"
20
        "google.golang.org/grpc"
21
        "google.golang.org/grpc/codes"
22
        "google.golang.org/grpc/status"
23

24
        "github.com/mindersec/minder/internal/util"
25
)
26

27
// ErrWrappedCLIError is an error that wraps another error and provides a message used from within the CLI
28
type ErrWrappedCLIError struct {
29
        Message string
30
        Err     error
31
}
32

33
func (e *ErrWrappedCLIError) Error() string {
×
34
        return e.Err.Error()
×
35
}
×
36

37
// PrintYesNoPrompt prints a yes/no prompt to the user and returns false if the user did not respond with yes or y
38
func PrintYesNoPrompt(cmd *cobra.Command, promptMsg, confirmMsg, fallbackMsg string, defaultYes bool) bool {
×
39
        // Print the warning banner with the prompt message
×
40
        cmd.Println(WarningBanner.Render(promptMsg))
×
41

×
42
        // Determine the default confirmation value
×
43
        defConf := confirmation.No
×
44
        if defaultYes {
×
45
                defConf = confirmation.Yes
×
46
        }
×
47

48
        // Prompt the user for confirmation
49
        input := confirmation.New(confirmMsg, defConf)
×
50
        ok, err := input.RunPrompt()
×
51
        if err != nil {
×
52
                cmd.Println(WarningBanner.Render(fmt.Sprintf("Error reading input: %v", err)))
×
53
                ok = false
×
54
        }
×
55

56
        // If the user did not confirm, print the fallback message
57
        if !ok {
×
58
                cmd.Println(Header.Render(fallbackMsg))
×
59
        }
×
60
        return ok
×
61
}
62

63
// GetAppContext is a helper for getting the cmd app context
64
func GetAppContext(ctx context.Context, v *viper.Viper) (context.Context, context.CancelFunc) {
×
65
        return GetAppContextWithTimeoutDuration(ctx, v, 20)
×
66
}
×
67

68
// GetAppContextWithTimeoutDuration is a helper for getting the cmd app context with a custom timeout
69
func GetAppContextWithTimeoutDuration(ctx context.Context, v *viper.Viper, tout int) (context.Context, context.CancelFunc) {
×
70
        v.SetDefault("cli.context_timeout", tout)
×
71
        timeout := v.GetInt("cli.context_timeout")
×
72

×
73
        ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
×
74
        return ctx, cancel
×
75
}
×
76

77
// GRPCClientWrapRunE is a wrapper for cobra commands that sets up the grpc client and context
78
func GRPCClientWrapRunE(
79
        runEFunc func(ctx context.Context, cmd *cobra.Command, args []string, c *grpc.ClientConn) error,
80
) func(cmd *cobra.Command, args []string) error {
67✔
81
        return func(cmd *cobra.Command, args []string) error {
67✔
82
                if err := viper.BindPFlags(cmd.Flags()); err != nil {
×
83
                        return fmt.Errorf("error binding flags: %s", err)
×
84
                }
×
85

86
                ctx, cancel := GetAppContext(cmd.Context(), viper.GetViper())
×
87
                defer cancel()
×
88

×
89
                c, err := GrpcForCommand(cmd, viper.GetViper())
×
90
                if err != nil {
×
91
                        return err
×
92
                }
×
93

94
                defer c.Close()
×
95

×
96
                return runEFunc(ctx, cmd, args, c)
×
97
        }
98
}
99

100
// MessageAndError prints a message and returns an error.
101
func MessageAndError(msg string, err error) error {
×
102
        return &ErrWrappedCLIError{Message: msg, Err: err}
×
103
}
×
104

105
// ExitNicelyOnError print a message and exit with the right code
106
func ExitNicelyOnError(err error, userMsg string) {
2✔
107
        var message string
2✔
108
        var details string
2✔
109
        exitCode := 1 // Default to 1
2✔
110
        if err != nil {
2✔
111
                if userMsg != "" {
×
112
                        // This handles the case where we want to print an explicit message before processing the error
×
113
                        fmt.Fprintf(os.Stderr, "Message: %s\n", userMsg)
×
114
                }
×
115
                // Check if the error is wrapped
116
                var wrappedErr *ErrWrappedCLIError
×
117
                if errors.As(err, &wrappedErr) {
×
118
                        // Print the wrapped message
×
119
                        message = wrappedErr.Message
×
120
                        // Continue processing the wrapped error
×
121
                        err = wrappedErr.Err
×
122
                }
×
123
                // Check if the error is a grpc status
124
                if rpcStatus, ok := status.FromError(err); ok {
×
125
                        nice := util.FromRpcError(rpcStatus)
×
126
                        // If the error is unauthenticated, we want to print a helpful message and exit, no need to print details
×
127
                        if rpcStatus.Code() == codes.Unauthenticated {
×
128
                                message = "It seems you are logged out. Please run \"minder auth login\" first."
×
129
                        } else {
×
130
                                details = nice.Details
×
131
                        }
×
132
                        exitCode = int(nice.Code)
×
133
                } else {
×
134
                        details = err.Error()
×
135
                }
×
136
                // Print the message, if any
137
                if message != "" {
×
138
                        fmt.Fprintf(os.Stderr, "Message: %s\n", message)
×
139
                }
×
140
                // Print the details, if any
141
                if details != "" {
×
142
                        fmt.Fprintf(os.Stderr, "Details: %s\n", details)
×
143
                }
×
144
                // Exit with the right code
145
                os.Exit(exitCode)
×
146
        }
147
}
148

149
// GetRepositoryName returns the repository name in the format owner/name
150
func GetRepositoryName(owner, name string) string {
×
151
        if owner == "" {
×
152
                return name
×
153
        }
×
154
        return fmt.Sprintf("%s/%s", owner, name)
×
155
}
156

157
var validRepoSlugRe = regexp.MustCompile(`(?i)^[-a-z0-9_\.]+\/[-a-z0-9_\.]+$`)
158

159
// ValidateRepositoryName checks if a repository name is valid
160
func ValidateRepositoryName(repository string) error {
×
161
        if !validRepoSlugRe.MatchString(repository) {
×
162
                return fmt.Errorf("invalid repository name: %s", repository)
×
163
        }
×
164
        return nil
×
165
}
166

167
// GetNameAndOwnerFromRepository returns the owner and name from a repository name in the format owner/name
168
func GetNameAndOwnerFromRepository(repository string) (string, string) {
×
169
        first, second, found := strings.Cut(repository, "/")
×
170
        if !found {
×
171
                return "", first
×
172
        }
×
173

174
        return first, second
×
175
}
176

177
// ConcatenateAndWrap takes a string and a maximum line length (maxLen),
178
// then outputs the string as a multiline string where each line does not exceed maxLen characters.
179
func ConcatenateAndWrap(input string, maxLen int) string {
×
180
        if maxLen <= 0 {
×
181
                return input
×
182
        }
×
183

184
        var result string
×
185
        var lineLength int
×
186

×
187
        for _, runeValue := range input {
×
188
                // If the line length equals the len, append a newline and reset lineLength
×
189
                if lineLength == maxLen {
×
190
                        if result[len(result)-1] != ' ' {
×
191
                                // We trim at a word
×
192
                                result += "-\n"
×
193
                        } else {
×
194
                                // We trim at a space, no need to add "-"
×
195
                                result += "\n"
×
196
                        }
×
197
                        lineLength = 0
×
198
                }
199
                result += string(runeValue)
×
200
                lineLength++
×
201
        }
202

203
        return result
×
204
}
205

206
// GetConfigDirPath returns the path to the config directory
207
func GetConfigDirPath() (string, error) {
14✔
208
        // Get the XDG_CONFIG_HOME environment variable
14✔
209
        xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
14✔
210

14✔
211
        // If XDG_CONFIG_HOME is not set or empty, use $HOME/.config as the base directory
14✔
212
        if xdgConfigHome == "" {
15✔
213
                homeDir, err := os.UserHomeDir()
1✔
214
                if err != nil {
1✔
215
                        return "", fmt.Errorf("error getting home directory: %v", err)
×
216
                }
×
217
                xdgConfigHome = filepath.Join(homeDir, ".config")
1✔
218
        }
219

220
        filePath := filepath.Join(xdgConfigHome, "minder")
14✔
221
        return filePath, nil
14✔
222
}
223

224
// IsYAMLFileAndNotATest checks if a file is a YAML file and not a test file
UNCOV
225
func IsYAMLFileAndNotATest(path string) bool {
×
226
        return (filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml")
×
227
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc