detached 1b18dd9

fix(cli): reject conflicting self target

commit
1b18dd94f7c909c763b3bc0dcd0e1707d8179756
author
brice · · 2026-07-24 05:04 UTC
parent
f3efd5c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +11 -3
M README.md +1 -1
@@ -50,7 +50,7 @@ vev cmd toast -l warn "build failed"
5050 vev cmd list-panes --json
5151 ```
5252
53-Target a session explicitly with `-s` (`vev cmd -s work new-tab`). Otherwise vev uses `$VEV` inside a pane, then the only live session; ambiguous targets fail. Run `vev cmd --help` for the scriptable command list and `vev cmd <command> --help` for command usage.
53+Target a session explicitly with `-s` (`vev cmd -s work new-tab`). Inside a pane, `--self` targets that pane; it cannot be combined with `-s`. Otherwise vev uses `$VEV` inside a pane, then the only live session; ambiguous targets fail. Run `vev cmd --help` for the scriptable command list and `vev cmd <command> --help` for command usage.
5454
5555 ## Keys
5656
@@ -50,7 +50,7 @@ vev cmd toast -l warn "build failed"
50 vev cmd list-panes --json50 vev cmd list-panes --json
51 ```51 ```
52 52
53-Target a session explicitly with `-s` (`vev cmd -s work new-tab`). Otherwise vev uses `$VEV` inside a pane, then the only live session; ambiguous targets fail. Run `vev cmd --help` for the scriptable command list and `vev cmd <command> --help` for command usage.53+Target a session explicitly with `-s` (`vev cmd -s work new-tab`). Inside a pane, `--self` targets that pane; it cannot be combined with `-s`. Otherwise vev uses `$VEV` inside a pane, then the only live session; ambiguous targets fail. Run `vev cmd --help` for the scriptable command list and `vev cmd <command> --help` for command usage.
54 54
55 ## Keys55 ## Keys
56 56
M internal/app/cmd.go +4 -1
@@ -80,6 +80,9 @@ func parseCmdArgs(args []string) (cmdInvocation, error) {
8080 invocation.help = true
8181 return invocation, nil
8282 }
83+ if invocation.self && invocation.session != "" {
84+ return cmdInvocation{}, usagef("`cmd --self` cannot be combined with `-s`")
85+ }
8386 cmd, ok := commandusecase.BySlug(invocation.slug)
8487 if !ok || !cmd.Scriptable {
8588 return cmdInvocation{}, usagef("unknown command %q; see `vev cmd --help`", invocation.slug)
@@ -155,7 +158,7 @@ func runCmdWithDeps(ctx context.Context, invocation cmdInvocation, deps cmdDeps)
155158
156159 transport, err := deps.dial(ctx, ipc.SocketDir())
157160 if err != nil {
158- return &exitCoded{code: 3, err: fmt.Errorf("%w: %v", errDaemonUnreachable, err)}
161+ return &exitCoded{code: 3, err: fmt.Errorf("%w: %w", errDaemonUnreachable, err)}
159162 }
160163 defer func() { _ = transport.Close() }()
161164
@@ -80,6 +80,9 @@ func parseCmdArgs(args []string) (cmdInvocation, error) {
80 invocation.help = true80 invocation.help = true
81 return invocation, nil81 return invocation, nil
82 }82 }
83+ if invocation.self && invocation.session != "" {
84+ return cmdInvocation{}, usagef("`cmd --self` cannot be combined with `-s`")
85+ }
83 cmd, ok := commandusecase.BySlug(invocation.slug)86 cmd, ok := commandusecase.BySlug(invocation.slug)
84 if !ok || !cmd.Scriptable {87 if !ok || !cmd.Scriptable {
85 return cmdInvocation{}, usagef("unknown command %q; see `vev cmd --help`", invocation.slug)88 return cmdInvocation{}, usagef("unknown command %q; see `vev cmd --help`", invocation.slug)
@@ -155,7 +158,7 @@ func runCmdWithDeps(ctx context.Context, invocation cmdInvocation, deps cmdDeps)
155 158
156 transport, err := deps.dial(ctx, ipc.SocketDir())159 transport, err := deps.dial(ctx, ipc.SocketDir())
157 if err != nil {160 if err != nil {
158- return &exitCoded{code: 3, err: fmt.Errorf("%w: %v", errDaemonUnreachable, err)}161+ return &exitCoded{code: 3, err: fmt.Errorf("%w: %w", errDaemonUnreachable, err)}
159 }162 }
160 defer func() { _ = transport.Close() }()163 defer func() { _ = transport.Close() }()
161 164
M internal/app/cmd_test.go +6 -1
@@ -25,6 +25,7 @@ func TestParseCmdArgs(t *testing.T) {
2525 {"session flag", []string{"cmd", "-s", "dev", "new-tab"}, cmdInvocation{slug: "new-tab", session: "dev"}, false},
2626 {"json flag", []string{"cmd", "list-panes", "--json"}, cmdInvocation{slug: "list-panes", jsonOut: true}, false},
2727 {"self flag", []string{"cmd", "--self", "split-right"}, cmdInvocation{slug: "split-right", self: true}, false},
28+ {"self and session conflict", []string{"cmd", "-s", "other", "--self", "split-right"}, cmdInvocation{}, true},
2829 {"passthrough args", []string{"cmd", "toast", "-l", "warn", "hello"}, cmdInvocation{slug: "toast", args: []string{"-l", "warn", "hello"}}, false},
2930 {"unknown slug", []string{"cmd", "frobnicate"}, cmdInvocation{}, true},
3031 {"non-scriptable slug", []string{"cmd", "detach"}, cmdInvocation{}, true},
@@ -145,17 +146,21 @@ func TestRunCmdExplicitSessionDoesNotUseEnvIDsWithoutSelf(t *testing.T) {
145146
146147 func TestRunCmdDoesNotAutostartAndClassifiesDialFailure(t *testing.T) {
147148 dials := 0
149+ dialErr := errors.New("unreachable")
148150 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{
149151 stdout: io.Discard,
150152 getenv: func(string) string { return "" },
151153 dial: func(context.Context, string) (ports.Transport, error) {
152154 dials++
153- return nil, errors.New("unreachable")
155+ return nil, dialErr
154156 },
155157 })
156158 if ExitCode(err) != 3 || dials != 1 {
157159 t.Fatalf("error=%v code=%d dials=%d, want unreachable/3 and one dial", err, ExitCode(err), dials)
158160 }
161+ if !errors.Is(err, dialErr) {
162+ t.Fatalf("error=%v does not wrap dial error", err)
163+ }
159164 }
160165
161166 func TestRunCmdDaemonErrorHasExitOne(t *testing.T) {
@@ -25,6 +25,7 @@ func TestParseCmdArgs(t *testing.T) {
25 {"session flag", []string{"cmd", "-s", "dev", "new-tab"}, cmdInvocation{slug: "new-tab", session: "dev"}, false},25 {"session flag", []string{"cmd", "-s", "dev", "new-tab"}, cmdInvocation{slug: "new-tab", session: "dev"}, false},
26 {"json flag", []string{"cmd", "list-panes", "--json"}, cmdInvocation{slug: "list-panes", jsonOut: true}, false},26 {"json flag", []string{"cmd", "list-panes", "--json"}, cmdInvocation{slug: "list-panes", jsonOut: true}, false},
27 {"self flag", []string{"cmd", "--self", "split-right"}, cmdInvocation{slug: "split-right", self: true}, false},27 {"self flag", []string{"cmd", "--self", "split-right"}, cmdInvocation{slug: "split-right", self: true}, false},
28+ {"self and session conflict", []string{"cmd", "-s", "other", "--self", "split-right"}, cmdInvocation{}, true},
28 {"passthrough args", []string{"cmd", "toast", "-l", "warn", "hello"}, cmdInvocation{slug: "toast", args: []string{"-l", "warn", "hello"}}, false},29 {"passthrough args", []string{"cmd", "toast", "-l", "warn", "hello"}, cmdInvocation{slug: "toast", args: []string{"-l", "warn", "hello"}}, false},
29 {"unknown slug", []string{"cmd", "frobnicate"}, cmdInvocation{}, true},30 {"unknown slug", []string{"cmd", "frobnicate"}, cmdInvocation{}, true},
30 {"non-scriptable slug", []string{"cmd", "detach"}, cmdInvocation{}, true},31 {"non-scriptable slug", []string{"cmd", "detach"}, cmdInvocation{}, true},
@@ -145,17 +146,21 @@ func TestRunCmdExplicitSessionDoesNotUseEnvIDsWithoutSelf(t *testing.T) {
145 146
146 func TestRunCmdDoesNotAutostartAndClassifiesDialFailure(t *testing.T) {147 func TestRunCmdDoesNotAutostartAndClassifiesDialFailure(t *testing.T) {
147 dials := 0148 dials := 0
149+ dialErr := errors.New("unreachable")
148 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{150 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{
149 stdout: io.Discard,151 stdout: io.Discard,
150 getenv: func(string) string { return "" },152 getenv: func(string) string { return "" },
151 dial: func(context.Context, string) (ports.Transport, error) {153 dial: func(context.Context, string) (ports.Transport, error) {
152 dials++154 dials++
153- return nil, errors.New("unreachable")155+ return nil, dialErr
154 },156 },
155 })157 })
156 if ExitCode(err) != 3 || dials != 1 {158 if ExitCode(err) != 3 || dials != 1 {
157 t.Fatalf("error=%v code=%d dials=%d, want unreachable/3 and one dial", err, ExitCode(err), dials)159 t.Fatalf("error=%v code=%d dials=%d, want unreachable/3 and one dial", err, ExitCode(err), dials)
158 }160 }
161+ if !errors.Is(err, dialErr) {
162+ t.Fatalf("error=%v does not wrap dial error", err)
163+ }
159 }164 }
160 165
161 func TestRunCmdDaemonErrorHasExitOne(t *testing.T) {166 func TestRunCmdDaemonErrorHasExitOne(t *testing.T) {

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