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

mindersec / minder / 25342198270

04 May 2026 08:36PM UTC coverage: 60.442% (+2.1%) from 58.3%
25342198270

Pull #6253

github

web-flow
Merge 537c7f40e into 1347d06af
Pull Request #6253: Update roadmap for 2026 (and possibly beyond)

20396 of 33745 relevant lines covered (60.44%)

38.95 hits per line

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

32.85
/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 {
23✔
34
        return e.Err.Error()
23✔
35
}
23✔
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) {
79✔
65
        return GetAppContextWithTimeoutDuration(ctx, v, 20)
79✔
66
}
79✔
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) {
79✔
70
        v.SetDefault("cli.context_timeout", tout)
79✔
71
        timeout := v.GetInt("cli.context_timeout")
79✔
72

79✔
73
        //nolint:gosec // See https://github.com/securego/gosec/pull/1585, should be fixed in next gosec
79✔
74
        return context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
79✔
75
}
79✔
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 {
54✔
81
        return func(cmd *cobra.Command, args []string) error {
54✔
82
                if err := viper.BindPFlags(cmd.Flags()); err != nil {
×
83
                        return fmt.Errorf("error binding flags: %s", err)
×
84
                }
×
85

86
                c, err := GrpcForCommand(cmd, viper.GetViper())
×
87
                if err != nil {
×
88
                        return err
×
89
                }
×
90

91
                ctx, cancel := GetAppContext(cmd.Context(), viper.GetViper())
×
92
                defer cancel()
×
93
                defer c.Close()
×
94

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

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

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

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

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

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

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

173
        return first, second
×
174
}
175

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

183
        var result string
×
184
        var lineLength int
×
185

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

202
        return result
×
203
}
204

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

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

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

223
// IsYAMLFileAndNotATest checks if a file is a YAML file and not a test file
224
func IsYAMLFileAndNotATest(path string) bool {
4✔
225
        return (filepath.Ext(path) == ".yaml" || filepath.Ext(path) == ".yml")
4✔
226
}
4✔
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