Skip to content

Commit 622b2e0

Browse files
Merge pull request #8407 from Shopify/gonzalo/no-input
Add `--no-input` to all commands
2 parents 3334bc5 + 2322c04 commit 622b2e0

24 files changed

Lines changed: 2007 additions & 186 deletions

File tree

‎.changeset/no-input.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@shopify/cli-kit': minor
3+
'@shopify/cli': minor
4+
---
5+
6+
Add `--no-input` to commands with global flags to disable interactive prompts and browser authentication.

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

Lines changed: 720 additions & 72 deletions
Large diffs are not rendered by default.

‎packages/cli-kit/src/private/node/session/device-authorization.test.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import {err, ok} from '../../../public/node/result.js'
1414
import {AbortError, BugError} from '../../../public/node/error.js'
1515
import {isCI, openURL, sleep} from '../../../public/node/system.js'
1616
import * as output from '../../../public/node/output.js'
17-
18-
import {beforeEach, describe, expect, test, vi} from 'vitest'
17+
import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest'
1918
import {Response} from 'node-fetch'
2019

2120
vi.mock('../../../public/node/context/fqdn.js')
@@ -27,11 +26,14 @@ vi.mock('./exchange.js')
2726
vi.mock('../../../public/node/system.js')
2827

2928
beforeEach(() => {
29+
vi.stubEnv('SHOPIFY_FLAG_NO_INPUT', '')
3030
vi.mocked(isTTY).mockReturnValue(true)
3131
vi.mocked(isCI).mockReturnValue(false)
3232
vi.mocked(openURL).mockResolvedValue(true)
3333
})
3434

