Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions SURFACE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2353,24 +2353,17 @@ FLAG fizzy reaction rm --styled type=bool
FLAG fizzy reaction rm --token type=string
FLAG fizzy reaction rm --verbose type=bool
FLAG fizzy search --agent type=bool
FLAG fizzy search --all type=bool
FLAG fizzy search --api-url type=string
FLAG fizzy search --assignee type=string
FLAG fizzy search --board type=string
FLAG fizzy search --count type=bool
FLAG fizzy search --help type=bool
FLAG fizzy search --ids-only type=bool
FLAG fizzy search --indexed-by type=string
FLAG fizzy search --jq type=string
FLAG fizzy search --json type=bool
FLAG fizzy search --limit type=int
FLAG fizzy search --markdown type=bool
FLAG fizzy search --page type=int
FLAG fizzy search --profile type=string
FLAG fizzy search --quiet type=bool
FLAG fizzy search --sort type=string
FLAG fizzy search --styled type=bool
FLAG fizzy search --tag type=string
FLAG fizzy search --token type=string
FLAG fizzy search --verbose type=bool
FLAG fizzy setup --agent type=bool
Expand Down
12 changes: 10 additions & 2 deletions e2e/cli_tests/search_and_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,17 @@ import (

func TestSearch(t *testing.T) {
h := newHarness(t)
// Single-token query
assertOK(t, h.Run("search", "test"))
assertOK(t, h.Run("search", "test", "--board", fixture.BoardID))
assertOK(t, h.Run("search", "test", "--all"))
// Multi-arg joined into a single q string
assertOK(t, h.Run("search", "login", "error"))
}

func TestCardListWithSearch(t *testing.T) {
// Filter use cases that used to live on `fizzy search` now belong here.
h := newHarness(t)
assertOK(t, h.Run("card", "list", "--search", "test", "--board", fixture.BoardID))
assertOK(t, h.Run("card", "list", "--search", "test", "--all"))
}

func TestAuthInvalidToken(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion internal/commands/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ var commandExamples = map[string]string{
"fizzy config show": "$ fizzy config show\n$ fizzy config show --verbose",
"fizzy config explain": "$ fizzy config explain\n$ fizzy config explain --profile acme",
"fizzy doctor": "$ fizzy doctor\n$ fizzy doctor --profile acme\n$ fizzy doctor --all-profiles",
"fizzy search": "$ fizzy search \"billing bug\"\n$ fizzy search \"billing bug\" --board <id>",
"fizzy search": "$ fizzy search \"billing bug\"\n$ fizzy search <card-id>",
"fizzy notification": "$ fizzy notification tray\n$ fizzy notification list",
"fizzy notification tray": "$ fizzy notification tray",
"fizzy user": "$ fizzy user list\n$ fizzy user show <id>",
Expand Down
101 changes: 16 additions & 85 deletions internal/commands/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,118 +2,49 @@ package commands

import (
"fmt"
"strconv"
"strings"

"github.com/spf13/cobra"
)

// Search flags
var searchBoard string
var searchTag string
var searchAssignee string
var searchIndexedBy string
var searchSort string
var searchPage int
var searchAll bool

var searchCmd = &cobra.Command{
Use: "search QUERY",
Use: "search QUERY...",
Short: "Search cards",
Long: "Searches cards by text. Multiple words are treated as separate terms (AND).",
Args: cobra.MinimumNArgs(1),
Long: `Searches cards using the dedicated full-text search endpoint.

The query is sent as a single string. If the query exactly matches a card ID,
that card is returned directly.

To filter cards by structured criteria (board, tag, assignee, status, etc.),
use 'fizzy card list' with --search and the relevant filter flags.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if err := requireAuthAndAccount(); err != nil {
return err
}
if err := checkLimitAll(searchAll); err != nil {
return err
}

query := strings.Join(args, " ")

ac := getSDK()
path := "/cards.json"

var params []string

// Add search terms
for term := range strings.FieldsSeq(query) {
params = append(params, "terms[]="+term)
raw, _, err := ac.Search().Search(cmd.Context(), &query)
if err != nil {
return convertSDKError(err)
}

// Add optional filters (search is cross-board by default;
// only scope to a board when explicitly requested via --board)
if searchBoard != "" {
params = append(params, "board_ids[]="+searchBoard)
}
if searchTag != "" {
params = append(params, "tag_ids[]="+searchTag)
}
if searchAssignee != "" {
params = append(params, "assignee_ids[]="+searchAssignee)
}
if searchIndexedBy != "" {
params = append(params, "indexed_by="+searchIndexedBy)
}
if searchSort != "" {
params = append(params, "sorted_by="+searchSort)
}
if searchPage > 0 {
params = append(params, "page="+strconv.Itoa(searchPage))
}

if len(params) > 0 {
path += "?" + strings.Join(params, "&")
}

var items any
var linkNext string

if searchAll {
pages, err := ac.GetAll(cmd.Context(), path)
if err != nil {
return convertSDKError(err)
}
items = jsonAnySlice(pages)
} else {
data, resp, err := ac.Cards().List(cmd.Context(), path)
if err != nil {
return convertSDKError(err)
}
items = normalizeAny(data)
linkNext = parseSDKLinkNext(resp)
}

// Build summary
items := normalizeAny(raw)
count := dataCount(items)
summary := fmt.Sprintf("%d results for \"%s\"", count, query)
if searchAll {
summary = fmt.Sprintf("%d results for \"%s\" (all)", count, query)
} else if searchPage > 0 {
summary = fmt.Sprintf("%d results for \"%s\" (page %d)", count, query, searchPage)
}
summary := fmt.Sprintf("%d results for %q", count, query)

// Build breadcrumbs
breadcrumbs := []Breadcrumb{
breadcrumb("show", "fizzy card show <number>", "View card details"),
breadcrumb("narrow", fmt.Sprintf("fizzy search \"%s\" --board <id>", query), "Filter by board"),
breadcrumb("filter", fmt.Sprintf("fizzy card list --search %q --board <id>", query), "Filter cards by criteria"),
}

hasNext := linkNext != ""
printListPaginated(items, searchColumns, hasNext, linkNext, searchAll, summary, breadcrumbs)
printList(items, searchColumns, summary, breadcrumbs)
return nil
},
}

func init() {
rootCmd.AddCommand(searchCmd)

searchCmd.Flags().StringVar(&searchBoard, "board", "", "Filter by board ID")
searchCmd.Flags().StringVar(&searchTag, "tag", "", "Filter by tag ID")
searchCmd.Flags().StringVar(&searchAssignee, "assignee", "", "Filter by assignee ID")
searchCmd.Flags().StringVar(&searchIndexedBy, "indexed-by", "", "Filter by status (all, closed, maybe, not_now, golden)")
searchCmd.Flags().StringVar(&searchSort, "sort", "", "Sort order: newest, oldest, or latest (default)")
searchCmd.Flags().IntVar(&searchPage, "page", 0, "Page number")
searchCmd.Flags().BoolVar(&searchAll, "all", false, "Fetch all pages")
}
Loading
Loading