Skip to content

feat(search): show search results as a table of versions and platforms - #3041

Merged
mikeland73 merged 1 commit into
jetify-com:mainfrom
kadaan:better-search-results
Nov 9, 2026
Merged

mikeland73 merged 1 commit into
jetify-com:mainfrom
kadaan:better-search-results

Conversation

@kadaan

@kadaan kadaan commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

devbox search <query> now prints a table of each package's versions and the platforms each version runs on. Consecutive versions available on the same platforms share a row:

Found 11+ results for "ripgrep":

┌─────────────┬────────────────────────┬───────────────────────────────┐
│ PACKAGE     │        VERSIONS        │           PLATFORMS           │
├─────────────┼────────────────────────┼───────────────────────────────┤
│ ripgrep     │ 15.2.0                 │ aarch64-darwin  aarch64-linux │
│             │                        │ x86_64-linux                  │
│             ├────────────────────────┼───────────────────────────────┤
│             │ 15.1.0  15.0.0         │ aarch64-darwin                │
│             │ 14.1.1  14.1.0         │ aarch64-linux                 │
│             │ 14.0.3  14.0.2         │ x86_64-darwin                 │
│             │ 14.0.1  13.0.0         │ x86_64-linux                  │
│             ├────────────────────────┼───────────────────────────────┤
│             │ 12.1.1                 │ x86_64-darwin                 │
│             │                        │ x86_64-linux                  │
├─────────────┼────────────────────────┼───────────────────────────────┤
│ ripgrep-all │ 1.0.0-alpha.5  0.10.10 │ aarch64-darwin  aarch64-linux │
│             │ 0.10.9         0.10.6  │ x86_64-darwin   x86_64-linux  │
│             │ 0.9.6                  │                               │
└─────────────┴────────────────────────┴───────────────────────────────┘
For more information go to: https://www.nixsearch.com/

Before, the output was one line per package, * ripgrep (15.2.0, 15.1.0, ... ...), with no way to tell which versions you can install on your machine.

Why /v2/search instead of /v1/search: /v1/search returns per-version platforms, but they don't match what Devbox can actually install. For example, it lists nodejs@24.0.0 on all four systems, but /v1/resolve and /v2/resolve only resolve it on Linux. The /v2 data agrees with the resolve endpoints.

One request per search. The client calls /v2/search?q=<query>&releases=11. Each result then carries its newest releases and their systems, so the CLI can show 10 versions and know there are more. --show-all sends releases=all.

By default the CLI still shows the top 10 packages and 10 versions of each, and prints the existing --show-all hint when it trims.

Fallback for other search services. A service that doesn't return releases still works. The client calls /v2/pkg once for each package it will display, at most 11 by default, several at a time. A package whose lookup fails is left out. The search fails only if every lookup fails with something other than a 404.

Breaking change for DEVBOX_SEARCH_HOST: a self-hosted search service now needs /v2/search, plus /v2/pkg if it doesn't return releases, instead of /v1/search. SEARCH_API.md documents both endpoints, the fields Devbox reads, and how /v2/pkg platforms (arm64/macOS) map to Nix systems (aarch64-darwin).

Open question: piped output. The table replaces the old * name (versions) lines everywhere, including when stdout is a pipe, so scripts that parse the old format will break. If that matters, one option is to print the table only on a terminal and keep the old list when piped (go-isatty is already a dependency). I'm happy to add that, or another format you'd prefer.

Other changes:

  • An empty result list still prints No results found for "<query>" and exits 0.
  • Found <n>+ results still uses the server's total_results.
  • The footer link points at the configured search host.
  • The table uses github.com/jedib0t/go-pretty/v6 v6.8.3, and vendor-hash is updated.

How was it tested?

  • golangci-lint run ./... reports 0 issues.
  • go test ./internal/searcher/ ./internal/boxcli/ passes. The tests cover:
    • internal/searcher:
      • inline releases make no /v2/pkg calls;
      • releases=all is sent when there's no limit;
      • the /v2/pkg fallback, with releases trimmed to the limit and duplicate systems collapsed;
      • a 404 and a 500 on single packages skip them;
      • the search fails when every lookup fails;
      • an empty result list.
    • internal/boxcli:
      • versions grouped by systems;
      • trimming and --show-all;
      • a package with no versions keeps its row;
      • a package whose name equals its version doesn't merge cells;
      • column alignment;
      • the footer uses the configured host.
  • I ran it by hand against https://www.nixsearch.com: devbox search ripgrep, devbox search ripgrep --show-all, and a query with no matches.
  • I ran it with DEVBOX_SEARCH_HOST=https://search.devbox.sh, which ignores releases, to exercise the /v2/pkg fallback. That service finds fewer packages, but the ripgrep rows match the nixsearch.com output exactly.

