Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions packages/app/src/cli/commands/app/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {appNamePrompt, createAsNewAppPrompt, selectAppPrompt} from '../../prompt
import {searchForAppsByNameFactory} from '../../services/dev/prompt-helpers.js'
import {isValidName} from '../../models/app/validation/common.js'
import {Flags} from '@oclif/core'
import {globalFlags} from '@shopify/cli-kit/node/cli'
import {globalFlags, requiredIfNonInteractive} from '@shopify/cli-kit/node/cli'
import {resolvePath, cwd} from '@shopify/cli-kit/node/path'
import {addPublicMetadata} from '@shopify/cli-kit/node/metadata'

Expand Down Expand Up @@ -38,12 +38,14 @@ export default class Init extends AppLinkedCommand {
default: async () => cwd(),
hidden: false,
}),
template: Flags.string({
description: `The app template. Accepts one of the following:
template: requiredIfNonInteractive(
Flags.string({
description: `The app template. Accepts one of the following:
- <${visibleTemplates.join('|')}>
- Any GitHub repo with optional branch and subpath, e.g., https://github.com/Shopify/<repository>/[subpath]#[branch]`,
env: 'SHOPIFY_FLAG_TEMPLATE',
}),
env: 'SHOPIFY_FLAG_TEMPLATE',
}),
),
flavor: Flags.string({
description: 'Which flavor of the given template to use.',
env: 'SHOPIFY_FLAG_TEMPLATE_FLAVOR',
Expand Down
12 changes: 12 additions & 0 deletions packages/cli-kit/src/public/node/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
error.skipOclifErrorHandling = true
const {errorHandler} = await import('./error-handler.js')
await errorHandler(error, this.config)
return Errors.handle(error)

Check failure on line 45 in packages/cli-kit/src/public/node/base-command.ts

View workflow job for this annotation

GitHub Actions / Unit tests with Node 26.1.0 in ubuntu-latest

src/cli/commands/app/init.test.ts > Init command > runs init command with default flags

Error: process.exit unexpectedly called with "1" ❯ Object.exit ../../node_modules/.pnpm/@oclif+core@4.5.3/node_modules/@oclif/core/lib/errors/handle.js:23:17 ❯ Object.handle ../../node_modules/.pnpm/@oclif+core@4.5.3/node_modules/@oclif/core/lib/errors/handle.js:58:22 ❯ Init.catch ../cli-kit/src/public/node/base-command.ts:45:19 ❯ Init._run ../../node_modules/.pnpm/@oclif+core@4.5.3/node_modules/@oclif/core/lib/command.js:185:13 ❯ src/cli/commands/app/init.test.ts:68:5
}

protected async init(): Promise<unknown> {
Expand Down Expand Up @@ -105,6 +105,7 @@
let result = await super.parse<TFlags, TGlobalFlags, TArgs>(options, argv)
result = await this.resultWithEnvironment<TFlags, TGlobalFlags, TArgs>(result, options, argv)
await addFromParsedFlags(result.flags)
this.failMissingNonTTYFlags(result.flags, this.nonInteractiveRequiredFlags())
return {...result, ...{argv: result.argv as string[]}}
}

Expand All @@ -130,6 +131,17 @@
})
}

// Collects the names of flags marked with the `requiredIfNonInteractive` factory (see
// `requiredIfNonInteractive` in `cli.ts`). The custom property is read from the live command class
// rather than the cached manifest, where it would have been stripped.
private nonInteractiveRequiredFlags(): string[] {
const ctor = this.constructor as unknown as {flags?: FlagInput; baseFlags?: FlagInput}
const allFlags = {...ctor.baseFlags, ...ctor.flags}
return Object.entries(allFlags)
.filter(([, flag]) => Boolean((flag as {requiredIfNonInteractive?: boolean}).requiredIfNonInteractive))
.map(([name]) => name)
}

private async resultWithEnvironment<
TFlags extends FlagOutput & {path?: string; verbose?: boolean},
TGlobalFlags extends FlagOutput,
Expand Down
26 changes: 26 additions & 0 deletions packages/cli-kit/src/public/node/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,32 @@ export const portFlag = (options: {description?: string; env?: string; hidden?:
return Flags.integer({min: 1, max: 65535, ...options, description})
}

/**
* Marks a flag as required when the CLI runs in a non-interactive terminal (e.g. CI, or piped input).
*
* The flag stays optional in interactive sessions, where the command can prompt for the value. In
* non-interactive sessions `BaseCommand` fails with a clear error if the flag is missing. The factory
* also prepends `(required if non-interactive)` to the flag's description so the requirement shows up
* in `--help`, mirroring how oclif renders `(required)`.
*
* Wrap any oclif flag definition with it, for example:
*
* ```
* template: requiredIfNonInteractive(Flags.string({description: 'The app template.'}))
* ```
* @param flag - A flag definition created with `Flags.string`, `Flags.boolean`, etc.
* @returns The same flag definition, annotated for non-interactive validation and help rendering.
*/
export function requiredIfNonInteractive<TFlag extends {description?: string}>(flag: TFlag): TFlag {
// Mutate the freshly-built flag in place: this keeps the original flag type intact (so command
// flag typings are unchanged) while attaching a custom property the parser ignores but
// `BaseCommand` reads from the live command class at parse time.
const annotated = flag as TFlag & {requiredIfNonInteractive?: boolean}
annotated.requiredIfNonInteractive = true
annotated.description = ['(required if non-interactive)', flag.description].filter(Boolean).join(' ')
return annotated
}

/**
* Clear the CLI cache, used to store some API responses and handle notifications status
*/
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,8 @@ FLAGS
--no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output.
--organization-id=<value> [env: SHOPIFY_FLAG_ORGANIZATION_ID] The organization ID. Your organization ID can be
found in your Dev Dashboard URL: https://dev.shopify.com/dashboard/<organization-id>
--template=<value> [env: SHOPIFY_FLAG_TEMPLATE] The app template. Accepts one of the following:
--template=<value> [env: SHOPIFY_FLAG_TEMPLATE] (required if non-interactive) The app template. Accepts
one of the following:
- <reactRouter|none>
- Any GitHub repo with optional branch and subpath, e.g.,
https://github.com/Shopify/<repository>/[subpath]#[branch]
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2499,7 +2499,7 @@
"type": "option"
},
"template": {
"description": "The app template. Accepts one of the following:\n - <reactRouter|none>\n - Any GitHub repo with optional branch and subpath, e.g., https://github.com/Shopify/<repository>/[subpath]#[branch]",
"description": "(required if non-interactive) The app template. Accepts one of the following:\n - <reactRouter|none>\n - Any GitHub repo with optional branch and subpath, e.g., https://github.com/Shopify/<repository>/[subpath]#[branch]",
"env": "SHOPIFY_FLAG_TEMPLATE",
"hasDynamicHelp": false,
"multiple": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/create-app/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"type": "option"
},
"template": {
"description": "The app template. Accepts one of the following:\n - <reactRouter|none>\n - Any GitHub repo with optional branch and subpath, e.g., https://github.com/Shopify/<repository>/[subpath]#[branch]",
"description": "(required if non-interactive) The app template. Accepts one of the following:\n - <reactRouter|none>\n - Any GitHub repo with optional branch and subpath, e.g., https://github.com/Shopify/<repository>/[subpath]#[branch]",
"env": "SHOPIFY_FLAG_TEMPLATE",
"hasDynamicHelp": false,
"multiple": false,
Expand Down
Loading