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
5 changes: 5 additions & 0 deletions .changeset/add-doc-search-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli': minor
---

Add `shopify doc search` to return relevant shopify.dev documentation chunks as JSON.
36 changes: 36 additions & 0 deletions docs-shopify.dev/commands/interfaces/doc-search.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// This is an autogenerated file. Don't edit this file manually.
/**
* The following flags are available for the `doc search` command:
* @publicDocs
*/
export interface docsearch {
/**
* Limit results to a specific API (for example: admin, storefront, hydrogen, functions). Unrecognized values are ignored.
* @environment SHOPIFY_FLAG_API_NAME
*/
'--api-name <value>'?: string

/**
* Limit results to a specific API version (for example: 2025-10, latest, current).
* @environment SHOPIFY_FLAG_API_VERSION
*/
'--api-version <value>'?: string

/**
* Disable color output.
* @environment SHOPIFY_FLAG_NO_COLOR
*/
'--no-color'?: ''

/**
* The search query.
* @environment SHOPIFY_FLAG_QUERY
*/
'--query <value>': string

/**
* Increase the verbosity of the output.
* @environment SHOPIFY_FLAG_VERBOSE
*/
'--verbose'?: ''
}
55 changes: 55 additions & 0 deletions docs-shopify.dev/generated/generated_docs_data_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -2742,6 +2742,61 @@
"value": "export interface configautoupgradestatus {\n\n}"
}
},
"docsearch": {
"docs-shopify.dev/commands/interfaces/doc-search.interface.ts": {
"filePath": "docs-shopify.dev/commands/interfaces/doc-search.interface.ts",
"name": "docsearch",
"description": "The following flags are available for the `doc search` command:",
"isPublicDocs": true,
"members": [
{
"filePath": "docs-shopify.dev/commands/interfaces/doc-search.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--api-name <value>",
"value": "string",
"description": "Limit results to a specific API (for example: admin, storefront, hydrogen, functions). Unrecognized values are ignored.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_API_NAME"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/doc-search.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--api-version <value>",
"value": "string",
"description": "Limit results to a specific API version (for example: 2025-10, latest, current).",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_API_VERSION"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/doc-search.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--no-color",
"value": "''",
"description": "Disable color output.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_NO_COLOR"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/doc-search.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--query <value>",
"value": "string",
"description": "The search query.",
"environmentValue": "SHOPIFY_FLAG_QUERY"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/doc-search.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--verbose",
"value": "''",
"description": "Increase the verbosity of the output.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_VERBOSE"
}
],
"value": "export interface docsearch {\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 * The search query.\n * @environment SHOPIFY_FLAG_QUERY\n */\n '--query <value>': string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
},
"help": {
"docs-shopify.dev/commands/interfaces/help.interface.ts": {
"filePath": "docs-shopify.dev/commands/interfaces/help.interface.ts",
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
* [`shopify config autoupgrade off`](#shopify-config-autoupgrade-off)
* [`shopify config autoupgrade on`](#shopify-config-autoupgrade-on)
* [`shopify config autoupgrade status`](#shopify-config-autoupgrade-status)
* [`shopify doc search`](#shopify-doc-search)
* [`shopify help [command] [flags]`](#shopify-help-command-flags)
* [`shopify hydrogen build`](#shopify-hydrogen-build)
* [`shopify hydrogen check RESOURCE`](#shopify-hydrogen-check-resource)
Expand Down Expand Up @@ -1213,6 +1214,35 @@ DESCRIPTION
Run `shopify config autoupgrade on` or `shopify config autoupgrade off` to configure it.
```

## `shopify doc search`

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 `doc fetch`.

```
USAGE
$ shopify doc search --query <value> [--api-name <value>] [--api-version <value>] [--no-color] [--verbose]

FLAGS
--api-name=<value> [env: SHOPIFY_FLAG_API_NAME] Limit results to a specific API (for example: admin, storefront,
hydrogen, functions). Unrecognized values are ignored.
--api-version=<value> [env: SHOPIFY_FLAG_API_VERSION] Limit results to a specific API version (for example: 2025-10,
latest, current).
--no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output.
--query=<value> (required) [env: SHOPIFY_FLAG_QUERY] The search query.
--verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output.

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 `doc fetch`.

EXAMPLES
# search shopify.dev for a topic
shopify doc search --query "subscribe to webhooks"
# narrow the search to a specific API and version
shopify doc search --query "create a product" --api-name admin --api-version latest
```

## `shopify help [command] [flags]`

Display help for Shopify CLI
Expand Down
62 changes: 62 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3435,6 +3435,68 @@
"strict": true,
"summary": "Watch and prints out changes to an app."
},
"doc:search": {
"aliases": [
],
"args": {
},
"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 `doc fetch`.",
"enableJsonFlag": false,
"examples": [
"# search shopify.dev for a topic\n shopify doc search --query \"subscribe to webhooks\"\n\n # narrow the search to a specific API and version\n shopify doc search --query \"create a product\" --api-name admin --api-version latest\n "
],
"flags": {
"api-name": {
"description": "Limit results to a specific API (for example: admin, storefront, hydrogen, functions). Unrecognized values are ignored.",
"env": "SHOPIFY_FLAG_API_NAME",
"hasDynamicHelp": false,
"multiple": false,
"name": "api-name",
"type": "option"
},
"api-version": {
"description": "Limit results to a specific API version (for example: 2025-10, latest, current).",
"env": "SHOPIFY_FLAG_API_VERSION",
"hasDynamicHelp": false,
"multiple": false,
"name": "api-version",
"type": "option"
},
"no-color": {
"allowNo": false,
"description": "Disable color output.",
"env": "SHOPIFY_FLAG_NO_COLOR",
"hidden": false,
"name": "no-color",
"type": "boolean"
},
"query": {
"description": "The search query.",
"env": "SHOPIFY_FLAG_QUERY",
"hasDynamicHelp": false,
"multiple": false,
"name": "query",
"required": true,
"type": "option"
},
"verbose": {
"allowNo": false,
"description": "Increase the verbosity of the output.",
"env": "SHOPIFY_FLAG_VERBOSE",
"hidden": false,
"name": "verbose",
"type": "boolean"
}
},
"hasDynamicHelp": false,
"hiddenAliases": [
],
"id": "doc:search",
"pluginAlias": "@shopify/cli",
"pluginName": "@shopify/cli",
"pluginType": "core",
"strict": true
},
"docs:generate": {
"aliases": [
],
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@
"scope": "shopify",
"topicSeparator": " ",
"topics": {
"doc": {
"description": "Search and fetch documentation from shopify.dev."
},
"hydrogen": {
"description": "Build Hydrogen storefronts."
},
Expand Down
41 changes: 41 additions & 0 deletions packages/cli/src/cli/commands/doc/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {docSearchService} from '../../services/commands/doc/search.js'
import Command from '@shopify/cli-kit/node/base-command'
import {globalFlags} from '@shopify/cli-kit/node/cli'
import {Flags} from '@oclif/core'

export default class DocSearch extends Command {
static 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 `doc fetch`.'

static examples = [
`# search shopify.dev for a topic
shopify doc search --query "subscribe to webhooks"

# narrow the search to a specific API and version
shopify doc search --query "create a product" --api-name admin --api-version latest
`,
]

static flags = {
...globalFlags,
query: Flags.string({
description: 'The search query.',
env: 'SHOPIFY_FLAG_QUERY',
required: true,
}),
'api-name': Flags.string({
description:
'Limit results to a specific API (for example: admin, storefront, hydrogen, functions). Unrecognized values are ignored.',
env: 'SHOPIFY_FLAG_API_NAME',
}),
'api-version': Flags.string({
description: 'Limit results to a specific API version (for example: 2025-10, latest, current).',
env: 'SHOPIFY_FLAG_API_VERSION',
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(DocSearch)
await docSearchService(flags.query, flags['api-name'], flags['api-version'])
}
}
86 changes: 86 additions & 0 deletions packages/cli/src/cli/services/commands/doc/search.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {docSearchService} from './search.js'
import {describe, expect, test, vi, beforeEach} from 'vitest'
import {shopifyFetch} from '@shopify/cli-kit/node/http'
import {outputResult} from '@shopify/cli-kit/node/output'
import {AbortError} from '@shopify/cli-kit/node/error'

vi.mock('@shopify/cli-kit/node/http')
// Only stub `outputResult`; keep the rest of the module real. Blanket-mocking it
// would also mock `stringifyMessage`, which `AbortError`'s constructor relies on —
// that would silently empty out every thrown error message.
vi.mock('@shopify/cli-kit/node/output', async (importOriginal) => ({
...(await importOriginal<typeof import('@shopify/cli-kit/node/output')>()),
outputResult: vi.fn(),
}))

const okResponse = (body: string) =>
({ok: true, status: 200, statusText: 'OK', text: () => Promise.resolve(body)}) as any

const errorResponse = (status: number, statusText: string, body: string) =>
({ok: false, status, statusText, text: () => Promise.resolve(body)}) as any

const resultsBody =
'[{"score":0.99,"content":"About webhooks","url":"https://shopify.dev/x","title":"Webhooks","domain":null}]'

beforeEach(() => {
vi.mocked(shopifyFetch).mockResolvedValue(okResponse(resultsBody))
})

describe('docSearchService', () => {
test('requests the search endpoint with the query and prints the raw JSON body', async () => {
await docSearchService('webhooks')

expect(shopifyFetch).toHaveBeenCalledWith('https://shopify.dev/assistant/search?query=webhooks', {
headers: {Accept: 'application/json', 'X-Shopify-Surface': 'cli'},
})
expect(outputResult).toHaveBeenCalledWith(resultsBody)
})

test('includes api_name and api_version params when provided', async () => {
await docSearchService('create a product', 'admin', 'latest')

expect(shopifyFetch).toHaveBeenCalledWith(
'https://shopify.dev/assistant/search?query=create+a+product&api_name=admin&api_version=latest',
{headers: {Accept: 'application/json', 'X-Shopify-Surface': 'cli'}},
)
})

test('URL-encodes queries with spaces and special characters', async () => {
await docSearchService('a & b?')

expect(shopifyFetch).toHaveBeenCalledWith('https://shopify.dev/assistant/search?query=a+%26+b%3F', {
headers: {Accept: 'application/json', 'X-Shopify-Surface': 'cli'},
})
})

test('surfaces the server error message from a non-ok JSON response', async () => {
vi.mocked(shopifyFetch).mockResolvedValue(
errorResponse(
400,
'Bad Request',
'{"error":"Invalid api_version \'2025-01\' for api_name \'admin\'. Available versions: 2026-07"}',
),
)

await expect(docSearchService('products', 'admin', '2025-01')).rejects.toThrowError(
/Invalid api_version '2025-01' for api_name 'admin'\. Available versions: 2026-07/,
)
expect(outputResult).not.toHaveBeenCalled()
})

test('falls back to the status line when a non-ok response is not JSON', async () => {
vi.mocked(shopifyFetch).mockResolvedValue(errorResponse(500, 'Internal Server Error', '<html>nope</html>'))

await expect(docSearchService('products')).rejects.toThrowError(AbortError)
await expect(docSearchService('products')).rejects.toThrowError(/500 Internal Server Error/)
expect(outputResult).not.toHaveBeenCalled()
})

test('reports a friendly error when the request cannot reach shopify.dev', async () => {
vi.mocked(shopifyFetch).mockRejectedValue(new Error('getaddrinfo ENOTFOUND shopify.dev'))

await expect(docSearchService('products')).rejects.toThrowError(AbortError)
await expect(docSearchService('products')).rejects.toThrowError(/Could not reach shopify\.dev/)
expect(outputResult).not.toHaveBeenCalled()
})
})
Loading
Loading