Skip to content

Commit 76bb8a2

Browse files
Add typed JSON output to theme pull
1 parent 96095dc commit 76bb8a2

15 files changed

Lines changed: 567 additions & 123 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+
Add typed JSON output to theme pull with theme details and the local download path.

‎docs-shopify.dev/generated/generated_docs_data_v2.json‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8152,6 +8152,15 @@
81528152
"isOptional": true,
81538153
"environmentValue": "SHOPIFY_FLAG_ENVIRONMENT"
81548154
},
8155+
{
8156+
"filePath": "docs-shopify.dev/commands/interfaces/theme-pull.interface.ts",
8157+
"syntaxKind": "PropertySignature",
8158+
"name": "-j, --json",
8159+
"value": "''",
8160+
"description": "Output the result as JSON. Automatically disables color output.",
8161+
"isOptional": true,
8162+
"environmentValue": "SHOPIFY_FLAG_JSON"
8163+
},
81558164
{
81568165
"filePath": "docs-shopify.dev/commands/interfaces/theme-pull.interface.ts",
81578166
"syntaxKind": "PropertySignature",
@@ -8207,7 +8216,7 @@
82078216
"environmentValue": "SHOPIFY_FLAG_IGNORE"
82088217
}
82098218
],
8210-
"value": "export interface themepull {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * Pull theme files from your remote development theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_DEVELOPMENT\n */\n '-d, --development'?: ''\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Skip downloading the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.\n * @environment SHOPIFY_FLAG_IGNORE\n */\n '-x, --ignore <value>'?: string\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Pull theme files from your remote live theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_LIVE\n */\n '-l, --live'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Prevent deleting local files that don't exist remotely.\n * @environment SHOPIFY_FLAG_NODELETE\n */\n '-n, --nodelete'?: ''\n\n /**\n * Download only the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.\n * @environment SHOPIFY_FLAG_ONLY\n */\n '-o, --only <value>'?: string\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password <value>'?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * Theme ID or name of the remote theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
8219+
"value": "export interface themepull {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * Pull theme files from your remote development theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_DEVELOPMENT\n */\n '-d, --development'?: ''\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Skip downloading the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.\n * @environment SHOPIFY_FLAG_IGNORE\n */\n '-x, --ignore <value>'?: string\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Pull theme files from your remote live theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_LIVE\n */\n '-l, --live'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Prevent deleting local files that don't exist remotely.\n * @environment SHOPIFY_FLAG_NODELETE\n */\n '-n, --nodelete'?: ''\n\n /**\n * Download only the specified files (Multiple flags allowed). Wrap the value in double quotes if you're using wildcards.\n * @environment SHOPIFY_FLAG_ONLY\n */\n '-o, --only <value>'?: string\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password <value>'?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * Theme ID or name of the remote theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
82118220
}
82128221
},
82138222
"themepush": {

‎packages/cli/README.md‎

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8645,7 +8645,7 @@ Download your remote theme files locally.
86458645
86468646
```
86478647
USAGE
8648-
$ shopify theme pull [--auth-alias <value>] [-d] [-e <value>...] [-x <value>...] [--json-schema] [-l]
8648+
$ shopify theme pull [--auth-alias <value>] [-d] [-e <value>...] [-x <value>...] [-j] [--json-schema] [-l]
86498649
[--no-color] [-n] [-o <value>...] [--password <value>] [--path <value>] [-s <value>] [-t <value>] [--verbose]
86508650

86518651
FLAGS
@@ -8658,6 +8658,10 @@ FLAGS
86588658
The environment to apply to the current command.
86598659
[env: SHOPIFY_FLAG_ENVIRONMENT]
86608660

8661+
-j, --json
8662+
Output the result as JSON. Automatically disables color output.
8663+
[env: SHOPIFY_FLAG_JSON]
8664+
86618665
-l, --live
86628666
Pull theme files from your remote live theme. Use --development, --live, or --theme in non-interactive environments.
86638667
[env: SHOPIFY_FLAG_LIVE]
@@ -8715,6 +8719,106 @@ DESCRIPTION
87158719
Retrieves theme files from Shopify.
87168720

87178721
If no theme is specified, then you're prompted to select the theme to pull from the list of the themes in your store.
8722+
8723+
Use `--json` for theme details and the local download path. With multiple environments, the result is an array of
8724+
successful results in requested order, each with its environment name. Failed or skipped environments are reported on
8725+
stderr and omitted; if none succeed, the result is an empty array. Existing exit behavior is unchanged.
8726+
8727+
Output from `--json` conforms to the `ThemePullResult` schema.
8728+
8729+
Use `--json-schema` to print the result, error, and event schemas.
8730+
8731+
```json
8732+
{
8733+
"anyOf": [
8734+
{
8735+
"type": "object",
8736+
"properties": {
8737+
"environment": {
8738+
"type": "string"
8739+
},
8740+
"path": {
8741+
"type": "string"
8742+
},
8743+
"theme": {
8744+
"$ref": "#/definitions/ThemePullTheme"
8745+
}
8746+
},
8747+
"required": [
8748+
"path",
8749+
"theme"
8750+
],
8751+
"additionalProperties": false
8752+
},
8753+
{
8754+
"type": "array",
8755+
"items": {
8756+
"type": "object",
8757+
"properties": {
8758+
"environment": {
8759+
"type": "string"
8760+
},
8761+
"path": {
8762+
"$ref": "#/definitions/ThemePullResult/anyOf/0/properties/path"
8763+
},
8764+
"theme": {
8765+
"$ref": "#/definitions/ThemePullTheme"
8766+
}
8767+
},
8768+
"required": [
8769+
"environment",
8770+
"path",
8771+
"theme"
8772+
],
8773+
"additionalProperties": false
8774+
}
8775+
}
8776+
],
8777+
"title": "ThemePullResult",
8778+
"definitions": {
8779+
"ThemePullTheme": {
8780+
"type": "object",
8781+
"properties": {
8782+
"id": {
8783+
"type": "number"
8784+
},
8785+
"name": {
8786+
"type": "string"
8787+
},
8788+
"role": {
8789+
"type": "string"
8790+
},
8791+
"processing": {
8792+
"type": "boolean"
8793+
},
8794+
"src": {
8795+
"type": "string"
8796+
},
8797+
"shop": {
8798+
"type": "string"
8799+
},
8800+
"editor_url": {
8801+
"type": "string"
8802+
},
8803+
"preview_url": {
8804+
"type": "string"
8805+
}
8806+
},
8807+
"required": [
8808+
"id",
8809+
"name",
8810+
"role",
8811+
"processing",
8812+
"shop",
8813+
"editor_url",
8814+
"preview_url"
8815+
],
8816+
"additionalProperties": false
8817+
}
8818+
},
8819+
"$schema": "http://json-schema.org/draft-07/schema#"
8820+
}
8821+
```
87188822
```
87198823
87208824
## `shopify theme push`

‎packages/cli/oclif.manifest.json‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11170,8 +11170,8 @@
1117011170
"args": {
1117111171
},
1117211172
"customPluginName": "@shopify/theme",
11173-
"description": "Retrieves theme files from Shopify.\n\nIf no theme is specified, then you're prompted to select the theme to pull from the list of the themes in your store.",
11174-
"descriptionWithMarkdown": "Retrieves theme files from Shopify.\n\nIf no theme is specified, then you're prompted to select the theme to pull from the list of the themes in your store.",
11173+
"description": "Retrieves theme files from Shopify.\n\nIf no theme is specified, then you're prompted to select the theme to pull from the list of the themes in your store.\n\nUse `--json` for theme details and the local download path. With multiple environments, the result is an array of successful results in requested order, each with its environment name. Failed or skipped environments are reported on stderr and omitted; if none succeed, the result is an empty array. Existing exit behavior is unchanged.\n\nOutput from `--json` conforms to the `ThemePullResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/ThemePullTheme\"\n }\n },\n \"required\": [\n \"path\",\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"$ref\": \"#/definitions/ThemePullResult/anyOf/0/properties/path\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/ThemePullTheme\"\n }\n },\n \"required\": [\n \"environment\",\n \"path\",\n \"theme\"\n ],\n \"additionalProperties\": false\n }\n }\n ],\n \"title\": \"ThemePullResult\",\n \"definitions\": {\n \"ThemePullTheme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\"\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"src\": {\n \"type\": \"string\"\n },\n \"shop\": {\n \"type\": \"string\"\n },\n \"editor_url\": {\n \"type\": \"string\"\n },\n \"preview_url\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"processing\",\n \"shop\",\n \"editor_url\",\n \"preview_url\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
11174+
"descriptionWithMarkdown": "Retrieves theme files from Shopify.\n\nIf no theme is specified, then you're prompted to select the theme to pull from the list of the themes in your store.\n\nUse `--json` for theme details and the local download path. With multiple environments, the result is an array of successful results in requested order, each with its environment name. Failed or skipped environments are reported on stderr and omitted; if none succeed, the result is an empty array. Existing exit behavior is unchanged.",
1117511175
"enableJsonFlag": false,
1117611176
"flags": {
1117711177
"auth-alias": {
@@ -11217,6 +11217,15 @@
1121711217
"name": "ignore",
1121811218
"type": "option"
1121911219
},
11220+
"json": {
11221+
"allowNo": false,
11222+
"char": "j",
11223+
"description": "Output the result as JSON. Automatically disables color output.",
11224+
"env": "SHOPIFY_FLAG_JSON",
11225+
"hidden": false,
11226+
"name": "json",
11227+
"type": "boolean"
11228+
},
1122011229
"json-schema": {
1122111230
"allowNo": false,
1122211231
"description": "Print the command's JSON schemas.",

‎packages/eslint-plugin-cli/rules/json-output-command-exceptions.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ const commandExceptions = [
6666
'packages/theme/src/cli/commands/theme/package.ts',
6767
'packages/theme/src/cli/commands/theme/preview.ts',
6868
'packages/theme/src/cli/commands/theme/profile.ts',
69-
'packages/theme/src/cli/commands/theme/pull.ts',
7069
'packages/theme/src/cli/commands/theme/share.ts',
7170

7271
// Streaming commands without a single finite result.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import Pull from './pull.js'
2+
import {executeThemePull} from '../../services/pull.js'
3+
import {themePullJsonOutputSchema} from '../../services/pull/types.js'
4+
import {captureStandardStreams} from '../../utilities/test-helpers/stdio.js'
5+
import {describe, expect, test, vi} from 'vitest'
6+
import {Config} from '@oclif/core'
7+
import {AbortError} from '@shopify/cli-kit/node/error'
8+
9+
vi.mock('../../services/pull.js')
10+
const session = {storeFqdn: 'test.myshopify.com', token: 'token'}
11+
12+
describe('theme pull JSON', () => {
13+
test('exposes the schema in help and retains JSON and inherited flags', () => {
14+
expect(Pull.jsonOutputSchema).toBe(themePullJsonOutputSchema)
15+
expect(Pull.description).toContain('ThemePullResult')
16+
expect(Pull.flags.json).toBeDefined()
17+
expect(Pull.baseFlags).toHaveProperty('json-schema')
18+
})
19+
20+
test('executes and writes the result through the real encoder', async () => {
21+
vi.mocked(executeThemePull).mockResolvedValue({
22+
theme: {
23+
id: 1,
24+
name: 'Theme',
25+
role: 'unpublished',
26+
processing: false,
27+
shop: session.storeFqdn,
28+
editor_url: 'editor',
29+
preview_url: 'preview',
30+
},
31+
path: '/theme',
32+
})
33+
const command = new Pull([], new Config({root: '.'}))
34+
const streams = await captureStandardStreams(() => command.command({json: true} as never, session, false))
35+
expect(JSON.parse(streams.stdout)).toEqual({
36+
path: '/theme',
37+
theme: {
38+
id: 1,
39+
name: 'Theme',
40+
role: 'unpublished',
41+
processing: false,
42+
shop: session.storeFqdn,
43+
editor_url: 'editor',
44+
preview_url: 'preview',
45+
},
46+
})
47+
expect(streams.stderr).toBe('')
48+
})
49+
50+
test('retains cancellation without emitting a success result', async () => {
51+
vi.mocked(executeThemePull).mockResolvedValue(undefined)
52+
const command = new Pull([], new Config({root: '.'}))
53+
const streams = await captureStandardStreams(() => command.command({json: true} as never, session, false))
54+
expect(streams.stdout).toBe('')
55+
})
56+
57+
test('propagates download failures without writing a result', async () => {
58+
const failure = new AbortError('download failed')
59+
vi.mocked(executeThemePull).mockRejectedValueOnce(failure)
60+
const command = new Pull([], new Config({root: '.'}))
61+
const streams = await captureStandardStreams(async () => {
62+
await expect(command.command({json: true} as never, session, false)).rejects.toBe(failure)
63+
})
64+
expect(streams.stdout).toBe('')
65+
})
66+
})

‎packages/theme/src/cli/commands/theme/pull.ts‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {globFlags, themeFlags} from '../../flags.js'
22
import ThemeCommand, {RequiredFlags} from '../../utilities/theme-command.js'
3-
import {pull} from '../../services/pull.js'
4-
import {globalFlags} from '@shopify/cli-kit/node/cli'
3+
import {executeThemePull} from '../../services/pull.js'
4+
import {renderThemePullResult, renderThemePullEnvironmentResults} from '../../services/pull/result.js'
5+
import {themePullJsonOutputSchema} from '../../services/pull/types.js'
6+
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
57
import {Flags} from '@oclif/core'
68
import {recordTiming} from '@shopify/cli-kit/node/analytics'
79
import {InferredFlags} from '@oclif/core/interfaces'
@@ -12,16 +14,23 @@ import type {NonTTYFlagRequirement} from '@shopify/cli-kit/node/base-command'
1214

1315
type PullFlags = InferredFlags<typeof Pull.flags>
1416
export default class Pull extends ThemeCommand {
17+
static get jsonOutputSchema() {
18+
return themePullJsonOutputSchema
19+
}
20+
1521
static summary = 'Download your remote theme files locally.'
1622

1723
static descriptionWithMarkdown = `Retrieves theme files from Shopify.
1824
19-
If no theme is specified, then you're prompted to select the theme to pull from the list of the themes in your store.`
25+
If no theme is specified, then you're prompted to select the theme to pull from the list of the themes in your store.
26+
27+
Use \`--json\` for theme details and the local download path. With multiple environments, the result is an array of successful results in requested order, each with its environment name. Failed or skipped environments are reported on stderr and omitted; if none succeed, the result is an empty array. Existing exit behavior is unchanged.`
2028

2129
static description = this.descriptionForHelp()
2230

2331
static flags = {
2432
...globalFlags,
33+
...jsonFlag,
2534
...themeFlags,
2635
...globFlags('download'),
2736
theme: Flags.string({
@@ -69,8 +78,23 @@ If no theme is specified, then you're prompted to select the theme to pull from
6978
context?: {stdout?: Writable; stderr?: Writable},
7079
) {
7180
recordTiming('theme-command:pull')
72-
await pull({...flags, noColor: flags['no-color']}, adminSession, multiEnvironment, context)
81+
const result = await executeThemePull(
82+
{...flags, noColor: flags['no-color']},
83+
adminSession,
84+
multiEnvironment,
85+
context,
86+
)
87+
if (result && !(flags.json && multiEnvironment)) renderThemePullResult(result, flags.json ? 'json' : 'text')
7388
recordTiming('theme-command:pull')
89+
return result
90+
}
91+
92+
protected collectsEnvironmentResults(flags: {json?: boolean}): boolean {
93+
return Boolean(flags.json)
94+
}
95+
96+
protected renderEnvironmentResults(results: {environment: string; result: unknown}[]): void {
97+
renderThemePullEnvironmentResults(results)
7498
}
7599

76100
protected storeAuthScopes(): string[] {

0 commit comments

Comments
 (0)