diff --git a/README.md b/README.md index eaec257..169ab73 100644 --- a/README.md +++ b/README.md @@ -606,7 +606,7 @@ Add custom color schemes using Windows Terminal JSON format in the `schemes` arr | Flag | Description | |---|---| | `--help`, `-h`, `help` | Show usage information | -| `--version`, `-v`, `version` | Print the version and exit | +| `--version`, `-v`, `version` | Print the version and exit. Add `--json` for script output | | `update` | Update dispatch to the latest release | | `--demo` | Load a demo database with synthetic sessions | | `--reindex` | Full chronicle reindex via Copilot CLI (falls back to FTS5 rebuild) | diff --git a/cmd/dispatch/cli.go b/cmd/dispatch/cli.go index 813dfa9..efbbf69 100644 --- a/cmd/dispatch/cli.go +++ b/cmd/dispatch/cli.go @@ -46,6 +46,39 @@ var ( doctorSessionCountFn = defaultSessionCount ) +type versionOutput struct { + Version string `json:"version"` +} + +func runVersion(w io.Writer, args []string) error { + if w == nil { + w = io.Discard + } + + jsonOut := false + rest := args + if len(rest) > 0 { + rest = rest[1:] + } + for _, arg := range rest { + switch arg { + case "--json": + jsonOut = true + default: + if strings.HasPrefix(arg, "-") { + return fmt.Errorf("unknown flag: %s", arg) + } + return fmt.Errorf("version does not take positional arguments, got %q", arg) + } + } + + if jsonOut { + return json.NewEncoder(w).Encode(versionOutput{Version: version.Version}) + } + _, err := fmt.Fprintln(w, version.Version) + return err +} + func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.UpdateInfo) (done bool, cleanup func(), startup startupOptions, err error) { var flags startupFlags for i := 0; i < len(args); i++ { @@ -57,7 +90,10 @@ func handleArgs(args []string, origStderr io.Writer, updateCh <-chan *update.Upd return true, cleanup, startupOptions{}, nil case "--version", "-v", "version": - fmt.Println(version.Version) + if vErr := runVersion(os.Stdout, args[i:]); vErr != nil { + fmt.Fprintf(os.Stderr, "version: %v\n", vErr) + return true, cleanup, startupOptions{}, vErr + } showUpdateNotification(origStderr, updateCh) return true, cleanup, startupOptions{}, nil diff --git a/cmd/dispatch/cli_test.go b/cmd/dispatch/cli_test.go index 50735af..fb1c391 100644 --- a/cmd/dispatch/cli_test.go +++ b/cmd/dispatch/cli_test.go @@ -85,6 +85,47 @@ func TestHandleArgs_VersionCommand(t *testing.T) { } } +func TestRunVersionJSON(t *testing.T) { + for _, args := range [][]string{ + {"version", "--json"}, + {"--version", "--json"}, + } { + var buf bytes.Buffer + if err := runVersion(&buf, args); err != nil { + t.Fatalf("runVersion(%v): %v", args, err) + } + var out versionOutput + if err := json.Unmarshal(buf.Bytes(), &out); err != nil { + t.Fatalf("output is not valid JSON: %v\n%s", err, buf.String()) + } + if out.Version == "" { + t.Errorf("version should be set for args %v", args) + } + } +} + +func TestRunVersionPlainText(t *testing.T) { + var buf bytes.Buffer + if err := runVersion(&buf, []string{"version"}); err != nil { + t.Fatalf("runVersion: %v", err) + } + if got := strings.TrimSpace(buf.String()); got == "" || strings.HasPrefix(got, "{") { + t.Errorf("plain version output = %q", got) + } +} + +func TestRunVersionRejectsExtraArgs(t *testing.T) { + cases := [][]string{ + {"version", "--yaml"}, + {"version", "extra"}, + } + for _, args := range cases { + if err := runVersion(io.Discard, args); err == nil { + t.Errorf("runVersion(%v) returned nil error, want error", args) + } + } +} + func TestRunCompletion_SupportedShells(t *testing.T) { for _, tc := range []struct { shell string diff --git a/cmd/dispatch/main.go b/cmd/dispatch/main.go index e6833e7..5e4f2e8 100644 --- a/cmd/dispatch/main.go +++ b/cmd/dispatch/main.go @@ -114,7 +114,7 @@ Usage: Commands: help Show this help message - version Print the version + version [--json] Print the version open [--mode M] Resume a session by ID (M: inplace, tab, window, pane) --print writes the resume command instead of launching open --last [--mode M] Resume the most recently active session diff --git a/cmd/dispatch/main_test.go b/cmd/dispatch/main_test.go index 08e19a2..c76a10e 100644 --- a/cmd/dispatch/main_test.go +++ b/cmd/dispatch/main_test.go @@ -457,13 +457,18 @@ func TestPrintUsage_Output(t *testing.T) { origStdout := os.Stdout os.Stdout = w + var buf bytes.Buffer + readDone := make(chan struct{}) + go func() { + _, _ = io.Copy(&buf, r) + close(readDone) + }() + printUsage() w.Close() os.Stdout = origStdout - - var buf bytes.Buffer - _, _ = io.Copy(&buf, r) + <-readDone output := buf.String() for _, want := range []string{"dispatch", "help", "version", "update", "--demo"} {