Community Contribution License

All community contributions in this pull request are licensed to the project
maintainers under the terms of the
Apache 2 License.

By creating this pull request, I represent that I have the right to license the
contributions to the project maintainers under the Apache 2 License as stated in
the
Community Contribution License.

@kadaan

kadaan commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

@mikeland73 Here is the improvement to the search output.

Search against the v2 search API, asking for each result's newest
releases and their systems, so `devbox search` lists versions alongside
the platforms they are available on, grouping consecutive versions that
share the same platforms. It requests 11 releases of the top 11 packages
by default, and every release with --show-all.

Search services that don't return releases fall back to one /v2/pkg
call per displayed package. A package whose lookup fails is left out,
and the search fails only when every lookup does.

Search services set with DEVBOX_SEARCH_HOST now need to implement
/v2/search, and /v2/pkg unless they return releases, instead of
/v1/search.
@kadaan
kadaan force-pushed the better-search-results branch from b6482cb to bff54d5 Compare November 7, 2026 00:41
@mikeland73

Copy link
Copy Markdown
Collaborator

@claude ptal

@claude

claude Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Claude finished @mikeland73's task in 31s —— View job


Review of #3041

I read the diff (internal/searcher/{client,model}.go, internal/boxcli/search.go). I couldn't build or run the tests here because the sandbox denied go build and go test, so this is a read-only review. Overall the change is clean and the tests cover the cases that matter. I found nothing blocking.

Looks good

  • hit.Releases == nil correctly separates "service doesn't support releases" from "package has no releases" ([] decodes to a non-nil slice).
  • Each fallback goroutine writes only to its own errs[n] and found[i], so there's no data race. The loop-variable capture is fine on Go 1.27.
  • lookupPackages returns nil if any lookup succeeded or all failures were 404s, and an error only when every lookup failed with a non-404. That matches the PR description.
  • sortedSystems sorts and de-dupes before slices.Equal is used for grouping, so ordering differences between releases can't split a row.
  • Replacing fmt.Println() with fmt.Fprintln(w) fixes a stray write to stdout instead of the writer. Dropping math.Min(10, float64(...)) for trimmedVersionsLength is a nice cleanup.

Suggestions (non-blocking)

  1. Piped output (your open question). Printing the old * name (versions) list when stdout isn't a TTY, using the go-isatty dependency you mentioned, is cheap and avoids breaking scripts. I'd add it, since the table's box-drawing characters and multi-line cells are hard to parse. If you'd rather keep one format, call the change out in the release notes.
  2. columnize with two items (search.go, columnize). len(data) <= columns collapses to one column, so exactly 2 versions stack vertically while 3 or more go two-wide. That's probably fine, but it's slightly surprising and worth a test or a comment if it's intended.
  3. Partial fallback failures are silent. When some /v2/pkg lookups fail (non-404), those packages are dropped with no hint. A short warning such as "N packages omitted (lookup failed)" would stop users from reading a truncated list as complete. The same applies to Found 11+ results, which comes from total_results and can exceed what's shown.
  4. Fallback fan-out. With --show-all and a service that lacks releases, limit == 0 makes /v2/search return every hit, and then one /v2/pkg call is made per hit (8 at a time). For broad queries that could be hundreds of requests. A cap, or a note in SEARCH_API.md, would help.
  5. Breaking change for DEVBOX_SEARCH_HOST. This is already documented in SEARCH_API.md. I'd also label the PR or commit as breaking so it lands in the release notes.
  6. PackageVersion in model.go. Package.Versions now uses SearchRelease. If PackageVersion has no remaining users, it can be removed.

@greptile-apps

greptile-apps Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5 Tier: plus

[Medium impact] The changes affect the CLI search workflow and search services configured by Devbox users.

The PR appears safe to merge, with non-blocking improvements to trimming hints, response types, and the footer URL.

Fix All in Claude CodeFindings

  1. P2 Hidden results lose their hint ▶
  2. P2 Unused fields can break search ▶
  3. P2 Security Footer exposes URL credentials ▶
Fix with agent prompt
### Issue 1
internal/searcher/client.go:69-70
`Search` cuts matches to 11 before dropping failed lookups. With 12 matches and one failed lookup among those 11, the CLI receives 10 packages. If their version lists are short, it omits the `--show-all` hint despite hiding more matches.

