| 122 | } |
| 123 | |
| 124 | func isUserError(err error) bool { |
| 125 | if err == nil { |
| 126 | return false |
| 127 | } |
| 128 | if clierr.IsUserFacing(err) { |
| 129 | return true |
| 130 | } |
| 131 | if errors.Is(err, utils.ErrSSHUnreachable) { |
| 132 | return true |
| 133 | } |
| 134 | // A persistent auth failure that survived key regeneration means the |
| 135 | // instance's sshd genuinely won't serve a freshly added key — a broken-node |
| 136 | // condition the user resolves by recreating the instance, not a CLI bug. |
| 137 | if errors.Is(err, utils.ErrPersistentAuthFailure) { |
| 138 | return true |
| 139 | } |
| 140 | // Cached SSH key exists but the OS denied the read (Windows NTFS ACLs, |
| 141 | // antivirus). A local environment issue on the user's machine, not a CLI bug. |
| 142 | if errors.Is(err, utils.ErrKeyUnreadable) { |
| 143 | return true |
| 144 | } |
| 145 | var notExistErr *pflag.NotExistError |
| 146 | if errors.As(err, ¬ExistErr) { |
| 147 | return true |
| 148 | } |
| 149 | var valueReqErr *pflag.ValueRequiredError |
| 150 | if errors.As(err, &valueReqErr) { |
| 151 | return true |
| 152 | } |
| 153 | var invalidValErr *pflag.InvalidValueError |
| 154 | if errors.As(err, &invalidValErr) { |
| 155 | return true |
| 156 | } |
| 157 | var invalidSyntaxErr *pflag.InvalidSyntaxError |
| 158 | if errors.As(err, &invalidSyntaxErr) { |
| 159 | return true |
| 160 | } |
| 161 | // Transport-level HTTP failures (DNS, conn refused, TLS, EOF, reset): |
| 162 | // classified at the api.Client boundary. Never a CLI bug. |
| 163 | if errors.Is(err, api.ErrTransport) { |
| 164 | return true |
| 165 | } |
| 166 | if isCancellationOrTimeout(err) { |
| 167 | return true |
| 168 | } |
| 169 | // API 4xx responses are always user errors (auth, validation, not found, etc.). |
| 170 | // Server errors (5xx) are captured separately in doRequest. |
| 171 | var apiErr *api.APIError |
| 172 | if errors.As(err, &apiErr) { |
| 173 | return true |
| 174 | } |
| 175 | msg := err.Error() |
| 176 | for _, sub := range userErrorSubstrings { |
| 177 | if strings.Contains(msg, sub) { |
| 178 | return true |
| 179 | } |
| 180 | } |
| 181 | return false |