diff --git a/cmd/dispatch/main.go b/cmd/dispatch/main.go index ec84c09..95886e5 100644 --- a/cmd/dispatch/main.go +++ b/cmd/dispatch/main.go @@ -126,7 +126,7 @@ Commands: completion Print shell completion (bash, zsh, fish, powershell) doctor [--json] Print environment diagnostics (--json for machine-readable output) stats [flags] Print session totals and breakdowns - search [query] [flags] Print matching sessions as JSON, CSV, IDs, paths, or a table + search [query] [flags] Print matching sessions as JSON, CSV, IDs, paths, commands, or a table tags [--json] List tags in use with per-tag session counts aliases [--json] List session aliases with orphan detection alias Set, reassign, clear (--clear), or remove (--remove) a session alias @@ -181,8 +181,9 @@ Search flags: --json Print results as JSON (default) --ids Print one session ID per line --paths Print one working directory per line + --commands Print one resume command per line --table Print a readable table - --format json|csv|ids|paths|table + --format json|csv|ids|paths|commands|table Choose the output format --query Text to match (also accepted as a positional argument) --deep Search turns, checkpoints, files, and refs too diff --git a/cmd/dispatch/search.go b/cmd/dispatch/search.go index 8e05c4d..4b1c6df 100644 --- a/cmd/dispatch/search.go +++ b/cmd/dispatch/search.go @@ -10,6 +10,7 @@ import ( "strings" "text/tabwriter" + "github.com/jongio/dispatch/internal/config" "github.com/jongio/dispatch/internal/data" ) @@ -29,11 +30,12 @@ const searchAllLimit = 100_000 type searchOutputFormat string const ( - searchFormatJSON searchOutputFormat = "json" - searchFormatIDs searchOutputFormat = "ids" - searchFormatTable searchOutputFormat = "table" - searchFormatCSV searchOutputFormat = "csv" - searchFormatPaths searchOutputFormat = "paths" + searchFormatJSON searchOutputFormat = "json" + searchFormatIDs searchOutputFormat = "ids" + searchFormatTable searchOutputFormat = "table" + searchFormatCSV searchOutputFormat = "csv" + searchFormatPaths searchOutputFormat = "paths" + searchFormatCommands searchOutputFormat = "commands" ) // searchOptions holds the parsed flags for the search command. @@ -98,6 +100,13 @@ func runSearch(w io.Writer, args []string) error { if opts.format == searchFormatPaths { return writeSearchPaths(w, sessions) } + if opts.format == searchFormatCommands { + cfg, err := configLoadFn() + if err != nil { + return fmt.Errorf("loading config: %w", err) + } + return writeSearchCommands(w, sessions, cfg) + } results := make([]searchSession, 0, len(sessions)) for _, s := range sessions { @@ -192,6 +201,19 @@ func writeSearchPaths(w io.Writer, sessions []data.Session) error { return nil } +func writeSearchCommands(w io.Writer, sessions []data.Session, cfg *config.Config) error { + for _, s := range sessions { + cmdStr, err := openResumeCmdFn(s.ID, openResumeConfig(cfg, &s)) + if err != nil { + return err + } + if _, err := fmt.Fprintln(w, cmdStr); err != nil { + return err + } + } + return nil +} + func shortSearchID(id string) string { if len(id) <= 12 { return id @@ -259,6 +281,8 @@ func parseSearchArgs(args []string) (searchOptions, error) { opts.format = searchFormatCSV case name == "--paths": opts.format = searchFormatPaths + case name == "--commands": + opts.format = searchFormatCommands case name == "--format": v, ni, err := takeValue(i, "--format", inlineOrEmpty(inline, hasInline)) if err != nil { @@ -431,8 +455,10 @@ func parseSearchFormat(v string) (searchOutputFormat, error) { return searchFormatCSV, nil case string(searchFormatPaths): return searchFormatPaths, nil + case string(searchFormatCommands): + return searchFormatCommands, nil default: - return "", fmt.Errorf("invalid --format value %q (want json, ids, table, csv, or paths)", v) + return "", fmt.Errorf("invalid --format value %q (want json, ids, table, csv, paths, or commands)", v) } } diff --git a/cmd/dispatch/search_test.go b/cmd/dispatch/search_test.go index 28f9697..e6e73bf 100644 --- a/cmd/dispatch/search_test.go +++ b/cmd/dispatch/search_test.go @@ -10,6 +10,7 @@ import ( "github.com/jongio/dispatch/internal/config" "github.com/jongio/dispatch/internal/data" + "github.com/jongio/dispatch/internal/platform" ) // withSearchList swaps the search command's session loader for a test double @@ -116,6 +117,9 @@ func TestParseSearchArgsIDFormats(t *testing.T) { {name: "paths shortcut", args: []string{"search", "--paths"}}, {name: "format paths separate", args: []string{"search", "--format", "paths"}}, {name: "format paths inline", args: []string{"search", "--format=paths"}}, + {name: "commands shortcut", args: []string{"search", "--commands"}}, + {name: "format commands separate", args: []string{"search", "--format", "commands"}}, + {name: "format commands inline", args: []string{"search", "--format=commands"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -133,6 +137,9 @@ func TestParseSearchArgsIDFormats(t *testing.T) { if strings.Contains(strings.Join(tc.args, " "), "paths") { want = searchFormatPaths } + if strings.Contains(strings.Join(tc.args, " "), "commands") { + want = searchFormatCommands + } if opts.format != want { t.Errorf("format = %q, want %s", opts.format, want) } @@ -387,6 +394,41 @@ func TestRunSearchPathsOutput(t *testing.T) { } } +func TestRunSearchCommandsOutput(t *testing.T) { + sessions := []data.Session{ + {ID: "session-a", Cwd: "/code/app"}, + {ID: "session-b", Cwd: "/code/other"}, + } + withSearchList(t, func(data.FilterOptions, data.SortOptions, int) ([]data.Session, error) { + return sessions, nil + }) + prevConfig := configLoadFn + configLoadFn = func() (*config.Config, error) { + cfg := config.Default() + cfg.Model = "gpt-test" + return cfg, nil + } + prevResume := openResumeCmdFn + openResumeCmdFn = func(id string, rc platform.ResumeConfig) (string, error) { + return "copilot --resume " + id + " --cwd " + rc.Cwd + " --model " + rc.Model, nil + } + t.Cleanup(func() { + configLoadFn = prevConfig + openResumeCmdFn = prevResume + }) + + var buf bytes.Buffer + if err := runSearch(&buf, []string{"search", "--commands"}); err != nil { + t.Fatalf("runSearch returned error: %v", err) + } + + want := "copilot --resume session-a --cwd /code/app --model gpt-test\n" + + "copilot --resume session-b --cwd /code/other --model gpt-test\n" + if got := buf.String(); got != want { + t.Errorf("output = %q, want %q", got, want) + } +} + func TestRunSearchEmptyIsEmptyArray(t *testing.T) { withSearchList(t, func(data.FilterOptions, data.SortOptions, int) ([]data.Session, error) { return nil, nil