Skip to content

Commit 2f4b95a

Browse files
nelsonwittwerclaude
andcommitted
Refactor shopify search to query the dev-assistant vector store as JSON
The `search` command opened a browser at shopify.dev?search=<query>, which now redirects to the docs SPA and silently drops the query — so it has been broken for users while still being invoked ~70-100x/month. Repurpose it as an agent-facing JSON tool: it queries the dev-assistant vector store (GET https://shopify.dev/assistant/search) and prints the matching documentation chunks as JSON to stdout. No browser. This complements `fetch-doc` (verbatim full-document retrieval) as the "chunked discovery" half. - `query` is now required. - Adds `--api-name` and `--api-version` filters, passed through to the server. - 400 responses surface the server's error message (e.g. valid api_version list). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 42da86f commit 2f4b95a

8 files changed

Lines changed: 248 additions & 39 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/cli': minor
3+
---
4+
5+
`shopify search` now queries the shopify.dev vector store and prints the most relevant documentation chunks as JSON to stdout, instead of opening a browser. This makes it usable for programmatic and agent-driven discovery. The `query` argument is now required, and two new optional filters are available: `--api-name` (for example `admin`, `storefront`, `hydrogen`) and `--api-version` (for example `2025-10`, `latest`, `current`). To download a full document verbatim, use `fetch-doc`.

docs-shopify.dev/commands/interfaces/search.interface.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,27 @@
44
* @publicDocs
55
*/
66
export interface search {
7+
/**
8+
* Limit results to a specific API (for example: admin, storefront, hydrogen, functions). Unrecognized values are ignored.
9+
* @environment SHOPIFY_FLAG_API_NAME
10+
*/
11+
'--api-name <value>'?: string
712

13+
/**
14+
* Limit results to a specific API version (for example: 2025-10, latest, current).
15+
* @environment SHOPIFY_FLAG_API_VERSION
16+
*/
17+
'--api-version <value>'?: string
18+
19+
/**
20+
* Disable color output.
21+
* @environment SHOPIFY_FLAG_NO_COLOR
22+
*/
23+
'--no-color'?: ''
24+
25+
/**
26+
* Increase the verbosity of the output.
27+
* @environment SHOPIFY_FLAG_VERBOSE
28+
*/
29+
'--verbose'?: ''
830
}

docs-shopify.dev/generated/generated_docs_data_v2.json

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4126,8 +4126,45 @@
41264126
"name": "search",
41274127
"description": "The following flags are available for the `search` command:",
41284128
"isPublicDocs": true,
4129-
"members": [],
4130-
"value": "export interface search {\n\n}"
4129+
"members": [
4130+
{
4131+
"filePath": "docs-shopify.dev/commands/interfaces/search.interface.ts",
4132+
"syntaxKind": "PropertySignature",
4133+
"name": "--api-name <value>",
4134+
"value": "string",
4135+
"description": "Limit results to a specific API (for example: admin, storefront, hydrogen, functions). Unrecognized values are ignored.",
4136+
"isOptional": true,
4137+
"environmentValue": "SHOPIFY_FLAG_API_NAME"
4138+
},
4139+
{
4140+
"filePath": "docs-shopify.dev/commands/interfaces/search.interface.ts",
4141+
"syntaxKind": "PropertySignature",
4142+
"name": "--api-version <value>",
4143+
"value": "string",
4144+
"description": "Limit results to a specific API version (for example: 2025-10, latest, current).",
4145+
"isOptional": true,
4146+
"environmentValue": "SHOPIFY_FLAG_API_VERSION"
4147+
},
4148+
{
4149+
"filePath": "docs-shopify.dev/commands/interfaces/search.interface.ts",
4150+
"syntaxKind": "PropertySignature",
4151+
"name": "--no-color",
4152+
"value": "''",
4153+
"description": "Disable color output.",
4154+
"isOptional": true,
4155+
"environmentValue": "SHOPIFY_FLAG_NO_COLOR"
4156+
},
4157+
{
4158+
"filePath": "docs-shopify.dev/commands/interfaces/search.interface.ts",
4159+
"syntaxKind": "PropertySignature",
4160+
"name": "--verbose",
4161+
"value": "''",
4162+
"description": "Increase the verbosity of the output.",
4163+
"isOptional": true,
4164+
"environmentValue": "SHOPIFY_FLAG_VERBOSE"
4165+
}
4166+
],
4167+
"value": "export interface search {\n /**\n * Limit results to a specific API (for example: admin, storefront, hydrogen, functions). Unrecognized values are ignored.\n * @environment SHOPIFY_FLAG_API_NAME\n */\n '--api-name <value>'?: string\n\n /**\n * Limit results to a specific API version (for example: 2025-10, latest, current).\n * @environment SHOPIFY_FLAG_API_VERSION\n */\n '--api-version <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
41314168
}
41324169
},
41334170
"storeauth": {

packages/cli/README.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2083,22 +2083,33 @@ DESCRIPTION
20832083

20842084
## `shopify search [query]`
20852085

2086-
Starts a search on shopify.dev.
2086+
Query the shopify.dev vector store and print the most relevant documentation chunks as JSON. Best for programmatic discovery — surfacing the relevant pieces of documentation for a topic, rather than retrieving a whole document. To download a full document verbatim, use `fetch-doc`.
20872087

20882088
```
20892089
USAGE
20902090
$ shopify search [query]
20912091
2092+
ARGUMENTS
2093+
QUERY The search query.
2094+
2095+
FLAGS
2096+
--api-name=<value> [env: SHOPIFY_FLAG_API_NAME] Limit results to a specific API (for example: admin, storefront,
2097+
hydrogen, functions). Unrecognized values are ignored.
2098+
--api-version=<value> [env: SHOPIFY_FLAG_API_VERSION] Limit results to a specific API version (for example: 2025-10,
2099+
latest, current).
2100+
--no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output.
2101+
--verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output.
2102+
20922103
DESCRIPTION
2093-
Starts a search on shopify.dev.
2104+
Query the shopify.dev vector store and print the most relevant documentation chunks as JSON. Best for programmatic
2105+
discovery — surfacing the relevant pieces of documentation for a topic, rather than retrieving a whole document. To
2106+
download a full document verbatim, use `fetch-doc`.
20942107
20952108
EXAMPLES
2096-
# open the search modal on Shopify.dev
2097-
shopify search
2098-
# search for a term on Shopify.dev
2099-
shopify search <query>
2100-
# search for a phrase on Shopify.dev
2101-
shopify search "<a search query separated by spaces>"
2109+
# search shopify.dev for a topic
2110+
shopify search "subscribe to webhooks"
2111+
# narrow the search to a specific API and version
2112+
shopify search "create a product" --api-name admin --api-version latest
21022113
```
21032114

21042115
## `shopify store auth`

packages/cli/oclif.manifest.json

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5649,15 +5649,49 @@
56495649
],
56505650
"args": {
56515651
"query": {
5652-
"name": "query"
5652+
"description": "The search query.",
5653+
"name": "query",
5654+
"required": true
56535655
}
56545656
},
5655-
"description": "Starts a search on shopify.dev.",
5657+
"description": "Query the shopify.dev vector store and print the most relevant documentation chunks as JSON. Best for programmatic discovery — surfacing the relevant pieces of documentation for a topic, rather than retrieving a whole document. To download a full document verbatim, use `fetch-doc`.",
56565658
"enableJsonFlag": false,
56575659
"examples": [
5658-
"# open the search modal on Shopify.dev\n shopify search\n\n # search for a term on Shopify.dev\n shopify search <query>\n\n # search for a phrase on Shopify.dev\n shopify search \"<a search query separated by spaces>\"\n "
5660+
"# search shopify.dev for a topic\n shopify search \"subscribe to webhooks\"\n\n # narrow the search to a specific API and version\n shopify search \"create a product\" --api-name admin --api-version latest\n "
56595661
],
56605662
"flags": {
5663+
"api-name": {
5664+
"description": "Limit results to a specific API (for example: admin, storefront, hydrogen, functions). Unrecognized values are ignored.",
5665+
"env": "SHOPIFY_FLAG_API_NAME",
5666+
"hasDynamicHelp": false,
5667+
"multiple": false,
5668+
"name": "api-name",
5669+
"type": "option"
5670+
},
5671+
"api-version": {
5672+
"description": "Limit results to a specific API version (for example: 2025-10, latest, current).",
5673+
"env": "SHOPIFY_FLAG_API_VERSION",
5674+
"hasDynamicHelp": false,
5675+
"multiple": false,
5676+
"name": "api-version",
5677+
"type": "option"
5678+
},
5679+
"no-color": {
5680+
"allowNo": false,
5681+
"description": "Disable color output.",
5682+
"env": "SHOPIFY_FLAG_NO_COLOR",
5683+
"hidden": false,
5684+
"name": "no-color",
5685+
"type": "boolean"
5686+
},
5687+
"verbose": {
5688+
"allowNo": false,
5689+
"description": "Increase the verbosity of the output.",
5690+
"env": "SHOPIFY_FLAG_VERBOSE",
5691+
"hidden": false,
5692+
"name": "verbose",
5693+
"type": "boolean"
5694+
}
56615695
},
56625696
"hasDynamicHelp": false,
56635697
"hiddenAliases": [
Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,46 @@
11
import {searchService} from '../services/commands/search.js'
22
import Command from '@shopify/cli-kit/node/base-command'
3-
import {Args} from '@oclif/core'
3+
import {globalFlags} from '@shopify/cli-kit/node/cli'
4+
import {Args, Flags} from '@oclif/core'
45

56
export default class Search extends Command {
6-
static description = 'Starts a search on shopify.dev.'
7+
static description =
8+
'Query the shopify.dev vector store and print the most relevant documentation chunks as JSON. Best for programmatic discovery — surfacing the relevant pieces of documentation for a topic, rather than retrieving a whole document. To download a full document verbatim, use `fetch-doc`.'
79

810
static usage = `search [query]`
911

1012
static examples = [
11-
`# open the search modal on Shopify.dev
12-
shopify search
13+
`# search shopify.dev for a topic
14+
shopify search "subscribe to webhooks"
1315
14-
# search for a term on Shopify.dev
15-
shopify search <query>
16-
17-
# search for a phrase on Shopify.dev
18-
shopify search "<a search query separated by spaces>"
16+
# narrow the search to a specific API and version
17+
shopify search "create a product" --api-name admin --api-version latest
1918
`,
2019
]
2120

2221
static args = {
23-
query: Args.string(),
22+
query: Args.string({
23+
name: 'query',
24+
required: true,
25+
description: 'The search query.',
26+
}),
27+
}
28+
29+
static flags = {
30+
...globalFlags,
31+
'api-name': Flags.string({
32+
description:
33+
'Limit results to a specific API (for example: admin, storefront, hydrogen, functions). Unrecognized values are ignored.',
34+
env: 'SHOPIFY_FLAG_API_NAME',
35+
}),
36+
'api-version': Flags.string({
37+
description: 'Limit results to a specific API version (for example: 2025-10, latest, current).',
38+
env: 'SHOPIFY_FLAG_API_VERSION',
39+
}),
2440
}
2541

2642
async run(): Promise<void> {
27-
const {args} = await this.parse(Search)
28-
await searchService(args.query)
43+
const {args, flags} = await this.parse(Search)
44+
await searchService(args.query, flags['api-name'], flags['api-version'])
2945
}
3046
}
Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,78 @@
11
import {searchService} from './search.js'
2-
import {describe, expect, test, vi} from 'vitest'
3-
import {openURL} from '@shopify/cli-kit/node/system'
2+
import {describe, expect, test, vi, beforeEach} from 'vitest'
3+
import {fetch} from '@shopify/cli-kit/node/http'
4+
import {outputResult} from '@shopify/cli-kit/node/output'
5+
import {AbortError} from '@shopify/cli-kit/node/error'
46

5-
vi.mock('@shopify/cli-kit/node/system')
7+
vi.mock('@shopify/cli-kit/node/http')
8+
// Only stub `outputResult`; keep the rest of the module real. Blanket-mocking it
9+
// would also mock `stringifyMessage`, which `AbortError`'s constructor relies on —
10+
// that would silently empty out every thrown error message.
11+
vi.mock('@shopify/cli-kit/node/output', async (importOriginal) => ({
12+
...(await importOriginal<typeof import('@shopify/cli-kit/node/output')>()),
13+
outputResult: vi.fn(),
14+
}))
15+
16+
const okResponse = (body: string) =>
17+
({ok: true, status: 200, statusText: 'OK', text: () => Promise.resolve(body)}) as any
18+
19+
const errorResponse = (status: number, statusText: string, body: string) =>
20+
({ok: false, status, statusText, text: () => Promise.resolve(body)}) as any
21+
22+
const resultsBody =
23+
'[{"score":0.99,"content":"About webhooks","url":"https://shopify.dev/x","title":"Webhooks","domain":null}]'
24+
25+
beforeEach(() => {
26+
vi.mocked(fetch).mockResolvedValue(okResponse(resultsBody))
27+
})
628

729
describe('searchService', () => {
8-
test('the right URL is open in the system when a query is passed', async () => {
9-
await searchService('deploy app')
30+
test('requests the search endpoint with the query and prints the raw JSON body', async () => {
31+
await searchService('webhooks')
32+
33+
expect(fetch).toHaveBeenCalledWith('https://shopify.dev/assistant/search?query=webhooks', {
34+
headers: {Accept: 'application/json'},
35+
})
36+
expect(outputResult).toHaveBeenCalledWith(resultsBody)
37+
})
38+
39+
test('includes api_name and api_version params when provided', async () => {
40+
await searchService('create a product', 'admin', 'latest')
41+
42+
expect(fetch).toHaveBeenCalledWith(
43+
'https://shopify.dev/assistant/search?query=create+a+product&api_name=admin&api_version=latest',
44+
{headers: {Accept: 'application/json'}},
45+
)
46+
})
47+
48+
test('URL-encodes queries with spaces and special characters', async () => {
49+
await searchService('a & b?')
50+
51+
expect(fetch).toHaveBeenCalledWith('https://shopify.dev/assistant/search?query=a+%26+b%3F', {
52+
headers: {Accept: 'application/json'},
53+
})
54+
})
55+
56+
test('surfaces the server error message from a non-ok JSON response', async () => {
57+
vi.mocked(fetch).mockResolvedValue(
58+
errorResponse(
59+
400,
60+
'Bad Request',
61+
'{"error":"Invalid api_version \'2025-01\' for api_name \'admin\'. Available versions: 2026-07"}',
62+
),
63+
)
1064

11-
expect(openURL).toBeCalledWith('https://shopify.dev?search=deploy+app')
65+
await expect(searchService('products', 'admin', '2025-01')).rejects.toThrowError(
66+
/Invalid api_version '2025-01' for api_name 'admin'\. Available versions: 2026-07/,
67+
)
68+
expect(outputResult).not.toHaveBeenCalled()
1269
})
1370

14-
test('the right URL is open in the system when a query is not passed', async () => {
15-
await searchService()
71+
test('falls back to the status line when a non-ok response is not JSON', async () => {
72+
vi.mocked(fetch).mockResolvedValue(errorResponse(500, 'Internal Server Error', '<html>nope</html>'))
1673

17-
expect(openURL).toBeCalledWith('https://shopify.dev?search=')
74+
await expect(searchService('products')).rejects.toThrowError(AbortError)
75+
await expect(searchService('products')).rejects.toThrowError(/500 Internal Server Error/)
76+
expect(outputResult).not.toHaveBeenCalled()
1877
})
1978
})
Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
1-
import {openURL} from '@shopify/cli-kit/node/system'
1+
import {fetch} from '@shopify/cli-kit/node/http'
2+
import {outputResult} from '@shopify/cli-kit/node/output'
3+
import {AbortError} from '@shopify/cli-kit/node/error'
24

3-
export async function searchService(query?: string) {
4-
const searchParams = new URLSearchParams()
5-
searchParams.append('search', query ?? '')
6-
await openURL(`https://shopify.dev?${searchParams.toString()}`)
5+
// The dev-assistant search endpoint queries the shopify.dev vector store and
6+
// returns an array of matching documentation chunks as JSON.
7+
const SEARCH_URL = 'https://shopify.dev/assistant/search'
8+
9+
export async function searchService(query: string, apiName?: string, apiVersion?: string) {
10+
const params = new URLSearchParams({query})
11+
if (apiName) params.append('api_name', apiName)
12+
if (apiVersion) params.append('api_version', apiVersion)
13+
14+
const response = await fetch(`${SEARCH_URL}?${params.toString()}`, {headers: {Accept: 'application/json'}})
15+
const body = await response.text()
16+
17+
if (!response.ok) {
18+
// The endpoint returns a JSON `{error}` body for 400s (e.g. an invalid api_version
19+
// lists the valid versions) — surface it directly instead of a bare status code.
20+
let message = `${response.status} ${response.statusText}`
21+
try {
22+
const parsed = JSON.parse(body)
23+
if (parsed?.error) message = parsed.error
24+
} catch (parseError) {
25+
// Body wasn't JSON; fall back to the status line. Rethrow anything unexpected.
26+
if (!(parseError instanceof SyntaxError)) throw parseError
27+
}
28+
throw new AbortError(`Search failed: ${message}`)
29+
}
30+
31+
outputResult(body)
732
}

0 commit comments

Comments
 (0)