detached 23e3a70

fix(cli): normalize error prefix

commit
23e3a7075804f42a957f9d5ab3eaed8c425cdcf1
author
brice · · 2026-07-18 13:56 UTC
parent
15085f3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +72 -1
M main.go +13 -1
@@ -3,13 +3,25 @@ package main
33 import (
44 "fmt"
55 "os"
6+ "strings"
67
78 "github.com/bnema/vev/internal/app"
89 )
910
1011 func main() {
1112 if err := app.Run(os.Args[1:]); err != nil {
12- fmt.Fprintln(os.Stderr, "vev:", err)
13+ fmt.Fprintln(os.Stderr, cliError(err))
1314 os.Exit(1)
1415 }
1516 }
17+
18+// cliError adds the sole user-facing CLI prefix. Errors may already include
19+// it when they originate from a runtime component, so remove any such prefix
20+// before rendering.
21+func cliError(err error) string {
22+ message := err.Error()
23+ for strings.HasPrefix(message, "vev:") {
24+ message = strings.TrimSpace(strings.TrimPrefix(message, "vev:"))
25+ }
26+ return "vev: " + message
27+}
@@ -3,13 +3,25 @@ package main
3 import (3 import (
4 "fmt"4 "fmt"
5 "os"5 "os"
6+ "strings"
6 7
7 "github.com/bnema/vev/internal/app"8 "github.com/bnema/vev/internal/app"
8 )9 )
9 10
10 func main() {11 func main() {
11 if err := app.Run(os.Args[1:]); err != nil {12 if err := app.Run(os.Args[1:]); err != nil {
12- fmt.Fprintln(os.Stderr, "vev:", err)13+ fmt.Fprintln(os.Stderr, cliError(err))
13 os.Exit(1)14 os.Exit(1)
14 }15 }
15 }16 }
17+
18+// cliError adds the sole user-facing CLI prefix. Errors may already include
19+// it when they originate from a runtime component, so remove any such prefix
20+// before rendering.
21+func cliError(err error) string {
22+ message := err.Error()
23+ for strings.HasPrefix(message, "vev:") {
24+ message = strings.TrimSpace(strings.TrimPrefix(message, "vev:"))
25+ }
26+ return "vev: " + message
27+}
A main_test.go +59 -0
@@ -0,0 +1,59 @@
1+package main
2+
3+import (
4+ "errors"
5+ "testing"
6+
7+ "github.com/bnema/vev/internal/app"
8+)
9+
10+func TestCLIError(t *testing.T) {
11+ tests := []struct {
12+ name string
13+ err error
14+ want string
15+ }{
16+ {
17+ name: "usage error",
18+ err: runError([]string{"unknown"}),
19+ want: `vev: unknown command "unknown"`,
20+ },
21+ {
22+ name: "domain validation error",
23+ err: runError([]string{"new", "invalid name"}),
24+ want: "vev: invalid session name: must match [A-Za-z0-9][A-Za-z0-9._-]{0,63}",
25+ },
26+ {
27+ name: "daemon error",
28+ err: errors.New("vev: daemon listen: address already in use"),
29+ want: "vev: daemon listen: address already in use",
30+ },
31+ {
32+ name: "session error",
33+ err: errors.New("vev: no such session: work"),
34+ want: "vev: no such session: work",
35+ },
36+ {
37+ name: "repeated prefix",
38+ err: errors.New("vev: vev: message"),
39+ want: "vev: message",
40+ },
41+ {
42+ name: "prefix whitespace",
43+ err: errors.New("vev:\t message"),
44+ want: "vev: message",
45+ },
46+ }
47+
48+ for _, tt := range tests {
49+ t.Run(tt.name, func(t *testing.T) {
50+ if got := cliError(tt.err); got != tt.want {
51+ t.Errorf("cliError(%v) = %q, want %q", tt.err, got, tt.want)
52+ }
53+ })
54+ }
55+}
56+
57+func runError(args []string) error {
58+ return app.Run(args)
59+}
@@ -0,0 +1,59 @@
1+package main
2+
3+import (
4+ "errors"
5+ "testing"
6+
7+ "github.com/bnema/vev/internal/app"
8+)
9+
10+func TestCLIError(t *testing.T) {
11+ tests := []struct {
12+ name string
13+ err error
14+ want string
15+ }{
16+ {
17+ name: "usage error",
18+ err: runError([]string{"unknown"}),
19+ want: `vev: unknown command "unknown"`,
20+ },
21+ {
22+ name: "domain validation error",
23+ err: runError([]string{"new", "invalid name"}),
24+ want: "vev: invalid session name: must match [A-Za-z0-9][A-Za-z0-9._-]{0,63}",
25+ },
26+ {
27+ name: "daemon error",
28+ err: errors.New("vev: daemon listen: address already in use"),
29+ want: "vev: daemon listen: address already in use",
30+ },
31+ {
32+ name: "session error",
33+ err: errors.New("vev: no such session: work"),
34+ want: "vev: no such session: work",
35+ },
36+ {
37+ name: "repeated prefix",
38+ err: errors.New("vev: vev: message"),
39+ want: "vev: message",
40+ },
41+ {
42+ name: "prefix whitespace",
43+ err: errors.New("vev:\t message"),
44+ want: "vev: message",
45+ },
46+ }
47+
48+ for _, tt := range tests {
49+ t.Run(tt.name, func(t *testing.T) {
50+ if got := cliError(tt.err); got != tt.want {
51+ t.Errorf("cliError(%v) = %q, want %q", tt.err, got, tt.want)
52+ }
53+ })
54+ }
55+}
56+
57+func runError(args []string) error {
58+ return app.Run(args)
59+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close