`newPackage` also loses this hint when empty versions consume the extra release slot. Keep truncation information when applying these limits instead of inferring it from the remaining items.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 2
internal/searcher/model.go:50-53
The new response types decode dates and nested outputs that search never uses. An empty timestamp or a changed output shape can reject the whole search response or drop a package, even when its name, versions, and platforms are usable.

Remove unused fields from these search-specific types, especially `LastUpdated`, `Date`, and `Outputs`, so unrelated metadata cannot prevent the table from being shown.

### Issue 3
internal/boxcli/search.go:156
If `DEVBOX_SEARCH_HOST` contains URL credentials, such as `https://user:secret@example.com`, this footer copies the password into stdout. Captured command output and CI logs will then contain it after a successful search. Remove the URL's user info before printing the link.

**How this was verified:** `Client()` reads the environment value unchanged, and `Host()` passes it directly to the footer's `fmt.Fprintf` call.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Changes devbox search to show versions and platforms in a table.

  • Search requests now get release and platform data from the v2 search service.
  • Search results now show versions beside the platforms they run on.

Acknowledged choices:

  • kadaan noted that tables also replace piped output and may break scripts.
  • kadaan documented the new endpoints required by self-hosted services; the v2 data agrees with package resolution.
  • kadaan documented dropping failed lookups and returning an error when every lookup fails.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["Package search"] --> B["GET /v2/search"]
  B --> C{"Releases included?"}
  C -->|Yes| E["Sort and deduplicate systems"]
  C -->|No| D["GET /v2/pkg: up to 8 at once"]
  D --> E
  E --> F["Group versions by platforms"]
  F --> G["Print table and footer"]
Loading

Reviews (1) · Last reviewed commit: "feat(search): show search results as a t..." · Reviewed by Greptile

Comment on lines +69 to +70
if limit > 0 && len(hits) > limit {
hits = hits[:limit]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hidden results lose their hint

Search cuts matches to 11 before dropping failed lookups. With 12 matches and one failed lookup among those 11, the CLI receives 10 packages. If their version lists are short, it omits the --show-all hint despite hiding more matches.

newPackage also loses this hint when empty versions consume the extra release slot. Keep truncation information when applying these limits instead of inferring it from the remaining items.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/searcher/client.go
Line: 69-70

Comment:
**Hidden results lose their hint**

`Search` cuts matches to 11 before dropping failed lookups. With 12 matches and one failed lookup among those 11, the CLI receives 10 packages. If their version lists are short, it omits the `--show-all` hint despite hiding more matches.

`newPackage` also loses this hint when empty versions consume the extra release slot. Keep truncation information when applying these limits instead of inferring it from the remaining items.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Comment on lines +50 to +53
AttributePath string `json:"attribute_path"`
CommitHash string `json:"commit_hash"`
Date time.Time `json:"date"`
Outputs []Output `json:"outputs"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused fields can break search

The new response types decode dates and nested outputs that search never uses. An empty timestamp or a changed output shape can reject the whole search response or drop a package, even when its name, versions, and platforms are usable.

Remove unused fields from these search-specific types, especially LastUpdated, Date, and Outputs, so unrelated metadata cannot prevent the table from being shown.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/searcher/model.go
Line: 50-53

Comment:
**Unused fields can break search**

The new response types decode dates and nested outputs that search never uses. An empty timestamp or a changed output shape can reject the whole search response or drop a package, even when its name, versions, and platforms are usable.

Remove unused fields from these search-specific types, especially `LastUpdated`, `Date`, and `Outputs`, so unrelated metadata cannot prevent the table from being shown.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

Comment thread internal/boxcli/search.go
trimmedVersionsLength,
)
}
fmt.Fprintf(w, "For more information go to: %s/\n", strings.TrimSuffix(host, "/"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security Footer exposes URL credentials

If DEVBOX_SEARCH_HOST contains URL credentials, such as https://user:secret@example.com, this footer copies the password into stdout. Captured command output and CI logs will then contain it after a successful search. Remove the URL's user info before printing the link.

How this was verified: Client() reads the environment value unchanged, and Host() passes it directly to the footer's fmt.Fprintf call.

Prompt To Fix With AI
This is a comment left during a code review.
Path: internal/boxcli/search.go
Line: 156

Comment:
**Footer exposes URL credentials**

If `DEVBOX_SEARCH_HOST` contains URL credentials, such as `https://user:secret@example.com`, this footer copies the password into stdout. Captured command output and CI logs will then contain it after a successful search. Remove the URL's user info before printing the link.

**How this was verified:** `Client()` reads the environment value unchanged, and `Host()` passes it directly to the footer's `fmt.Fprintf` call.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

@mikeland73
mikeland73 merged commit 86aadba into jetify-com:main Nov 9, 2026
29 of 30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants