diff --git a/packages/cli-kit/src/private/node/conf-store.ts b/packages/cli-kit/src/private/node/conf-store.ts index 72b583f1f5f..66ae513b5df 100644 --- a/packages/cli-kit/src/private/node/conf-store.ts +++ b/packages/cli-kit/src/private/node/conf-store.ts @@ -268,12 +268,11 @@ export async function runWithRateLimit(options: RunWithRateLimitOptions, config /** * Get auto-upgrade preference. - * Defaults to true if the preference has never been explicitly set. * - * @returns Whether auto-upgrade is enabled. + * @returns Whether auto-upgrade is enabled, or undefined if never set. */ -export function getAutoUpgradeEnabled(config: LocalStorage = cliKitStore()): boolean { - return config.get('autoUpgradeEnabled') ?? true +export function getAutoUpgradeEnabled(config: LocalStorage = cliKitStore()): boolean | undefined { + return config.get('autoUpgradeEnabled') } /** diff --git a/packages/cli-kit/src/public/node/upgrade.test.ts b/packages/cli-kit/src/public/node/upgrade.test.ts index bb21b077d88..af82d4853de 100644 --- a/packages/cli-kit/src/public/node/upgrade.test.ts +++ b/packages/cli-kit/src/public/node/upgrade.test.ts @@ -230,11 +230,11 @@ describe('versionToAutoUpgrade', () => { expect(versionToAutoUpgrade()).toBeUndefined() }) - test('returns the newer version when auto-upgrade preference has never been set (default enabled)', () => { + test('returns undefined when auto-upgrade preference has never been set', () => { vi.mocked(checkForCachedNewVersion).mockReturnValue('3.91.0') vi.mocked(isCI).mockReturnValue(false) - vi.mocked(getAutoUpgradeEnabled).mockReturnValue(true) - expect(versionToAutoUpgrade()).toBe('3.91.0') + vi.mocked(getAutoUpgradeEnabled).mockReturnValue(undefined) + expect(versionToAutoUpgrade()).toBeUndefined() }) test('returns the newer version for a major version change when auto-upgrade is enabled', () => { diff --git a/packages/cli-kit/src/public/node/upgrade.ts b/packages/cli-kit/src/public/node/upgrade.ts index ccc349a63f1..d690324ab3b 100644 --- a/packages/cli-kit/src/public/node/upgrade.ts +++ b/packages/cli-kit/src/public/node/upgrade.ts @@ -13,6 +13,7 @@ import { import {outputContent, outputDebug, outputInfo, outputToken, outputWarn} from './output.js' import {cwd, moduleDirectory, sniffForPath} from './path.js' import {exec, isCI} from './system.js' +import {renderConfirmationPrompt} from './ui.js' import {isPreReleaseVersion} from './version.js' import {getAutoUpgradeEnabled, setAutoUpgradeEnabled, runAtMinimumInterval} from '../../private/node/conf-store.js' import {CLI_KIT_VERSION} from '../common/version.js' @@ -80,7 +81,7 @@ export async function runCLIUpgrade(): Promise { /** * Returns the version to auto-upgrade to, or undefined if auto-upgrade should be skipped. - * Auto-upgrade is enabled by default and can be disabled via `setAutoUpgradeEnabled(false)`. + * Auto-upgrade is disabled by default and must be enabled via `shopify upgrade`. * Also skips for CI, pre-release versions, or when no newer version is available. * * @returns The version string to upgrade to, or undefined if no upgrade should happen. @@ -153,6 +154,24 @@ export function getOutputUpdateCLIReminder(version: string, isMajor = false): st return base } +/** + * Prompts the user to enable or disable automatic upgrades, then persists their choice. + * + * @returns Whether the user chose to enable auto-upgrade. + */ +export async function promptAutoUpgrade(): Promise { + const current = getAutoUpgradeEnabled() + if (current !== undefined) return current + + const enabled = await renderConfirmationPrompt({ + message: 'Enable automatic updates for Shopify CLI?', + confirmationMessage: 'Yes, automatically update', + cancellationMessage: "No, I'll update manually", + }) + setAutoUpgradeEnabled(enabled) + return enabled +} + async function upgradeLocalShopify(projectDir: string, currentVersion: string) { const packageJson = (await findUpAndReadPackageJson(projectDir)).content const packageJsonDependencies = packageJson.dependencies ?? {} diff --git a/packages/cli/src/cli/commands/config/autoupgrade/constants.ts b/packages/cli/src/cli/commands/config/autoupgrade/constants.ts index aea28e71b97..3c217d9c477 100644 --- a/packages/cli/src/cli/commands/config/autoupgrade/constants.ts +++ b/packages/cli/src/cli/commands/config/autoupgrade/constants.ts @@ -1,4 +1,5 @@ export const autoUpgradeStatus = { on: 'Auto-upgrade on. Shopify CLI will update automatically after each command.', off: "Auto-upgrade off. You'll need to run `shopify upgrade` to update manually.", + notConfigured: 'Auto-upgrade not configured. Run `shopify config autoupgrade on` to enable it.', } as const diff --git a/packages/cli/src/cli/commands/config/autoupgrade/status.test.ts b/packages/cli/src/cli/commands/config/autoupgrade/status.test.ts index 64eb4bf456d..f869a935f7c 100644 --- a/packages/cli/src/cli/commands/config/autoupgrade/status.test.ts +++ b/packages/cli/src/cli/commands/config/autoupgrade/status.test.ts @@ -49,9 +49,9 @@ describe('AutoupgradeStatus', () => { `) }) - test('displays auto-upgrade on message when never explicitly set (default enabled)', async () => { + test('displays not configured message when never set', async () => { // Given - vi.mocked(getAutoUpgradeEnabled).mockReturnValue(true) + vi.mocked(getAutoUpgradeEnabled).mockReturnValue(undefined) const config = new Config({root: __dirname}) const outputMock = mockAndCaptureOutput() outputMock.clear() @@ -63,7 +63,8 @@ describe('AutoupgradeStatus', () => { expect(outputMock.info()).toMatchInlineSnapshot(` "╭─ info ───────────────────────────────────────────────────────────────────────╮ │ │ - │ Auto-upgrade on. Shopify CLI will update automatically after each command. │ + │ Auto-upgrade not configured. Run \`shopify config autoupgrade on\` to enable │ + │ it. │ │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ " diff --git a/packages/cli/src/cli/commands/config/autoupgrade/status.ts b/packages/cli/src/cli/commands/config/autoupgrade/status.ts index dd9f07fd100..c87ddc10dee 100644 --- a/packages/cli/src/cli/commands/config/autoupgrade/status.ts +++ b/packages/cli/src/cli/commands/config/autoupgrade/status.ts @@ -17,7 +17,9 @@ export default class AutoupgradeStatus extends Command { async run(): Promise { const enabled = getAutoUpgradeEnabled() - if (enabled) { + if (enabled === undefined) { + renderInfo({body: autoUpgradeStatus.notConfigured}) + } else if (enabled) { renderInfo({body: autoUpgradeStatus.on}) } else { renderInfo({body: autoUpgradeStatus.off}) diff --git a/packages/cli/src/cli/commands/upgrade.test.ts b/packages/cli/src/cli/commands/upgrade.test.ts index 231d94df2c7..f7a5a44d194 100644 --- a/packages/cli/src/cli/commands/upgrade.test.ts +++ b/packages/cli/src/cli/commands/upgrade.test.ts @@ -1,15 +1,45 @@ import Upgrade from './upgrade.js' -import {runCLIUpgrade} from '@shopify/cli-kit/node/upgrade' -import {describe, test, vi, expect} from 'vitest' +import {promptAutoUpgrade, runCLIUpgrade} from '@shopify/cli-kit/node/upgrade' +import {addPublicMetadata} from '@shopify/cli-kit/node/metadata' +import {describe, test, vi, expect, afterEach} from 'vitest' vi.mock('@shopify/cli-kit/node/upgrade') +vi.mock('@shopify/cli-kit/node/metadata', () => ({ + addPublicMetadata: vi.fn().mockResolvedValue(undefined), +})) + +afterEach(() => { + vi.mocked(addPublicMetadata).mockClear() +}) describe('upgrade command', () => { - test('calls runCLIUpgrade directly without prompting', async () => { + test('calls promptAutoUpgrade and runCLIUpgrade', async () => { + vi.mocked(promptAutoUpgrade).mockResolvedValue(true) vi.mocked(runCLIUpgrade).mockResolvedValue(undefined) await Upgrade.run([], import.meta.url) + expect(promptAutoUpgrade).toHaveBeenCalledOnce() expect(runCLIUpgrade).toHaveBeenCalledOnce() }) + + test('records env_auto_upgrade_accepted=true when user opts in', async () => { + vi.mocked(promptAutoUpgrade).mockResolvedValue(true) + vi.mocked(runCLIUpgrade).mockResolvedValue(undefined) + + await Upgrade.run([], import.meta.url) + + const calls = vi.mocked(addPublicMetadata).mock.calls.map((call) => call[0]()) + expect(calls).toContainEqual(expect.objectContaining({env_auto_upgrade_accepted: true})) + }) + + test('records env_auto_upgrade_accepted=false when user opts out', async () => { + vi.mocked(promptAutoUpgrade).mockResolvedValue(false) + vi.mocked(runCLIUpgrade).mockResolvedValue(undefined) + + await Upgrade.run([], import.meta.url) + + const calls = vi.mocked(addPublicMetadata).mock.calls.map((call) => call[0]()) + expect(calls).toContainEqual(expect.objectContaining({env_auto_upgrade_accepted: false})) + }) }) diff --git a/packages/cli/src/cli/commands/upgrade.ts b/packages/cli/src/cli/commands/upgrade.ts index c8e39ffaf2c..28af7f872fe 100644 --- a/packages/cli/src/cli/commands/upgrade.ts +++ b/packages/cli/src/cli/commands/upgrade.ts @@ -1,5 +1,6 @@ import Command from '@shopify/cli-kit/node/base-command' -import {runCLIUpgrade} from '@shopify/cli-kit/node/upgrade' +import {promptAutoUpgrade, runCLIUpgrade} from '@shopify/cli-kit/node/upgrade' +import {addPublicMetadata} from '@shopify/cli-kit/node/metadata' export default class Upgrade extends Command { static summary = 'Upgrades Shopify CLI.' @@ -9,6 +10,8 @@ export default class Upgrade extends Command { static description = this.descriptionWithoutMarkdown() async run(): Promise { + const accepted = await promptAutoUpgrade() + await addPublicMetadata(() => ({env_auto_upgrade_accepted: accepted})) await runCLIUpgrade() } }