diff --git a/.changeset/add-doc-search-command.md b/.changeset/add-doc-search-command.md new file mode 100644 index 00000000000..23544132171 --- /dev/null +++ b/.changeset/add-doc-search-command.md @@ -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`. diff --git a/docs-shopify.dev/commands/interfaces/doc-search.interface.ts b/docs-shopify.dev/commands/interfaces/doc-search.interface.ts new file mode 100644 index 00000000000..1b74a9c043e --- /dev/null +++ b/docs-shopify.dev/commands/interfaces/doc-search.interface.ts @@ -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 '?: string + + /** + * Limit results to a specific API version (for example: 2025-10, latest, current). + * @environment SHOPIFY_FLAG_API_VERSION + */ + '--api-version '?: string + + /** + * Disable color output. + * @environment SHOPIFY_FLAG_NO_COLOR + */ + '--no-color'?: '' + + /** + * Increase the verbosity of the output. + * @environment SHOPIFY_FLAG_VERBOSE + */ + '--verbose'?: '' +} diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index 782fa48b708..0dc9a4814b7 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -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": "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": "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 '?: 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 '?: 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", diff --git a/packages/cli/README.md b/packages/cli/README.md index f7d6f3620d6..f22936aca08 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -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) @@ -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= [env: SHOPIFY_FLAG_API_NAME] Limit results to a specific API (for example: admin, storefront, + hydrogen, functions). Unrecognized values are ignored. + --api-version= [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 diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index eeacfe2b751..d0e8add091d 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -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": [ ], diff --git a/packages/cli/package.json b/packages/cli/package.json index 715e12c3c6f..536d106982b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -94,6 +94,9 @@ "scope": "shopify", "topicSeparator": " ", "topics": { + "doc": { + "description": "Search and fetch documentation from shopify.dev." + }, "hydrogen": { "description": "Build Hydrogen storefronts." }, diff --git a/packages/cli/src/cli/commands/doc/search.ts b/packages/cli/src/cli/commands/doc/search.ts new file mode 100644 index 00000000000..33aceb0875e --- /dev/null +++ b/packages/cli/src/cli/commands/doc/search.ts @@ -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 { + const {args, flags} = await this.parse(DocSearch) + await docSearchService(args.query, flags['api-name'], flags['api-version']) + } +} diff --git a/packages/cli/src/cli/services/commands/doc/search.test.ts b/packages/cli/src/cli/services/commands/doc/search.test.ts new file mode 100644 index 00000000000..2b86109956f --- /dev/null +++ b/packages/cli/src/cli/services/commands/doc/search.test.ts @@ -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()), + 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', 'nope')) + + await expect(docSearchService('products')).rejects.toThrowError(AbortError) + await expect(docSearchService('products')).rejects.toThrowError(/500 Internal Server Error/) + expect(outputResult).not.toHaveBeenCalled() + }) +}) diff --git a/packages/cli/src/cli/services/commands/doc/search.ts b/packages/cli/src/cli/services/commands/doc/search.ts new file mode 100644 index 00000000000..c249e6a4c8b --- /dev/null +++ b/packages/cli/src/cli/services/commands/doc/search.ts @@ -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) +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ff506419c63..efa05b36422 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -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' @@ -145,6 +146,7 @@ export const COMMANDS: any = { ...HydrogenCommands, ...StoreCommands, search: Search, + 'doc:search': DocSearch, upgrade: Upgrade, version: VersionCommand, help: HelpCommand, diff --git a/packages/e2e/data/snapshots/commands.txt b/packages/e2e/data/snapshots/commands.txt index 9e9afb6c946..8ce280cc296 100644 --- a/packages/e2e/data/snapshots/commands.txt +++ b/packages/e2e/data/snapshots/commands.txt @@ -49,6 +49,8 @@ │ ├─ off │ ├─ on │ └─ status +├─ doc +│ └─ search ├─ help ├─ hydrogen │ ├─ build