fix(cli): reject conflicting self target
- commit
- 1b18dd94f7c909c763b3bc0dcd0e1707d8179756
- author
- brice · · 2026-07-24 05:04 UTC
- parent
- f3efd5c
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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" | |||
| 50 | 50 | 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 | 55 | ## Keys | |
| 56 | 56 | ||
| @@ -50,7 +50,7 @@ vev cmd toast -l warn "build failed" | |||||
| 50 | vev cmd list-panes --json | 50 | 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 | ## Keys | 55 | ## Keys | ||
| 56 | 56 | ||||
M internal/app/cmd.go +4 -1
| @@ -80,6 +80,9 @@ func parseCmdArgs(args []string) (cmdInvocation, error) { | |||
| 80 | 80 | invocation.help = true | |
| 81 | 81 | return invocation, nil | |
| 82 | 82 | } | |
| 83 | + | if invocation.self && invocation.session != "" { | |
| 84 | + | return cmdInvocation{}, usagef("`cmd --self` cannot be combined with `-s`") | |
| 85 | + | } | |
| 83 | 86 | cmd, ok := commandusecase.BySlug(invocation.slug) | |
| 84 | 87 | if !ok || !cmd.Scriptable { | |
| 85 | 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 | 159 | transport, err := deps.dial(ctx, ipc.SocketDir()) | |
| 157 | 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 | 163 | defer func() { _ = transport.Close() }() | |
| 161 | 164 | ||
| @@ -80,6 +80,9 @@ func parseCmdArgs(args []string) (cmdInvocation, error) { | |||||
| 80 | invocation.help = true | 80 | invocation.help = true | ||
| 81 | return invocation, nil | 81 | 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) { | |||
| 25 | 25 | {"session flag", []string{"cmd", "-s", "dev", "new-tab"}, cmdInvocation{slug: "new-tab", session: "dev"}, false}, | |
| 26 | 26 | {"json flag", []string{"cmd", "list-panes", "--json"}, cmdInvocation{slug: "list-panes", jsonOut: true}, false}, | |
| 27 | 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 | 29 | {"passthrough args", []string{"cmd", "toast", "-l", "warn", "hello"}, cmdInvocation{slug: "toast", args: []string{"-l", "warn", "hello"}}, false}, | |
| 29 | 30 | {"unknown slug", []string{"cmd", "frobnicate"}, cmdInvocation{}, true}, | |
| 30 | 31 | {"non-scriptable slug", []string{"cmd", "detach"}, cmdInvocation{}, true}, | |
| @@ -145,17 +146,21 @@ func TestRunCmdExplicitSessionDoesNotUseEnvIDsWithoutSelf(t *testing.T) { | |||
| 145 | 146 | ||
| 146 | 147 | func TestRunCmdDoesNotAutostartAndClassifiesDialFailure(t *testing.T) { | |
| 147 | 148 | dials := 0 | |
| 149 | + | dialErr := errors.New("unreachable") | |
| 148 | 150 | err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{ | |
| 149 | 151 | stdout: io.Discard, | |
| 150 | 152 | getenv: func(string) string { return "" }, | |
| 151 | 153 | dial: func(context.Context, string) (ports.Transport, error) { | |
| 152 | 154 | dials++ | |
| 153 | - | return nil, errors.New("unreachable") | |
| 155 | + | return nil, dialErr | |
| 154 | 156 | }, | |
| 155 | 157 | }) | |
| 156 | 158 | if ExitCode(err) != 3 || dials != 1 { | |
| 157 | 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 | 166 | 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 := 0 | 148 | 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) { | ||