mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
When AI agents run `create-next-app` with explicit flags like `--typescript --tailwind --eslint --app --src-dir`, the CLI still enters interactive mode and prompts for any unspecified options: ``` ➜ npx create-next-app my-app --typescript --tailwind --eslint --app --src-dir --use-pnpm ✔ Would you like to use React Compiler? … No / Yes ✔ Would you like to customize the import alias (`@/*` by default)? … No / Yes ? Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? › No / Yes ``` Agents can sometimes answer these interactive prompts correctly, but often they can't. When they fail to navigate the prompts, they fall back to scaffolding the project themselves from scratch — generating files based on stale training data. This means they might initialize an app using deprecated patterns or pin to an older version of Next.js (e.g. 15) instead of the latest. Using `create-next-app` non-interactively is the best way to ensure agents always produce up-to-date scaffolding. Previously, `hasProvidedOptions` only skipped the initial "use recommended defaults?" meta-prompt but still showed individual prompts for each missing option. Now when any config flags are provided, all remaining options use the recommended defaults without prompting. The resolved defaults are printed to stdout so the caller knows exactly what was assumed and which flags to pass to override: ``` Using defaults for unprovided options: --eslint ESLint (use --biome for Biome, --no-eslint for None) --no-react-compiler No React Compiler (use --react-compiler for React Compiler) --agents-md AGENTS.md (use --no-agents-md for No AGENTS.md) --import-alias "@/*" To customize, re-run with explicit flags. ``` The `displayConfig` array is extended with a `flags` field so this output is auto-generated from the same source of truth used for the interactive prompts. Existing behavior for `--yes`, CI mode, and fully interactive mode (no flags) is unchanged.
826 lines
25 KiB
JavaScript
826 lines
25 KiB
JavaScript
#!/usr/bin/env node
|
|
/* eslint-disable import/no-extraneous-dependencies */
|
|
import ciInfo from 'ci-info'
|
|
import { Command } from 'commander'
|
|
import Conf from 'conf'
|
|
import { existsSync } from 'node:fs'
|
|
import { basename, resolve } from 'node:path'
|
|
import { blue, bold, cyan, green, red, yellow } from 'picocolors'
|
|
import type { InitialReturnValue } from 'prompts'
|
|
import prompts from 'prompts'
|
|
import updateCheck from 'update-check'
|
|
import { createApp, DownloadError } from './create-app'
|
|
import type { PackageManager } from './helpers/get-pkg-manager'
|
|
import { getPkgManager } from './helpers/get-pkg-manager'
|
|
import { isFolderEmpty } from './helpers/is-folder-empty'
|
|
import { validateNpmName } from './helpers/validate-pkg'
|
|
import packageJson from './package.json'
|
|
import { Bundler } from './templates'
|
|
|
|
let projectPath: string = ''
|
|
|
|
const handleSigTerm = () => process.exit(0)
|
|
|
|
process.on('SIGINT', handleSigTerm)
|
|
process.on('SIGTERM', handleSigTerm)
|
|
|
|
const onPromptState = (state: {
|
|
value: InitialReturnValue
|
|
aborted: boolean
|
|
exited: boolean
|
|
}) => {
|
|
if (state.aborted) {
|
|
// If we don't re-enable the terminal cursor before exiting
|
|
// the program, the cursor will remain hidden
|
|
process.stdout.write('\x1B[?25h')
|
|
process.stdout.write('\n')
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
const program = new Command(packageJson.name)
|
|
.version(
|
|
packageJson.version,
|
|
'-v, --version',
|
|
'Output the current version of create-next-app.'
|
|
)
|
|
.argument('[directory]')
|
|
.usage('[directory] [options]')
|
|
.helpOption('-h, --help', 'Display this help message.')
|
|
.option('--ts, --typescript', 'Initialize as a TypeScript project. (default)')
|
|
.option('--js, --javascript', 'Initialize as a JavaScript project.')
|
|
.option('--tailwind', 'Initialize with Tailwind CSS config. (default)')
|
|
.option('--react-compiler', 'Initialize with React Compiler enabled.')
|
|
.option('--eslint', 'Initialize with ESLint config.')
|
|
.option('--biome', 'Initialize with Biome config.')
|
|
.option('--app', 'Initialize as an App Router project.')
|
|
.option('--src-dir', "Initialize inside a 'src/' directory.")
|
|
.option('--rspack', 'Enable Rspack as the bundler.')
|
|
.option(
|
|
'--import-alias <prefix/*>',
|
|
'Specify import alias to use (default "@/*").'
|
|
)
|
|
.option('--api', 'Initialize a headless API using the App Router.')
|
|
.option('--empty', 'Initialize an empty project.')
|
|
.option(
|
|
'--use-npm',
|
|
'Explicitly tell the CLI to bootstrap the application using npm.'
|
|
)
|
|
.option(
|
|
'--use-pnpm',
|
|
'Explicitly tell the CLI to bootstrap the application using pnpm.'
|
|
)
|
|
.option(
|
|
'--use-yarn',
|
|
'Explicitly tell the CLI to bootstrap the application using Yarn.'
|
|
)
|
|
.option(
|
|
'--use-bun',
|
|
'Explicitly tell the CLI to bootstrap the application using Bun.'
|
|
)
|
|
.option(
|
|
'--reset, --reset-preferences',
|
|
'Reset the preferences saved for create-next-app.'
|
|
)
|
|
.option(
|
|
'--skip-install',
|
|
'Explicitly tell the CLI to skip installing packages.'
|
|
)
|
|
.option('--yes', 'Use saved preferences or defaults for unprovided options.')
|
|
.option(
|
|
'-e, --example <example-name|github-url>',
|
|
`
|
|
|
|
An example to bootstrap the app with. You can use an example name
|
|
from the official Next.js repo or a public GitHub URL. The URL can use
|
|
any branch and/or subdirectory.
|
|
`
|
|
)
|
|
.option(
|
|
'--example-path <path-to-example>',
|
|
`
|
|
|
|
In a rare case, your GitHub URL might contain a branch name with
|
|
a slash (e.g. bug/fix-1) and the path to the example (e.g. foo/bar).
|
|
In this case, you must specify the path to the example separately:
|
|
--example-path foo/bar
|
|
`
|
|
)
|
|
.option(
|
|
'--agents-md',
|
|
'Include AGENTS.md to guide coding agents to write up-to-date Next.js code. (default)'
|
|
)
|
|
.option('--disable-git', `Skip initializing a git repository.`)
|
|
.action((name) => {
|
|
// Commander does not implicitly support negated options. When they are used
|
|
// by the user they will be interpreted as the positional argument (name) in
|
|
// the action handler. See https://github.com/tj/commander.js/pull/1355
|
|
if (name && !name.startsWith('--no-')) {
|
|
projectPath = name
|
|
}
|
|
})
|
|
.allowUnknownOption()
|
|
.parse(process.argv)
|
|
|
|
const opts = program.opts()
|
|
const { args } = program
|
|
|
|
const packageManager: PackageManager = !!opts.useNpm
|
|
? 'npm'
|
|
: !!opts.usePnpm
|
|
? 'pnpm'
|
|
: !!opts.useYarn
|
|
? 'yarn'
|
|
: !!opts.useBun
|
|
? 'bun'
|
|
: getPkgManager()
|
|
|
|
async function run(): Promise<void> {
|
|
const conf = new Conf({ projectName: 'create-next-app' })
|
|
|
|
if (opts.resetPreferences) {
|
|
const { resetPreferences } = await prompts({
|
|
onState: onPromptState,
|
|
type: 'toggle',
|
|
name: 'resetPreferences',
|
|
message: 'Would you like to reset the saved preferences?',
|
|
initial: false,
|
|
active: 'Yes',
|
|
inactive: 'No',
|
|
})
|
|
if (resetPreferences) {
|
|
conf.clear()
|
|
console.log('The preferences have been reset successfully!')
|
|
}
|
|
process.exit(0)
|
|
}
|
|
|
|
if (typeof projectPath === 'string') {
|
|
projectPath = projectPath.trim()
|
|
}
|
|
|
|
if (!projectPath) {
|
|
const res = await prompts({
|
|
onState: onPromptState,
|
|
type: 'text',
|
|
name: 'path',
|
|
message: 'What is your project named?',
|
|
initial: 'my-app',
|
|
validate: (name) => {
|
|
const validation = validateNpmName(basename(resolve(name)))
|
|
if (validation.valid) {
|
|
return true
|
|
}
|
|
return 'Invalid project name: ' + validation.problems[0]
|
|
},
|
|
})
|
|
|
|
if (typeof res.path === 'string') {
|
|
projectPath = res.path.trim()
|
|
}
|
|
}
|
|
|
|
if (!projectPath) {
|
|
console.log(
|
|
'\nPlease specify the project directory:\n' +
|
|
` ${cyan(opts.name())} ${green('<project-directory>')}\n` +
|
|
'For example:\n' +
|
|
` ${cyan(opts.name())} ${green('my-next-app')}\n\n` +
|
|
`Run ${cyan(`${opts.name()} --help`)} to see all options.`
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
const appPath = resolve(projectPath)
|
|
const appName = basename(appPath)
|
|
|
|
const validation = validateNpmName(appName)
|
|
if (!validation.valid) {
|
|
console.error(
|
|
`Could not create a project called ${red(
|
|
`"${appName}"`
|
|
)} because of npm naming restrictions:`
|
|
)
|
|
|
|
validation.problems.forEach((p) =>
|
|
console.error(` ${red(bold('*'))} ${p}`)
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
if (opts.example === true) {
|
|
console.error(
|
|
'Please provide an example name or url, otherwise remove the example option.'
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
if (existsSync(appPath) && !isFolderEmpty(appPath, appName)) {
|
|
process.exit(1)
|
|
}
|
|
|
|
const example = typeof opts.example === 'string' && opts.example.trim()
|
|
const preferences = (conf.get('preferences') || {}) as Record<
|
|
string,
|
|
boolean | string
|
|
>
|
|
|
|
/**
|
|
* If the user does not provide the necessary flags, prompt them for their
|
|
* preferences, unless `--yes` option was specified, or when running in CI.
|
|
*/
|
|
let skipPrompt = ciInfo.isCI || opts.yes
|
|
let useRecommendedDefaults = false
|
|
|
|
if (!example) {
|
|
const defaults: typeof preferences = {
|
|
typescript: true,
|
|
eslint: false,
|
|
linter: 'eslint',
|
|
tailwind: true,
|
|
app: true,
|
|
srcDir: false,
|
|
importAlias: '@/*',
|
|
customizeImportAlias: false,
|
|
empty: false,
|
|
disableGit: false,
|
|
reactCompiler: false,
|
|
agentsMd: true,
|
|
}
|
|
|
|
type DisplayConfigItem = {
|
|
key: keyof typeof defaults
|
|
values?: Record<string, string>
|
|
flags?: Record<string, string>
|
|
}
|
|
|
|
const displayConfig: DisplayConfigItem[] = [
|
|
{
|
|
key: 'typescript',
|
|
values: { true: 'TypeScript', false: 'JavaScript' },
|
|
flags: { true: '--ts', false: '--js' },
|
|
},
|
|
{
|
|
key: 'linter',
|
|
values: { eslint: 'ESLint', biome: 'Biome', none: 'None' },
|
|
flags: { eslint: '--eslint', biome: '--biome', none: '--no-eslint' },
|
|
},
|
|
{
|
|
key: 'reactCompiler',
|
|
values: { true: 'React Compiler', false: 'No React Compiler' },
|
|
flags: { true: '--react-compiler', false: '--no-react-compiler' },
|
|
},
|
|
{
|
|
key: 'tailwind',
|
|
values: { true: 'Tailwind CSS', false: 'No Tailwind CSS' },
|
|
flags: { true: '--tailwind', false: '--no-tailwind' },
|
|
},
|
|
{
|
|
key: 'srcDir',
|
|
values: { true: 'src/ directory', false: 'No src/ directory' },
|
|
flags: { true: '--src-dir', false: '--no-src-dir' },
|
|
},
|
|
{
|
|
key: 'app',
|
|
values: { true: 'App Router', false: 'Pages Router' },
|
|
flags: { true: '--app', false: '--no-app' },
|
|
},
|
|
{
|
|
key: 'agentsMd',
|
|
values: { true: 'AGENTS.md', false: 'No AGENTS.md' },
|
|
flags: { true: '--agents-md', false: '--no-agents-md' },
|
|
},
|
|
]
|
|
|
|
// Helper to format settings for display based on displayConfig
|
|
const formatSettingsDescription = (
|
|
settings: Record<string, boolean | string>
|
|
) => {
|
|
const descriptions: string[] = []
|
|
|
|
for (const config of displayConfig) {
|
|
const value = settings[config.key]
|
|
|
|
if (config.values) {
|
|
// Look up the display label for this value
|
|
const label = config.values[String(value)]
|
|
if (label) {
|
|
descriptions.push(label)
|
|
}
|
|
}
|
|
}
|
|
|
|
return descriptions.join(', ')
|
|
}
|
|
|
|
// Check if we have saved preferences
|
|
const hasSavedPreferences = Object.keys(preferences).length > 0
|
|
|
|
// Check if user provided any configuration flags
|
|
// If they did, skip all prompts and use recommended defaults for unspecified
|
|
// options. This is critical for AI agents, which pass flags like
|
|
// --typescript --tailwind --app and expect the rest to use sensible defaults
|
|
// without entering interactive mode.
|
|
const hasProvidedOptions = process.argv.some((arg) => arg.startsWith('--'))
|
|
|
|
if (!skipPrompt && hasProvidedOptions) {
|
|
skipPrompt = true
|
|
useRecommendedDefaults = true
|
|
}
|
|
|
|
// Only show the "recommended defaults" prompt if:
|
|
// - Not in CI and not using --yes flag
|
|
// - User hasn't provided any custom options
|
|
if (!skipPrompt && !hasProvidedOptions) {
|
|
const choices: Array<{
|
|
title: string
|
|
value: string
|
|
description?: string
|
|
}> = [
|
|
{
|
|
title: 'Yes, use recommended defaults',
|
|
value: 'recommended',
|
|
description: formatSettingsDescription(defaults),
|
|
},
|
|
{
|
|
title: 'No, customize settings',
|
|
value: 'customize',
|
|
description: 'Choose your own preferences',
|
|
},
|
|
]
|
|
|
|
// Add "reuse previous settings" option if we have saved preferences
|
|
if (hasSavedPreferences) {
|
|
const prefDescription = formatSettingsDescription(preferences)
|
|
choices.splice(1, 0, {
|
|
title: 'No, reuse previous settings',
|
|
value: 'reuse',
|
|
description: prefDescription,
|
|
})
|
|
}
|
|
|
|
const { setupChoice } = await prompts(
|
|
{
|
|
type: 'select',
|
|
name: 'setupChoice',
|
|
message: 'Would you like to use the recommended Next.js defaults?',
|
|
choices,
|
|
initial: 0,
|
|
},
|
|
{
|
|
onCancel: () => {
|
|
console.error('Exiting.')
|
|
process.exit(1)
|
|
},
|
|
}
|
|
)
|
|
|
|
if (setupChoice === 'recommended') {
|
|
useRecommendedDefaults = true
|
|
skipPrompt = true
|
|
} else if (setupChoice === 'reuse') {
|
|
skipPrompt = true
|
|
}
|
|
}
|
|
|
|
// If using recommended defaults, populate preferences with defaults
|
|
// This ensures they are saved for reuse next time
|
|
if (useRecommendedDefaults) {
|
|
Object.assign(preferences, defaults)
|
|
}
|
|
|
|
const getPrefOrDefault = (field: string) => {
|
|
// If using recommended defaults, always use hardcoded defaults
|
|
if (useRecommendedDefaults) {
|
|
return defaults[field]
|
|
}
|
|
|
|
// If not using the recommended template, we prefer saved preferences, otherwise defaults.
|
|
return preferences[field] ?? defaults[field]
|
|
}
|
|
|
|
if (!opts.typescript && !opts.javascript) {
|
|
if (skipPrompt) {
|
|
// default to TypeScript in CI as we can't prompt to
|
|
// prevent breaking setup flows
|
|
opts.typescript = getPrefOrDefault('typescript')
|
|
} else {
|
|
const styledTypeScript = blue('TypeScript')
|
|
const { typescript } = await prompts(
|
|
{
|
|
type: 'toggle',
|
|
name: 'typescript',
|
|
message: `Would you like to use ${styledTypeScript}?`,
|
|
initial: getPrefOrDefault('typescript'),
|
|
active: 'Yes',
|
|
inactive: 'No',
|
|
},
|
|
{
|
|
/**
|
|
* User inputs Ctrl+C or Ctrl+D to exit the prompt. We should close the
|
|
* process and not write to the file system.
|
|
*/
|
|
onCancel: () => {
|
|
console.error('Exiting.')
|
|
process.exit(1)
|
|
},
|
|
}
|
|
)
|
|
/**
|
|
* Depending on the prompt response, set the appropriate program flags.
|
|
*/
|
|
opts.typescript = Boolean(typescript)
|
|
opts.javascript = !typescript
|
|
preferences.typescript = Boolean(typescript)
|
|
}
|
|
}
|
|
|
|
// Determine linter choice if not specified via CLI flags
|
|
// Support both --no-linter (new) and --no-eslint (legacy) for backward compatibility
|
|
const noLinter =
|
|
args.includes('--no-linter') || args.includes('--no-eslint')
|
|
|
|
if (!opts.eslint && !opts.biome && !noLinter && !opts.api) {
|
|
if (skipPrompt) {
|
|
const preferredLinter = getPrefOrDefault('linter')
|
|
opts.eslint = preferredLinter === 'eslint'
|
|
opts.biome = preferredLinter === 'biome'
|
|
// No need to set noLinter flag since we check args at runtime
|
|
} else {
|
|
const linterIndexMap = {
|
|
eslint: 0,
|
|
biome: 1,
|
|
none: 2,
|
|
}
|
|
const { linter } = await prompts({
|
|
onState: onPromptState,
|
|
type: 'select',
|
|
name: 'linter',
|
|
message: 'Which linter would you like to use?',
|
|
choices: [
|
|
{
|
|
title: 'ESLint',
|
|
value: 'eslint',
|
|
description: 'More comprehensive lint rules',
|
|
},
|
|
{
|
|
title: 'Biome',
|
|
value: 'biome',
|
|
description: 'Fast formatter and linter (fewer rules)',
|
|
},
|
|
{
|
|
title: 'None',
|
|
value: 'none',
|
|
description: 'Skip linter configuration',
|
|
},
|
|
],
|
|
initial:
|
|
linterIndexMap[
|
|
getPrefOrDefault('linter') as keyof typeof linterIndexMap
|
|
],
|
|
})
|
|
|
|
opts.eslint = linter === 'eslint'
|
|
opts.biome = linter === 'biome'
|
|
preferences.linter = linter
|
|
|
|
// Keep backwards compatibility with old eslint preference
|
|
preferences.eslint = linter === 'eslint'
|
|
}
|
|
} else if (opts.eslint) {
|
|
opts.biome = false
|
|
preferences.linter = 'eslint'
|
|
preferences.eslint = true
|
|
} else if (opts.biome) {
|
|
opts.eslint = false
|
|
preferences.linter = 'biome'
|
|
preferences.eslint = false
|
|
} else if (noLinter) {
|
|
opts.eslint = false
|
|
opts.biome = false
|
|
preferences.linter = 'none'
|
|
preferences.eslint = false
|
|
}
|
|
|
|
if (
|
|
!opts.reactCompiler &&
|
|
!args.includes('--no-react-compiler') &&
|
|
!opts.api
|
|
) {
|
|
if (skipPrompt) {
|
|
opts.reactCompiler = getPrefOrDefault('reactCompiler')
|
|
} else {
|
|
const styledReactCompiler = blue('React Compiler')
|
|
const { reactCompiler } = await prompts({
|
|
onState: onPromptState,
|
|
type: 'toggle',
|
|
name: 'reactCompiler',
|
|
message: `Would you like to use ${styledReactCompiler}?`,
|
|
initial: getPrefOrDefault('reactCompiler'),
|
|
active: 'Yes',
|
|
inactive: 'No',
|
|
})
|
|
opts.reactCompiler = Boolean(reactCompiler)
|
|
preferences.reactCompiler = Boolean(reactCompiler)
|
|
}
|
|
}
|
|
|
|
if (!opts.tailwind && !args.includes('--no-tailwind') && !opts.api) {
|
|
if (skipPrompt) {
|
|
opts.tailwind = getPrefOrDefault('tailwind')
|
|
} else {
|
|
const tw = blue('Tailwind CSS')
|
|
const { tailwind } = await prompts({
|
|
onState: onPromptState,
|
|
type: 'toggle',
|
|
name: 'tailwind',
|
|
message: `Would you like to use ${tw}?`,
|
|
initial: getPrefOrDefault('tailwind'),
|
|
active: 'Yes',
|
|
inactive: 'No',
|
|
})
|
|
opts.tailwind = Boolean(tailwind)
|
|
preferences.tailwind = Boolean(tailwind)
|
|
}
|
|
}
|
|
|
|
if (!opts.srcDir && !args.includes('--no-src-dir')) {
|
|
if (skipPrompt) {
|
|
opts.srcDir = getPrefOrDefault('srcDir')
|
|
} else {
|
|
const styledSrcDir = blue('`src/` directory')
|
|
const { srcDir } = await prompts({
|
|
onState: onPromptState,
|
|
type: 'toggle',
|
|
name: 'srcDir',
|
|
message: `Would you like your code inside a ${styledSrcDir}?`,
|
|
initial: getPrefOrDefault('srcDir'),
|
|
active: 'Yes',
|
|
inactive: 'No',
|
|
})
|
|
opts.srcDir = Boolean(srcDir)
|
|
preferences.srcDir = Boolean(srcDir)
|
|
}
|
|
}
|
|
|
|
if (!opts.app && !args.includes('--no-app') && !opts.api) {
|
|
if (skipPrompt) {
|
|
opts.app = getPrefOrDefault('app')
|
|
} else {
|
|
const styledAppDir = blue('App Router')
|
|
const { app } = await prompts({
|
|
onState: onPromptState,
|
|
type: 'toggle',
|
|
name: 'app',
|
|
message: `Would you like to use ${styledAppDir}? (recommended)`,
|
|
initial: getPrefOrDefault('app'),
|
|
active: 'Yes',
|
|
inactive: 'No',
|
|
})
|
|
opts.app = Boolean(app)
|
|
preferences.app = Boolean(app)
|
|
}
|
|
}
|
|
|
|
const importAliasPattern = /^[^*"]+\/\*\s*$/
|
|
if (
|
|
typeof opts.importAlias !== 'string' ||
|
|
!importAliasPattern.test(opts.importAlias)
|
|
) {
|
|
if (skipPrompt) {
|
|
// We don't use preferences here because the default value is @/* regardless of existing preferences
|
|
opts.importAlias = defaults.importAlias
|
|
} else if (args.includes('--no-import-alias')) {
|
|
opts.importAlias = defaults.importAlias
|
|
} else {
|
|
const styledImportAlias = blue('import alias')
|
|
|
|
const { customizeImportAlias } = await prompts({
|
|
onState: onPromptState,
|
|
type: 'toggle',
|
|
name: 'customizeImportAlias',
|
|
message: `Would you like to customize the ${styledImportAlias} (\`${defaults.importAlias}\` by default)?`,
|
|
initial: getPrefOrDefault('customizeImportAlias'),
|
|
active: 'Yes',
|
|
inactive: 'No',
|
|
})
|
|
|
|
if (!customizeImportAlias) {
|
|
// We don't use preferences here because the default value is @/* regardless of existing preferences
|
|
opts.importAlias = defaults.importAlias
|
|
} else {
|
|
const { importAlias } = await prompts({
|
|
onState: onPromptState,
|
|
type: 'text',
|
|
name: 'importAlias',
|
|
message: `What ${styledImportAlias} would you like configured?`,
|
|
initial: getPrefOrDefault('importAlias'),
|
|
validate: (value) =>
|
|
importAliasPattern.test(value)
|
|
? true
|
|
: 'Import alias must follow the pattern <prefix>/*',
|
|
})
|
|
opts.importAlias = importAlias
|
|
preferences.importAlias = importAlias
|
|
}
|
|
}
|
|
}
|
|
|
|
if (args.includes('--no-agents-md')) {
|
|
opts.agentsMd = false
|
|
} else if (!opts.agentsMd) {
|
|
if (skipPrompt) {
|
|
opts.agentsMd = getPrefOrDefault('agentsMd')
|
|
} else {
|
|
const { agentsMd } = await prompts(
|
|
{
|
|
type: 'toggle',
|
|
name: 'agentsMd',
|
|
message:
|
|
'Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code?',
|
|
initial: getPrefOrDefault('agentsMd'),
|
|
active: 'Yes',
|
|
inactive: 'No',
|
|
},
|
|
{
|
|
onCancel: () => {
|
|
console.error('Exiting.')
|
|
process.exit(1)
|
|
},
|
|
}
|
|
)
|
|
opts.agentsMd = Boolean(agentsMd)
|
|
preferences.agentsMd = Boolean(agentsMd)
|
|
}
|
|
}
|
|
|
|
// When prompts were skipped because flags were provided, print the
|
|
// defaults that were assumed so agents and users know what to override.
|
|
if (hasProvidedOptions && useRecommendedDefaults) {
|
|
const lines: string[] = []
|
|
|
|
for (const config of displayConfig) {
|
|
if (!config.flags || !config.values) continue
|
|
|
|
// Skip options the user already specified explicitly
|
|
const wasExplicit = process.argv.some((arg) =>
|
|
Object.values(config.flags!).includes(arg)
|
|
)
|
|
if (wasExplicit) continue
|
|
|
|
const value = String(defaults[config.key])
|
|
const flag = config.flags[value]
|
|
const label = config.values[value]
|
|
if (!flag || !label) continue
|
|
|
|
// Show alternatives the user could pass instead
|
|
const alts: string[] = []
|
|
for (const [k, f] of Object.entries(config.flags)) {
|
|
if (k !== value && config.values[k]) {
|
|
alts.push(`${f} for ${config.values[k]}`)
|
|
}
|
|
}
|
|
|
|
const altText = alts.length > 0 ? ` (use ${alts.join(', ')})` : ''
|
|
lines.push(` ${flag.padEnd(24)}${label}${altText}`)
|
|
}
|
|
|
|
// Import alias is not a boolean toggle, handle separately
|
|
const hasImportAlias = process.argv.some(
|
|
(arg) =>
|
|
arg.startsWith('--import-alias') ||
|
|
arg.startsWith('--no-import-alias')
|
|
)
|
|
if (!hasImportAlias) {
|
|
lines.push(` ${'--import-alias'.padEnd(24)}"${defaults.importAlias}"`)
|
|
}
|
|
|
|
if (lines.length > 0) {
|
|
console.log(
|
|
'\nUsing defaults for unprovided options:\n\n' +
|
|
lines.join('\n') +
|
|
'\n'
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
const bundler: Bundler = opts.rspack ? Bundler.Rspack : Bundler.Turbopack
|
|
|
|
try {
|
|
await createApp({
|
|
appPath,
|
|
packageManager,
|
|
example: example && example !== 'default' ? example : undefined,
|
|
examplePath: opts.examplePath,
|
|
typescript: opts.typescript,
|
|
tailwind: opts.tailwind,
|
|
eslint: opts.eslint,
|
|
biome: opts.biome,
|
|
app: opts.app,
|
|
srcDir: opts.srcDir,
|
|
importAlias: opts.importAlias,
|
|
skipInstall: opts.skipInstall,
|
|
empty: opts.empty,
|
|
api: opts.api,
|
|
bundler,
|
|
disableGit: opts.disableGit,
|
|
reactCompiler: opts.reactCompiler,
|
|
agentsMd: opts.agentsMd,
|
|
})
|
|
} catch (reason) {
|
|
if (!(reason instanceof DownloadError)) {
|
|
throw reason
|
|
}
|
|
|
|
const res = await prompts({
|
|
onState: onPromptState,
|
|
type: 'confirm',
|
|
name: 'builtin',
|
|
message:
|
|
`Could not download "${example}" because of a connectivity issue between your machine and GitHub.\n` +
|
|
`Do you want to use the default template instead?`,
|
|
initial: true,
|
|
})
|
|
if (!res.builtin) {
|
|
throw reason
|
|
}
|
|
|
|
await createApp({
|
|
appPath,
|
|
packageManager,
|
|
typescript: opts.typescript,
|
|
eslint: opts.eslint,
|
|
biome: opts.biome,
|
|
tailwind: opts.tailwind,
|
|
app: opts.app,
|
|
srcDir: opts.srcDir,
|
|
importAlias: opts.importAlias,
|
|
skipInstall: opts.skipInstall,
|
|
empty: opts.empty,
|
|
bundler,
|
|
disableGit: opts.disableGit,
|
|
reactCompiler: opts.reactCompiler,
|
|
agentsMd: opts.agentsMd,
|
|
})
|
|
}
|
|
conf.set('preferences', preferences)
|
|
}
|
|
|
|
// Determine the appropriate dist-tag to check for updates.
|
|
// For prerelease versions like "16.1.1-canary.32", extract "canary" and check
|
|
// against that dist-tag. This ensures canary users are notified about newer
|
|
// canary releases, not incorrectly prompted to "update" to stable.
|
|
function getDistTag(version: string): string {
|
|
const prereleaseMatch = version.match(/-([a-z]+)/)
|
|
return prereleaseMatch ? prereleaseMatch[1] : 'latest'
|
|
}
|
|
|
|
const update = updateCheck(packageJson, {
|
|
distTag: getDistTag(packageJson.version),
|
|
}).catch(() => null)
|
|
|
|
async function notifyUpdate(): Promise<void> {
|
|
try {
|
|
if ((await update)?.latest) {
|
|
const global = {
|
|
npm: 'npm i -g',
|
|
yarn: 'yarn global add',
|
|
pnpm: 'pnpm add -g',
|
|
bun: 'bun add -g',
|
|
}
|
|
const distTag = getDistTag(packageJson.version)
|
|
const pkgTag = distTag === 'latest' ? '' : `@${distTag}`
|
|
const updateMessage = `${global[packageManager]} create-next-app${pkgTag}`
|
|
console.log(
|
|
yellow(bold('A new version of `create-next-app` is available!')) +
|
|
'\n' +
|
|
'You can update by running: ' +
|
|
cyan(updateMessage) +
|
|
'\n'
|
|
)
|
|
}
|
|
process.exit(0)
|
|
} catch {
|
|
// ignore error
|
|
}
|
|
}
|
|
|
|
async function exit(reason: { command?: string }) {
|
|
console.log()
|
|
console.log('Aborting installation.')
|
|
if (reason.command) {
|
|
console.log(` ${cyan(reason.command)} has failed.`)
|
|
} else {
|
|
console.log(
|
|
red('Unexpected error. Please report it as a bug:') + '\n',
|
|
reason
|
|
)
|
|
}
|
|
console.log()
|
|
await notifyUpdate()
|
|
process.exit(1)
|
|
}
|
|
|
|
run().then(notifyUpdate).catch(exit)
|