feat(cli): add search and list commands - #2154
Conversation
ef64fbd to
12c344b
Compare
There was a problem hiding this comment.
🟡 Changes recommended
It introduces a small but real edge-case bug in the new shared truncate helper and some newly added exported APIs/types should be tightened/renamed for clarity and contract accuracy.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds discovery-oriented CLI capabilities to complement ovsx show by introducing ovsx search (paged full-text search via /api/-/search) and ovsx list (namespace inventory via /api/{namespace}), while consolidating shared tabular formatting helpers into a reusable module.
Changes:
- Add
ovsx searchcommand with local validation for sort keys/orders, paging output, and--json. - Add
ovsx listcommand to print a stable, sorted list of extensions in a namespace, with--json. - Extract shared formatting utilities (aligned tables, count/number formatting, truncation) into
cli/src/table.tsand reuse them fromshow.
File summaries
| File | Description |
|---|---|
| cli/src/main.ts | Wires new search and list commands into the CLI, including option parsing. |
| cli/src/search.ts | Implements search request/validation and result table + paging footer output. |
| cli/src/search-options.ts | Defines typed options contract for the search command. |
| cli/src/list.ts | Implements namespace listing output and stable alphabetical ordering. |
| cli/src/list-options.ts | Defines typed options contract for the list command. |
| cli/src/registry.ts | Adds registry client methods/types for /api/-/search and /api/{namespace}. |
| cli/src/table.ts | New shared table/number/truncation formatting helpers used across commands. |
| cli/src/show.ts | Switches show to use shared table/count/number helpers instead of local copies. |
| cli/src/util.ts | Adds a shared integer option parser for commander-based numeric flags. |
| cli/test/unit/search.spec.ts | Unit tests for search output, paging, validation, error surfacing, and --json. |
| cli/test/unit/list.spec.ts | Unit tests for list output, sorting, pluralization, error surfacing, and --json. |
Review details
Suppressed comments (3)
cli/src/registry.ts:471
- In the server API schema,
SearchEntry.deprecatedis always present (primitivebooleaninSearchEntryJson), so it shouldn’t be optional in the CLI type definition.
deprecated?: boolean;
cli/src/registry.ts:477
SearchResult.extensionsis required/non-null in the server schema (@NotNullinSearchResultJson), so making it optional in the CLI type definition reduces type safety for consumers.
extensions?: SearchEntry[];
cli/src/registry.ts:482
- In the server API schema,
Namespace.verifiedis required (@NotNullinNamespaceJson), so this should be non-optional in the CLI type definition to match the contract.
verified?: boolean;
- Files reviewed: 11/11 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
12c344b to
d92a2ca
Compare
d92a2ca to
a115fe5
Compare
a115fe5 to
66c1bef
Compare
Completes the discovery half of the CLI. 'ovsx show' inspects an extension you can already name; until now there was no way to find out what a registry holds - 'get --metadata' needs an identifier you already have, which is the gap that made writing the deletion guide for #1620 awkward. 'ovsx search [text]' queries /api/-/search, the endpoint built for exactly this, and prints one row per result with the identifier, version, downloads, rating and a truncated description. It takes --category, --target, --sort-by, --sort-order, --size and --offset, and reports where the page sits in the result set so paging is discoverable rather than guesswork. The text is optional, since browsing a category on its own is a reasonable thing to want. Sort keys and orders are checked locally, because the registry answers a bad one with a bare 400 that says little. 'ovsx list <namespace>' prints the extensions in a namespace from /api/{namespace}, sorted by name so the output is stable enough to diff and pipe rather than depending on the response's key order. That is the question an operator of a private registry actually asks - "what is published here" - and it needs no token, so it works on a deployment with no login provider configured. Both take --json. Neither uses /api/v2/-/query. The search endpoint returns a summary shape built for result lists, and a namespace listing from /api/{namespace} is a single small response, where the same namespace via the query endpoint is 477KB because it repeats every extension-level field on each version/target-platform row. The table formatting show introduced moves to a shared table module, so the three commands align columns and format counts the same way rather than each carrying a copy. Refs #2149, #1620 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
66c1bef to
9b64588
Compare
…it takes Two review points from Copilot on #2154. truncate returned an ellipsis for a max of 0, one character wider than the budget it was handed - the shape of bug that makes a table wrap. No caller can reach it today (the only call site passes a width of 60), but table.ts was extracted precisely to be shared, so it should hold at its boundaries. Covered by a spec of its own: the helpers had no direct tests, only whatever width the calling command happened to use. parsePositiveInt accepted 0, as its own docstring and error message said. Zero is the right answer for --offset, whose default it is, so the name was the wrong half; renamed to parseNonNegativeInt. Free to do now because the export is new in this PR and has no consumers outside it. Left alone: the suggestion to make SearchEntry.verified/deprecated, SearchResult.extensions and Namespace.verified required to match the server's @NotNull. Those classes are @JsonInclude(NON_NULL), so a null boxed Boolean is omitted rather than serialised, and the search endpoint returns its errors *as* a SearchResultJson with no extensions at all - which is why the type extends Response and why the server's own merge loop null-checks getExtensions() twice. Requiring them would be a claim nothing validates at runtime, and would make `result.extensions ?? []` look like dead code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes the discovery half of the CLI.
ovsx show(#2153) inspects an extension you can already name; there was no way to find out what a registry actually holds —get --metadataneeds an identifier you already have, which is the gap that made writing the deletion guide for #1620 awkward.search
Queries
/api/-/search, the endpoint built for this, which returns a summary shape rather than whole extension records. Takes--category,--target,--sort-by,--sort-order,--sizeand--offset, and reports where the page sits in the result set so paging is discoverable instead of guesswork.Two deliberate details:
ovsx search --category Snippetsworks.{"error": "sortBy parameter must be ..."}and a 400; catching it in the CLI gives a better message and saves the round trip. Worth knowing while reviewing: the accepted key isdownloadCount, notdownloads— I got that wrong myself when first poking at the API by hand.list
Prints the extensions in a namespace from
/api/{namespace}— one small response, no paging needed (45 extensions forredhat, ~2KB). Sorted by name, so the output is stable enough to diff and pipe rather than depending on the response object's key order.This is the question an operator of a private registry actually asks — what is published here — and it needs no token, so it works on a deployment with no login provider configured, which is the #1620 scenario.
Why neither uses
/api/v2/-/querySame reasoning as the endpoint change in #2153, in both directions. The search endpoint already returns exactly the fields a result table needs. And a namespace listing via the query endpoint is 477KB for
redhatagainst ~2KB from/api/{namespace}, because it repeats every extension-level field —files,tags,description,publisher— on each version/target-platform row.The trade-off is that
listprints names only. Anything more per extension would mean either that 477KB response or a request per extension;showcovers the detail for one.Shared formatting
The table helpers
showintroduced move into atablemodule, so the three commands align columns, format counts and truncate the same way instead of each carrying a copy. That unification is the reason this is stacked rather than branched frommain— done separately, both PRs would have landed their own copy.Testing
18 new cases across
search.spec.tsandlist.spec.tsagainst local HTTP stubs, following the existingunpublish.spec.tspattern: filter and paging pass-through, the page-position line, empty results, deprecated marking, local sort validation, registry-reported errors,--json, name sorting and count pluralisation. Full CLI suite green at 89 tests,tscandeslintclean.Both commands were also run against open-vsx.org — the output above is real, not mocked up.
Refs #2149, #1620
🤖 Generated with Claude Code