-
Notifications
You must be signed in to change notification settings - Fork 112
feat(init): detect agent/CI environments and skip interactive prompts #1264
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
110a396
9244180
0f5863c
fce0445
11bcdb1
23e27c9
4896282
10c2b57
d1285e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,7 @@ import type { TemplateData } from '../utils/starter-templates' | |
| import { existsSync } from 'node:fs' | ||
| import process from 'node:process' | ||
|
|
||
| import { box, cancel, confirm, intro, isCancel, outro, select, spinner, tasks, text } from '@clack/prompts' | ||
| import { box, cancel, confirm, intro, isCancel, note, outro, select, spinner, tasks, text } from '@clack/prompts' | ||
| import { defineCommand } from 'citty' | ||
| import { colors } from 'consola/utils' | ||
| import { downloadTemplate, startShell } from 'giget' | ||
|
|
@@ -104,6 +104,11 @@ export default defineCommand({ | |
| type: 'string', | ||
| description: 'Use Nuxt nightly release channel (3x or latest)', | ||
| }, | ||
| yes: { | ||
| type: 'boolean', | ||
| alias: 'y', | ||
| description: 'Use default values for all prompts', | ||
| }, | ||
| }, | ||
| async run(ctx) { | ||
| if (!ctx.args.offline && !ctx.args.preferOffline && !ctx.args.template) { | ||
|
|
@@ -116,6 +121,14 @@ export default defineCommand({ | |
|
|
||
| intro(colors.bold(`Welcome to Nuxt!`.split('').map(m => `${themeColor}${m}`).join(''))) | ||
|
|
||
| const isNonInteractive = ctx.args.yes === true || !hasTTY | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than defaulting immediately to |
||
|
|
||
| if (isNonInteractive) { | ||
| logger.info('Running in non-interactive mode. Prompts will use defaults.') | ||
| } | ||
|
|
||
| const currentPackageManager = detectCurrentPackageManager() | ||
|
|
||
| let availableTemplates: Record<string, TemplateData> = {} | ||
|
|
||
| if (!ctx.args.template || !ctx.args.dir) { | ||
|
|
@@ -139,26 +152,55 @@ export default defineCommand({ | |
| } | ||
| } | ||
|
|
||
| if (isNonInteractive && !ctx.args.dir) { | ||
| const binName = basename(process.argv[1] || 'nuxi').replace(/\.[cm]?js$/, '') || 'nuxi' | ||
| // `create-nuxt` runs init as the root command, `nuxi`/`nuxt` run it as the `init` subcommand | ||
| const usesInitSubcommand = process.argv.slice(2).find(arg => !arg.startsWith('-')) === 'init' | ||
| const initCmd = usesInitSubcommand ? `${binName} init` : binName | ||
| const templateLines = Object.entries(availableTemplates).map(([name, data]) => | ||
| ` ${colors.cyan(name)} ${data?.description ?? ''}${name === DEFAULT_TEMPLATE_NAME ? colors.dim(' (default)') : ''}`, | ||
| ) | ||
|
|
||
| note( | ||
|
atinux marked this conversation as resolved.
|
||
| [ | ||
| 'Available templates:', | ||
| '', | ||
| ...templateLines, | ||
| '', | ||
| `Run ${colors.cyan(`${initCmd} --help`)} for all options.`, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if it should just display the help before the list of templates. I think this could be simplified by merging this message with my suggestion above: if options are missing show the help and the list of templates because it's needed to provide a value |
||
| ].join('\n'), | ||
| 'Non-interactive mode', | ||
| ) | ||
|
atinux marked this conversation as resolved.
|
||
|
|
||
| outro(`Provide a project directory to proceed, e.g: ${initCmd} <dir>`) | ||
| process.exit(2) | ||
| } | ||
|
|
||
| let templateName = ctx.args.template | ||
| if (!templateName) { | ||
| const result = await select({ | ||
| message: 'Which template would you like to use?', | ||
| options: Object.entries(availableTemplates).map(([name, data]) => { | ||
| return { | ||
| value: name, | ||
| label: data ? `${colors.whiteBright(name)} – ${data.description}` : name, | ||
| hint: name === DEFAULT_TEMPLATE_NAME ? 'recommended' : undefined, | ||
| } | ||
| }), | ||
| initialValue: DEFAULT_TEMPLATE_NAME, | ||
| }) | ||
|
|
||
| if (isCancel(result)) { | ||
| cancel('Operation cancelled.') | ||
| process.exit(1) | ||
| if (isNonInteractive) { | ||
| templateName = DEFAULT_TEMPLATE_NAME | ||
| } | ||
| else { | ||
| const result = await select({ | ||
| message: 'Which template would you like to use?', | ||
| options: Object.entries(availableTemplates).map(([name, data]) => { | ||
| return { | ||
| value: name, | ||
| label: data ? `${colors.whiteBright(name)} – ${data.description}` : name, | ||
| hint: name === DEFAULT_TEMPLATE_NAME ? 'recommended' : undefined, | ||
| } | ||
| }), | ||
| initialValue: DEFAULT_TEMPLATE_NAME, | ||
| }) | ||
|
|
||
| templateName = result | ||
| if (isCancel(result)) { | ||
| cancel('Operation cancelled.') | ||
| process.exit(1) | ||
| } | ||
|
|
||
| templateName = result | ||
| } | ||
| } | ||
|
|
||
| // Fallback to default if still not set | ||
|
|
@@ -196,6 +238,14 @@ export default defineCommand({ | |
| // when no `--force` flag is provided | ||
| const shouldVerify = !shouldForce && existsSync(templateDownloadPath) | ||
| if (shouldVerify) { | ||
| if (isNonInteractive) { | ||
| logger.error( | ||
| `Directory ${colors.cyan(relativeToProcess(templateDownloadPath))} already exists. ` | ||
| + `Pass ${colors.cyan('--force')} to override or specify a different directory.`, | ||
| ) | ||
| process.exit(1) | ||
| } | ||
|
|
||
| const selectedAction = await select({ | ||
| message: `The directory ${colors.cyan(relativeToProcess(templateDownloadPath))} already exists. What would you like to do?`, | ||
| options: [ | ||
|
|
@@ -319,7 +369,6 @@ export default defineCommand({ | |
| nightlySpinner.stop(`Updated to nightly version ${colors.cyan(nightlyChannelVersion)}`) | ||
| } | ||
|
|
||
| const currentPackageManager = detectCurrentPackageManager() | ||
| // Resolve package manager | ||
| const packageManagerArg = ctx.args.packageManager as PackageManagerName | ||
| const packageManagerSelectOptions = packageManagerOptions.map(pm => ({ | ||
|
|
@@ -332,6 +381,9 @@ export default defineCommand({ | |
| if (packageManagerOptions.includes(packageManagerArg)) { | ||
| selectedPackageManager = packageManagerArg | ||
| } | ||
| else if (isNonInteractive) { | ||
| selectedPackageManager = currentPackageManager ?? 'npm' | ||
| } | ||
| else { | ||
| const result = await select({ | ||
| message: 'Which package manager would you like to use?', | ||
|
|
@@ -347,25 +399,26 @@ export default defineCommand({ | |
| selectedPackageManager = result | ||
| } | ||
|
|
||
| // Determine if we should init git | ||
| let gitInit: boolean | undefined = ctx.args.gitInit === 'false' as unknown ? false : ctx.args.gitInit | ||
| let gitInit: boolean | undefined = ctx.args.gitInit | ||
| if (gitInit === undefined) { | ||
| const result = await confirm({ | ||
| message: 'Initialize git repository?', | ||
| }) | ||
|
|
||
| if (isCancel(result)) { | ||
| cancel('Operation cancelled.') | ||
| process.exit(1) | ||
| if (isNonInteractive) { | ||
| gitInit = false | ||
| } | ||
| else { | ||
| const result = await confirm({ | ||
| message: 'Initialize git repository?', | ||
| }) | ||
|
|
||
| if (isCancel(result)) { | ||
| cancel('Operation cancelled.') | ||
| process.exit(1) | ||
| } | ||
|
|
||
| gitInit = result | ||
| gitInit = result | ||
| } | ||
| } | ||
|
|
||
| // Install project dependencies and initialize git | ||
| // or skip installation based on the '--no-install' flag | ||
| // citty v0.2.0 with node:util.parseArgs returns 'false' string for --install=false | ||
| if (ctx.args.install === false || (ctx.args.install as unknown) === 'false') { | ||
| if (!ctx.args.install) { | ||
| logger.info('Skipping install dependencies step.') | ||
| } | ||
| else { | ||
|
|
@@ -433,57 +486,59 @@ export default defineCommand({ | |
|
|
||
| // ...or offer to browse and install modules (if not offline) | ||
| else if (!ctx.args.offline && !ctx.args.preferOffline) { | ||
| const modulesPromise = fetchModules() | ||
| const wantsUserModules = await confirm({ | ||
| message: `Would you like to browse and install modules?`, | ||
| initialValue: false, | ||
| }) | ||
| if (!isNonInteractive) { | ||
| const modulesPromise = fetchModules() | ||
| const wantsUserModules = await confirm({ | ||
| message: `Would you like to browse and install modules?`, | ||
| initialValue: false, | ||
| }) | ||
|
|
||
| if (isCancel(wantsUserModules)) { | ||
| cancel('Operation cancelled.') | ||
| process.exit(1) | ||
| } | ||
| if (isCancel(wantsUserModules)) { | ||
| cancel('Operation cancelled.') | ||
| process.exit(1) | ||
| } | ||
|
|
||
| if (wantsUserModules) { | ||
| const modulesSpinner = spinner() | ||
| modulesSpinner.start('Fetching available modules') | ||
| if (wantsUserModules) { | ||
| const modulesSpinner = spinner() | ||
| modulesSpinner.start('Fetching available modules') | ||
|
|
||
| const [response, templateDeps, nuxtVersion] = await Promise.all([ | ||
| modulesPromise, | ||
| getTemplateDependencies(template.dir), | ||
| getNuxtVersion(template.dir), | ||
| ]) | ||
| const [response, templateDeps, nuxtVersion] = await Promise.all([ | ||
| modulesPromise, | ||
| getTemplateDependencies(template.dir), | ||
| getNuxtVersion(template.dir), | ||
| ]) | ||
|
|
||
| modulesSpinner.stop('Modules loaded') | ||
| modulesSpinner.stop('Modules loaded') | ||
|
|
||
| const allModules = response | ||
| .filter(module => | ||
| module.npm !== '@nuxt/devtools' | ||
| && !templateDeps.includes(module.npm) | ||
| && (!module.compatibility.nuxt || checkNuxtCompatibility(module, nuxtVersion)), | ||
| ) | ||
| const allModules = response | ||
| .filter(module => | ||
| module.npm !== '@nuxt/devtools' | ||
| && !templateDeps.includes(module.npm) | ||
| && (!module.compatibility.nuxt || checkNuxtCompatibility(module, nuxtVersion)), | ||
| ) | ||
|
|
||
| if (allModules.length === 0) { | ||
| logger.info('All modules are already included in this template.') | ||
| } | ||
| else { | ||
| const result = await selectModulesAutocomplete({ modules: allModules }) | ||
| if (allModules.length === 0) { | ||
| logger.info('All modules are already included in this template.') | ||
| } | ||
| else { | ||
| const result = await selectModulesAutocomplete({ modules: allModules }) | ||
|
|
||
| if (result.selected.length > 0) { | ||
| const modules = result.selected | ||
| if (result.selected.length > 0) { | ||
| const modules = result.selected | ||
|
|
||
| const allDependencies = Object.fromEntries( | ||
| await Promise.all(modules.map(async module => | ||
| [module, await getModuleDependencies(module)] as const, | ||
| )), | ||
| ) | ||
| const allDependencies = Object.fromEntries( | ||
| await Promise.all(modules.map(async module => | ||
| [module, await getModuleDependencies(module)] as const, | ||
| )), | ||
| ) | ||
|
|
||
| const { toInstall, skipped } = filterModules(modules, allDependencies) | ||
| const { toInstall, skipped } = filterModules(modules, allDependencies) | ||
|
|
||
| if (skipped.length) { | ||
| logger.info(`The following modules are already included as dependencies of another module and will not be installed: ${skipped.map(m => colors.cyan(m)).join(', ')}`) | ||
| if (skipped.length) { | ||
| logger.info(`The following modules are already included as dependencies of another module and will not be installed: ${skipped.map(m => colors.cyan(m)).join(', ')}`) | ||
| } | ||
| modulesToAdd.push(...toInstall) | ||
| } | ||
| modulesToAdd.push(...toInstall) | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.