35+
afterEach(() => vi.unstubAllEnvs())
36+
3537
describe('requestDeviceAuthorization', () => {
3638
const data: any = {
3739
device_code: 'device_code',
@@ -162,6 +164,19 @@ describe('requestDeviceAuthorization', () => {
162164
expect(outputInfo).not.toHaveBeenCalledWith('👉 Press any key to open the login page on your browser')
163165
})
164166

167+
test('does not open the browser when input is disabled', async () => {
168+
const response = new Response(JSON.stringify(data))
169+
vi.mocked(shopifyFetch).mockResolvedValue(response)
170+
vi.mocked(identityFqdn).mockResolvedValue('fqdn.com')
171+
vi.mocked(clientId).mockReturnValue('clientId')
172+
vi.stubEnv('SHOPIFY_FLAG_NO_INPUT', 'true')
173+
174+
await expect(requestDeviceAuthorization(['scope1', 'scope2'])).rejects.toThrow(
175+
'Authorization is required to continue, but the current environment does not support interactive prompts.',
176+
)
177+
expect(openURL).not.toHaveBeenCalled()
178+
})
179+
165180
test('when the response is not valid JSON, throw an error with context without retrying', async () => {
166181
// Given
167182
const response = new Response('not valid JSON')

‎packages/cli-kit/src/private/node/session/device-authorization.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {isGatewayErrorStatus} from '../api/status-checks.js'
55
import {identityFqdn} from '../../../public/node/context/fqdn.js'
66
import {recordRetry} from '../../../public/node/analytics.js'
77
import {shopifyFetch} from '../../../public/node/http.js'
8+
import {isInputDisabled} from '../../../public/node/no-input.js'
89
import {outputContent, outputDebug, outputInfo, outputToken} from '../../../public/node/output.js'
910
import {AbortError, BugError} from '../../../public/node/error.js'
1011
import {isCI, openURL, sleep} from '../../../public/node/system.js'
@@ -64,7 +65,7 @@ export async function requestDeviceAuthorization(scopes: string[]): Promise<Devi
6465

6566
outputInfo('\nTo run this command, log in to Shopify.')
6667

67-
if (isCI()) {
68+
if (isInputDisabled() || isCI()) {
6869
throw new AbortError(
6970
'Authorization is required to continue, but the current environment does not support interactive prompts.',
7071
'To resolve this, specify credentials in your environment, or run the command in an interactive environment such as your local terminal.',

‎packages/cli-kit/src/public/node/base-command.test.ts‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,6 @@ describe('applying environments', async () => {
346346
test(testName, async () => {
347347
testResult = {}
348348
testError = undefined
349-
350349
await inTemporaryDirectory(async (tmpDir) => {
351350
await writeFile(joinPath(tmpDir, 'shopify.environments.toml'), encodeTOML(allEnvironments as any))
352351
await testFunc(tmpDir)
@@ -603,6 +602,32 @@ describe('applying environments', async () => {
603602
},
604603
)
605604

605+
runTestInTmpDir('provides --no-input to commands through the global flags', async (tmpDir: string) => {
606+
// When
607+
await MockCommand.run(['--path', tmpDir, '--no-input'])
608+
609+
// Then
610+
expect(testError).toBeUndefined()
611+
expect(testResult['no-input']).toBe(true)
612+
})
613+
614+
runTestInTmpDir('treats disabled input as non-interactive in a TTY', async (tmpDir: string) => {
615+
// When
616+
vi.stubEnv('SHOPIFY_FLAG_NO_INPUT', 'true')
617+
await MockCommandWithRequiredFlagInNonTTY.run(['--path', tmpDir, '--no-input'])
618+
619+
// Then
620+
expect(unstyled(testError!.message)).toMatch('Flag not specified:\n\n--nonTTYRequiredFlag')
621+
})
622+
623+
runTestInTmpDir('supports disabling input through the environment', async (tmpDir: string) => {
624+
vi.stubEnv('SHOPIFY_FLAG_NO_INPUT', 'true')
625+
626+
await MockCommandWithRequiredFlagInNonTTY.run(['--path', tmpDir])
627+
628+
expect(unstyled(testError!.message)).toMatch('Flag not specified:\n\n--nonTTYRequiredFlag')
629+
})
630+
606631
runTestInTmpDir('does not throw in TTY mode when a non-TTY required argument is missing', async (tmpDir: string) => {
607632
// Given — simulate interactive terminal
608633
Object.defineProperty(process.stdin, 'isTTY', {value: true, configurable: true, writable: true})
@@ -627,6 +652,14 @@ describe('applying environments', async () => {
627652
expect(unstyled(testError!.message)).toMatch('Flag not specified:\n\n--nonTTYRequiredFlag')
628653
})
629654

655+
runTestInTmpDir('treats piped stdin as non-interactive', async (tmpDir: string) => {
656+
Object.defineProperty(process.stdin, 'isTTY', {value: false, configurable: true, writable: true})
657+
658+
await MockCommandWithRequiredFlagInNonTTY.run(['--path', tmpDir])
659+
660+
expect(unstyled(testError!.message)).toMatch('Flag not specified:\n\n--nonTTYRequiredFlag')
661+
})
662+
630663
runTestInTmpDir('reports all missing declarative non-TTY requirements', async (tmpDir: string) => {
631664
// Given
632665
vi.stubEnv('CI', 'true')

‎packages/cli-kit/src/public/node/cli.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {isTruthy} from './context/utilities.js'
2+
import {noInputFlag} from './no-input.js'
23
import {launchCLI as defaultLaunchCli} from './cli-launcher.js'
34
import {environmentVariables} from '../../private/node/constants.js'
45
import {Flags} from '@oclif/core'
@@ -131,6 +132,7 @@ export async function runCreateCLI(
131132
* are shared across all the commands.
132133
*/
133134
export const globalFlags = {
135+
...noInputFlag,
134136
'no-color': Flags.boolean({
135137
hidden: false,
136138
description: 'Disable color output.',

‎packages/cli-kit/src/public/node/context/local.test.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
} from './local.js'
1414
import {fileExists} from '../fs.js'
1515
import {exec} from '../system.js'
16-
1716
import {afterEach, expect, describe, vi, test} from 'vitest'
1817
import * as os from 'os'
1918
import type {NetworkInterfaceInfo} from 'os'
@@ -34,6 +33,7 @@ describe('isTerminalInteractive', () => {
3433
const originalEnv = {...process.env}
3534

3635
afterEach(() => {
36+
vi.unstubAllEnvs()
3737
process.stdout.isTTY = originalIsTTY
3838
process.env.TERM = originalEnv.TERM
3939
if (originalEnv.CI === undefined) {
@@ -77,6 +77,15 @@ describe('isTerminalInteractive', () => {
7777
process.env.TERM = 'xterm-256color'
7878
expect(isTerminalInteractive()).toBe(false)
7979
})
80+
81+
test('returns false when user input is disabled', () => {
82+
process.stdout.isTTY = true
83+
delete process.env.CI
84+
process.env.TERM = 'xterm-256color'
85+
vi.stubEnv('SHOPIFY_FLAG_NO_INPUT', 'true')
86+
87+
expect(isTerminalInteractive()).toBe(false)
88+
})
8089
})
8190

8291
describe('isUnitTest', () => {

‎packages/cli-kit/src/public/node/context/local.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {isTruthy} from './utilities.js'
2+
import {isInputDisabled} from '../no-input.js'
23
import {getCIMetadata, isSet, Metadata} from '../../../private/node/context/utilities.js'
34
import {defaultThemeKitAccessDomain, environmentVariables, pathConstants} from '../../../private/node/constants.js'
45
import {randomUUID} from 'crypto'
@@ -22,7 +23,7 @@ async function lazyExec(command: string, args: string[]): Promise<void> {
2223
* @returns True if the terminal is interactive.
2324
*/
2425
export function isTerminalInteractive(): boolean {
25-
return Boolean(process.stdout.isTTY && process.env.TERM !== 'dumb' && !('CI' in process.env))
26+
return Boolean(!isInputDisabled() && process.stdout.isTTY && process.env.TERM !== 'dumb' && !('CI' in process.env))
2627
}
2728

2829
/**

‎packages/cli-kit/src/public/node/error-handler.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ vi.mock('./context/local.js')
4343
vi.mock('../../public/node/crypto.js')
4444
vi.mock('../../private/node/context/service.js')
4545
vi.mock('../../private/node/session.js')
46-
vi.mock('@oclif/core', () => ({
46+
vi.mock('@oclif/core', async (importOriginal) => ({
47+
...(await importOriginal<typeof import('@oclif/core')>()),
4748
settings: {
4849
debug: false,
4950
},
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {isInputDisabled} from './no-input.js'
2+
import {describe, expect, test} from 'vitest'
3+
4+
describe('isInputDisabled', () => {
5+
test('returns true when --no-input is present', () => {
6+
expect(isInputDisabled(['node', 'shopify', '--no-input'], {})).toBe(true)
7+
})
8+
9+
test('returns true when SHOPIFY_FLAG_NO_INPUT is enabled', () => {
10+
expect(isInputDisabled([], {SHOPIFY_FLAG_NO_INPUT: 'true'})).toBe(true)
11+
})
12+
13+
test('returns false when input is not disabled', () => {
14+
expect(isInputDisabled([], {})).toBe(false)
15+
})
16+
})

0 commit comments

Comments
 (0)