Skip to content

Commit b869857

Browse files
committed
feat(app-config): add app config pull command
1 parent 3cd97de commit b869857

File tree

3 files changed

+153
-0
lines changed

3 files changed

+153
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import {appFlags} from '../../../flags.js'
2+
import {localAppContext} from '../../../services/app-context.js'
3+
import pull from '../../../services/app/config/pull.js'
4+
import AppUnlinkedCommand, {AppUnlinkedCommandOutput} from '../../../utilities/app-unlinked-command.js'
5+
import {renderSuccess} from '@shopify/cli-kit/node/ui'
6+
import {formatPackageManagerCommand} from '@shopify/cli-kit/node/output'
7+
import {globalFlags} from '@shopify/cli-kit/node/cli'
8+
9+
export default class ConfigPull extends AppUnlinkedCommand {
10+
static summary = 'Refresh an already-linked app configuration without prompts.'
11+
12+
static descriptionWithMarkdown = `Pulls the latest configuration from the already-linked Shopify app and updates the selected configuration file.
13+
14+
This command reuses the existing linked app and organization and skips all interactive prompts. Use \`--config\` to target a specific configuration file, or omit it to use the default one.`
15+
16+
static description = this.descriptionWithoutMarkdown()
17+
18+
static flags = {
19+
...globalFlags,
20+
...appFlags,
21+
}
22+
23+
public async run(): Promise<AppUnlinkedCommandOutput> {
24+
const {flags} = await this.parse(ConfigPull)
25+
26+
// Run the pull service (no prompts)
27+
const {configuration, remoteApp} = await pull({
28+
directory: flags.path,
29+
configName: flags.config,
30+
})
31+
32+
// Get local app context so the return type matches other commands
33+
const app = await localAppContext({
34+
directory: flags.path,
35+
userProvidedConfigName: flags.config,
36+
})
37+
38+
renderSuccess({
39+
headline: `Pulled latest configuration for "${configuration.name}"`,
40+
body: `Updated ${configuration.path ?? flags.config ?? 'app configuration'} using the already-linked app "${
41+
remoteApp.title
42+
}".`,
43+
nextSteps: [
44+
[
45+
'To deploy your updated configuration, run',
46+
{
47+
command: formatPackageManagerCommand(app.packageManager, 'shopify app deploy'),
48+
},
49+
],
50+
],
51+
})
52+
53+
return {app}
54+
}
55+
}

packages/app/src/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Build from './commands/app/build.js'
22
import ConfigLink from './commands/app/config/link.js'
33
import ConfigUse from './commands/app/config/use.js'
4+
import ConfigPull from './commands/app/config/pull.js'
45
import DemoWatcher from './commands/app/demo/watcher.js'
56
import Deploy from './commands/app/deploy.js'
67
import Dev from './commands/app/dev.js'
@@ -47,6 +48,7 @@ export const commands: {[key: string]: typeof AppLinkedCommand | typeof AppUnlin
4748
'app:release': Release,
4849
'app:config:link': ConfigLink,
4950
'app:config:use': ConfigUse,
51+
'app:config:pull': ConfigPull,
5052
'app:env:pull': EnvPull,
5153
'app:env:show': EnvShow,
5254
'app:execute': Execute,
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// packages/app/src/cli/services/app/config/pull.ts
2+
3+
import {LinkOptions, loadLocalAppOptions, overwriteLocalConfigFileWithRemoteAppConfiguration} from './link.js'
4+
import {CurrentAppConfiguration, isCurrentAppSchema} from '../../../models/app/app.js'
5+
import {OrganizationApp} from '../../../models/organization.js'
6+
import {loadApp, AppConfigurationFileName, getAppConfigurationFileName} from '../../../models/app/loader.js'
7+
import {configurationFileNames} from '../../../constants.js'
8+
import {appFromIdentifiers} from '../../context.js'
9+
import {fetchSpecifications} from '../../generate/fetch-extension-specifications.js'
10+
import {RemoteAwareExtensionSpecification} from '../../../models/extensions/specification.js'
11+
import {Flag} from '../../../utilities/developer-platform-client.js'
12+
import {loadLocalExtensionsSpecifications} from '../../../models/extensions/load-specifications.js'
13+
import {AbortError} from '@shopify/cli-kit/node/error'
14+
import {basename} from '@shopify/cli-kit/node/path'
15+
16+
export interface PullOptions {
17+
directory: string
18+
configName?: string
19+
}
20+
21+
export interface PullOutput {
22+
configuration: CurrentAppConfiguration
23+
remoteApp: OrganizationApp
24+
}
25+
26+
/**
27+
* Refresh an already-linked app configuration without prompting for org/app.
28+
*/
29+
export default async function pull(options: PullOptions): Promise<PullOutput> {
30+
const {directory, configName} = options
31+
32+
// 1) Load the current config (default, or the one passed with --config)
33+
const app = await loadApp({
34+
specifications: await loadLocalExtensionsSpecifications(),
35+
directory,
36+
mode: 'report',
37+
userProvidedConfigName: configName,
38+
remoteFlags: undefined,
39+
})
40+
41+
const configuration = app.configuration
42+
43+
if (!isCurrentAppSchema(configuration) || !configuration.client_id) {
44+
throw new AbortError(
45+
'The selected configuration is not linked to a remote app.',
46+
'Run `shopify app config link` first to link this configuration to a Shopify app.',
47+
)
48+
}
49+
50+
// 2) Resolve remote app from the client_id in the config
51+
const remoteApp = await appFromIdentifiers({apiKey: configuration.client_id})
52+
if (!remoteApp) {
53+
throw new AbortError(
54+
'Could not find the remote app linked in this configuration.',
55+
'Try linking the configuration again with `shopify app config link`.',
56+
)
57+
}
58+
59+
const developerPlatformClient = remoteApp.developerPlatformClient
60+
61+
// 3) Fetch remote specs/flags for that app
62+
const specifications: RemoteAwareExtensionSpecification[] = await fetchSpecifications({
63+
developerPlatformClient,
64+
app: remoteApp,
65+
})
66+
const flags: Flag[] = remoteApp.flags
67+
68+
// 4) Reuse helpers from link.ts to build and write the file
69+
const linkOptions: LinkOptions = {
70+
directory,
71+
configName,
72+
developerPlatformClient,
73+
apiKey: configuration.client_id,
74+
}
75+
76+
const localAppOptions = await loadLocalAppOptions(linkOptions, specifications, flags, remoteApp.apiKey)
77+
78+
// Decide which config file to overwrite:
79+
// - if config has a path, reuse that file
80+
// - otherwise, fallback to --config or default app config name
81+
const configFileName: AppConfigurationFileName =
82+
(configuration.path && (basename(configuration.path) as AppConfigurationFileName)) ||
83+
getAppConfigurationFileName(configName ?? configurationFileNames.app)
84+
85+
const mergedConfiguration = await overwriteLocalConfigFileWithRemoteAppConfiguration({
86+
remoteApp,
87+
developerPlatformClient,
88+
specifications,
89+
flags,
90+
configFileName,
91+
appDirectory: localAppOptions.appDirectory ?? directory,
92+
localAppOptions,
93+
})
94+
95+
return {configuration: mergedConfiguration, remoteApp}
96+
}

0 commit comments

Comments
 (0)