diff --git a/packages/create-nuxt/test/init.spec.ts b/packages/create-nuxt/test/init.spec.ts
index ff931946e..2bc77a05d 100644
--- a/packages/create-nuxt/test/init.spec.ts
+++ b/packages/create-nuxt/test/init.spec.ts
@@ -11,6 +11,37 @@ import { describe, expect, it } from 'vitest'
const fixtureDir = fileURLToPath(new URL('../../../playground', import.meta.url))
const createNuxt = fileURLToPath(new URL('../bin/create-nuxt.mjs', import.meta.url))
+describe('non-interactive mode', () => {
+ it('should exit with code 2 when no dir is provided and no TTY', { timeout: isWindows ? 200000 : 50000 }, async () => {
+ const result = await x(createNuxt, ['--yes', '--preferOffline'], {
+ throwOnError: false,
+ nodeOptions: { stdio: 'pipe', cwd: fixtureDir },
+ })
+
+ expect(result.exitCode).toBe(2)
+ expect(result.stdout).toContain('Non-interactive mode')
+ expect(result.stdout).toContain('create-nuxt
')
+ })
+
+ it('should proceed without prompts when dir is provided with --yes', { timeout: isWindows ? 200000 : 50000 }, async () => {
+ const dir = tmpdir()
+ const installPath = join(dir, 'non-interactive-test')
+
+ await rm(installPath, { recursive: true, force: true })
+ try {
+ await x(createNuxt, [installPath, '--yes', '--template=minimal', '--no-gitInit', '--preferOffline', '--no-install'], {
+ throwOnError: true,
+ nodeOptions: { stdio: 'pipe', cwd: fixtureDir },
+ })
+
+ expect(existsSync(join(installPath, 'package.json'))).toBeTruthy()
+ }
+ finally {
+ await rm(installPath, { recursive: true, force: true })
+ }
+ })
+})
+
describe('init command package name slugification', () => {
it('should slugify directory names with special characters', { timeout: isWindows ? 200000 : 50000 }, async () => {
const dir = tmpdir()
@@ -19,7 +50,7 @@ describe('init command package name slugification', () => {
await rm(installPath, { recursive: true, force: true })
try {
- await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], {
+ await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--no-gitInit', '--preferOffline', '--no-install'], {
throwOnError: true,
nodeOptions: { stdio: 'inherit', cwd: fixtureDir },
})
@@ -47,7 +78,7 @@ describe('init command package name slugification', () => {
await rm(installPath, { recursive: true, force: true })
try {
- await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], {
+ await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--no-gitInit', '--preferOffline', '--no-install'], {
throwOnError: true,
nodeOptions: { stdio: 'inherit', cwd: fixtureDir },
})
@@ -74,7 +105,7 @@ describe('init command package name slugification', () => {
await rm(installPath, { recursive: true, force: true })
try {
- await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], {
+ await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--no-gitInit', '--preferOffline', '--no-install'], {
throwOnError: true,
nodeOptions: { stdio: 'inherit', cwd: fixtureDir },
})
@@ -100,7 +131,7 @@ describe('init command package name slugification', () => {
await rm(installPath, { recursive: true, force: true })
try {
- await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], {
+ await x(createNuxt, [installPath, '--packageManager=pnpm', '--template=minimal', '--no-gitInit', '--preferOffline', '--no-install'], {
throwOnError: true,
nodeOptions: { stdio: 'inherit', cwd: fixtureDir },
})
diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts
index ecae0e43c..9f79534e8 100644
--- a/packages/nuxi/src/commands/init.ts
+++ b/packages/nuxi/src/commands/init.ts
@@ -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
+
+ if (isNonInteractive) {
+ logger.info('Running in non-interactive mode. Prompts will use defaults.')
+ }
+
+ const currentPackageManager = detectCurrentPackageManager()
+
let availableTemplates: Record = {}
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(
+ [
+ 'Available templates:',
+ '',
+ ...templateLines,
+ '',
+ `Run ${colors.cyan(`${initCmd} --help`)} for all options.`,
+ ].join('\n'),
+ 'Non-interactive mode',
+ )
+
+ outro(`Provide a project directory to proceed, e.g: ${initCmd} `)
+ 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)
}
}
}
diff --git a/packages/nuxt-cli/test/e2e/commands.spec.ts b/packages/nuxt-cli/test/e2e/commands.spec.ts
index 2cb827ffd..46e7d6caa 100644
--- a/packages/nuxt-cli/test/e2e/commands.spec.ts
+++ b/packages/nuxt-cli/test/e2e/commands.spec.ts
@@ -72,7 +72,7 @@ describe('commands', () => {
})
// Test that server responds
- const response = await fetchWithPolling(`http://127.0.0.1:${port}`)
+ const response = await fetchWithPolling(`http://127.0.0.1:${port}`, {}, 30, 300)
expect.soft(response?.status).toBe(200)
previewProcess.kill()
@@ -121,7 +121,7 @@ describe('commands', () => {
await rm(installPath, { recursive: true, force: true })
try {
- await x(nuxi, ['init', installPath, `--packageManager=${pm}`, '--template=minimal', '--gitInit=false', '--preferOffline', '--install=false'], {
+ await x(nuxi, ['init', installPath, `--packageManager=${pm}`, '--template=minimal', '--no-gitInit', '--preferOffline', '--no-install'], {
throwOnError: true,
nodeOptions: { stdio: 'inherit', cwd: fixtureDir },
})