Skip to content
Draft
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`, which queries the shopify.dev vector store and prints the most relevant documentation chunks as JSON to stdout. This makes it usable for programmatic and agent-driven discovery. The `query` argument is required, and two 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 `doc fetch`.
30 changes: 30 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,30 @@
// 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'?: ''

/**
* Increase the verbosity of the output.
* @environment SHOPIFY_FLAG_VERBOSE
*/
'--verbose'?: ''
}
47 changes: 47 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,53 @@
"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": "--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 * 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
32 changes: 32 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 [query]`](#shopify-doc-search-query)
* [`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 @@ -1212,6 +1213,37 @@ DESCRIPTION
Run `shopify config autoupgrade on` or `shopify config autoupgrade off` to configure it.
```

## `shopify doc search [query]`

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]

ARGUMENTS
QUERY The search query.

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.
--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 "subscribe to webhooks"
# narrow the search to a specific API and version
shopify doc search "create a product" --api-name admin --api-version latest
```

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

Display help for Shopify CLI
Expand Down
59 changes: 59 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3424,6 +3424,65 @@
"strict": true,
"summary": "Watch and prints out changes to an app."
},
"doc:search": {
"aliases": [
],
"args": {
"query": {
"description": "The search query.",
"name": "query",
"required": true
}
},
"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 \"subscribe to webhooks\"\n\n # narrow the search to a specific API and version\n shopify doc search \"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"
},
"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,
"usage": "doc search [query]"
},
"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
46 changes: 46 additions & 0 deletions packages/cli/src/cli/commands/doc/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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 {Args, 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 usage = `doc search [query]`

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

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

static args = {
query: Args.string({
name: 'query',
required: true,
description: 'The search query.',
}),
}

static flags = {
...globalFlags,
'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 {args, flags} = await this.parse(DocSearch)
await docSearchService(args.query, flags['api-name'], flags['api-version'])
}
}
78 changes: 78 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,78 @@
import {docSearchService} from './search.js'
import {describe, expect, test, vi, beforeEach} from 'vitest'
import {fetch} 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(fetch).mockResolvedValue(okResponse(resultsBody))
})

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

expect(fetch).toHaveBeenCalledWith('https://shopify.dev/assistant/search?query=webhooks', {
headers: {Accept: 'application/json'},
})
expect(outputResult).toHaveBeenCalledWith(resultsBody)
})

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

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

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

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

test('surfaces the server error message from a non-ok JSON response', async () => {
vi.mocked(fetch).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(fetch).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()
})
})
32 changes: 32 additions & 0 deletions packages/cli/src/cli/services/commands/doc/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {fetch} from '@shopify/cli-kit/node/http'
import {outputResult} from '@shopify/cli-kit/node/output'
import {AbortError} from '@shopify/cli-kit/node/error'

// The dev-assistant search endpoint queries the shopify.dev vector store and
// returns an array of matching documentation chunks as JSON.
const SEARCH_URL = 'https://shopify.dev/assistant/search'

export async function docSearchService(query: string, apiName?: string, apiVersion?: string) {
const params = new URLSearchParams({query})
if (apiName) params.append('api_name', apiName)
if (apiVersion) params.append('api_version', apiVersion)

const response = await fetch(`${SEARCH_URL}?${params.toString()}`, {headers: {Accept: 'application/json'}})
const body = await response.text()

if (!response.ok) {
// The endpoint returns a JSON `{error}` body for 400s (e.g. an invalid api_version
// lists the valid versions) — surface it directly instead of a bare status code.
let message = `${response.status} ${response.statusText}`
try {
const parsed = JSON.parse(body)
if (parsed?.error) message = parsed.error
} catch (parseError) {
// Body wasn't JSON; fall back to the status line. Rethrow anything unexpected.
if (!(parseError instanceof SyntaxError)) throw parseError
}
throw new AbortError(`Search failed: ${message}`)
}

outputResult(body)
}
2 changes: 2 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import VersionCommand from './cli/commands/version.js'
import Search from './cli/commands/search.js'
import DocSearch from './cli/commands/doc/search.js'
import Upgrade from './cli/commands/upgrade.js'
import Logout from './cli/commands/auth/logout.js'
import Login from './cli/commands/auth/login.js'
Expand Down Expand Up @@ -145,6 +146,7 @@ export const COMMANDS: any = {
...HydrogenCommands,
...StoreCommands,
search: Search,
'doc:search': DocSearch,
upgrade: Upgrade,
version: VersionCommand,
help: HelpCommand,
Expand Down
2 changes: 2 additions & 0 deletions packages/e2e/data/snapshots/commands.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
│ ├─ off
│ ├─ on
│ └─ status
├─ doc
│ └─ search
├─ help
├─ hydrogen
│ ├─ build
Expand Down
Loading