From 110a396e04dc40f9c9f5092d9aa28c24e18b25a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 18:31:24 +0000 Subject: [PATCH 1/5] feat(init): detect agent/CI environments and skip interactive prompts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses `std-env` v4's `isAgent`, `isCI`, and `hasTTY` to automatically detect when the CLI is run non-interactively (AI agents, CI pipelines, piped input) and apply sensible defaults instead of hanging on prompts. Adds two explicit flags for opt-in: - `--defaults` / `-y`: accept all defaults (like `npm init -y`) - `--no-interactive`: same, more explicit for scripting contexts When non-interactive mode is detected, the CLI: - Logs the reason (agent name, CI, no TTY, or flag) - Displays a full options reference — including all available templates fetched live — so agents can discover flags and re-run with custom settings - Auto-selects: template=minimal, dir=template's defaultDir, package manager=detected or npm, gitInit=false, modules=skipped - Fails fast (instead of hanging) when the target directory already exists, instructing the caller to pass `--force` All existing interactive behaviour is preserved when a TTY is present and none of the new flags are set. https://claude.ai/code/session_01LsDZBSg7peDmxh6QW33ag8 --- packages/nuxi/src/commands/init.ts | 247 ++++++++++++++++++++--------- 1 file changed, 170 insertions(+), 77 deletions(-) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index ecae0e43..a456a587 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' @@ -13,7 +13,7 @@ import { installDependencies } from 'nypm' import { $fetch } from 'ofetch' import { basename, join, relative, resolve } from 'pathe' import { findFile, readPackageJSON, writePackageJSON } from 'pkg-types' -import { hasTTY } from 'std-env' +import { agent, hasTTY, isAgent, isCI } from 'std-env' import { x } from 'tinyexec' import { runCommand } from '../run' @@ -104,6 +104,16 @@ export default defineCommand({ type: 'string', description: 'Use Nuxt nightly release channel (3x or latest)', }, + defaults: { + type: 'boolean', + alias: 'y', + description: 'Use defaults for all prompts (useful for CI/agent environments)', + }, + interactive: { + type: 'boolean', + default: true, + negativeDescription: 'Disable interactive prompts and use defaults', + }, }, async run(ctx) { if (!ctx.args.offline && !ctx.args.preferOffline && !ctx.args.template) { @@ -116,9 +126,28 @@ export default defineCommand({ intro(colors.bold(`Welcome to Nuxt!`.split('').map(m => `${themeColor}${m}`).join(''))) + // Detect non-interactive environments: agent, CI, no TTY, or explicit flags + const isNonInteractive + = ctx.args.defaults === true + || ctx.args.interactive === false + || isAgent // AI coding agent (Claude, Cursor, Copilot, Devin, Gemini…) + || isCI // CI/CD pipeline + || !hasTTY // no terminal attached (piped, subprocess, etc.) + + if (isNonInteractive) { + const reason = ctx.args.defaults || ctx.args.interactive === false + ? 'flag' + : isAgent + ? `agent (${agent})` + : isCI + ? 'CI environment' + : 'no TTY' + logger.info(`Running in non-interactive mode (${reason}). Prompts will use defaults.`) + } + let availableTemplates: Record = {} - if (!ctx.args.template || !ctx.args.dir) { + if (!ctx.args.template || !ctx.args.dir || isNonInteractive) { const defaultTemplates = await import('../data/templates').then(r => r.templates) if (ctx.args.offline || ctx.args.preferOffline) { // In offline mode, use static templates directly @@ -139,26 +168,61 @@ export default defineCommand({ } } + // In non-interactive mode, print all available options so agents/scripts + // can discover flags and re-run with custom settings. + if (isNonInteractive) { + const templateLines = Object.entries(availableTemplates).map(([name, data]) => + ` ${colors.cyan(name.padEnd(18))} ${data?.description ?? ''}`, + ) + + note( + [ + colors.bold('Options (re-run with any of these to customise):'), + '', + `${colors.cyan('--template')} `, + ...templateLines, + '', + `${colors.cyan('')} Project directory ${colors.dim('(default: nuxt-app)')}`, + `${colors.cyan('--packageManager')} npm | pnpm | yarn | bun | deno ${colors.dim('(default: auto-detected)')}`, + `${colors.cyan('--gitInit')} / ${colors.cyan('--no-gitInit')} Initialise git repo ${colors.dim('(default: false)')}`, + `${colors.cyan('--install')} / ${colors.cyan('--no-install')} Install dependencies ${colors.dim('(default: true)')}`, + `${colors.cyan('--modules')} Nuxt modules to install ${colors.dim('(comma-separated npm names)')}`, + `${colors.dim(' e.g. @nuxtjs/tailwindcss,@nuxt/image,@nuxt/content')}`, + `${colors.dim(' Full list: https://nuxt.com/modules')}`, + `${colors.cyan('--no-modules')} Skip module prompt`, + `${colors.cyan('--force')} Override existing directory`, + `${colors.cyan('--offline')} Use cached templates`, + ].join('\n'), + 'Available options', + ) + } + 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 + logger.info(`Auto-selected template: ${colors.cyan(templateName)}`) } + 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, + }) + + if (isCancel(result)) { + cancel('Operation cancelled.') + process.exit(1) + } - templateName = result + templateName = result + } } // Fallback to default if still not set @@ -172,18 +236,24 @@ export default defineCommand({ let dir = ctx.args.dir if (dir === '') { const defaultDir = availableTemplates[templateName]?.defaultDir || 'nuxt-app' - const result = await text({ - message: 'Where would you like to create your project?', - placeholder: `./${defaultDir}`, - defaultValue: defaultDir, - }) - - if (isCancel(result)) { - cancel('Operation cancelled.') - process.exit(1) + if (isNonInteractive) { + dir = defaultDir + logger.info(`Auto-selected directory: ${colors.cyan(dir)}`) } + else { + const result = await text({ + message: 'Where would you like to create your project?', + placeholder: `./${defaultDir}`, + defaultValue: defaultDir, + }) + + if (isCancel(result)) { + cancel('Operation cancelled.') + process.exit(1) + } - dir = result + dir = result + } } const cwd = resolve(ctx.args.cwd) @@ -196,6 +266,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: [ @@ -332,6 +410,10 @@ export default defineCommand({ if (packageManagerOptions.includes(packageManagerArg)) { selectedPackageManager = packageManagerArg } + else if (isNonInteractive) { + selectedPackageManager = currentPackageManager ?? 'npm' + logger.info(`Auto-selected package manager: ${colors.cyan(selectedPackageManager)}`) + } else { const result = await select({ message: 'Which package manager would you like to use?', @@ -350,16 +432,22 @@ export default defineCommand({ // Determine if we should init git let gitInit: boolean | undefined = ctx.args.gitInit === 'false' as unknown ? false : 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 + logger.info(`Auto-selected git init: ${colors.cyan('false')}`) } + else { + const result = await confirm({ + message: 'Initialize git repository?', + }) - gitInit = result + if (isCancel(result)) { + cancel('Operation cancelled.') + process.exit(1) + } + + gitInit = result + } } // Install project dependencies and initialize git @@ -433,57 +521,62 @@ 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 (isCancel(wantsUserModules)) { - cancel('Operation cancelled.') - process.exit(1) + if (isNonInteractive) { + logger.info(`Auto-skipping module browser. Use ${colors.cyan('--modules=')} to install modules.`) } + else { + const modulesPromise = fetchModules() + const wantsUserModules = await confirm({ + message: `Would you like to browse and install modules?`, + initialValue: false, + }) - if (wantsUserModules) { - const modulesSpinner = spinner() - modulesSpinner.start('Fetching available modules') + if (isCancel(wantsUserModules)) { + cancel('Operation cancelled.') + process.exit(1) + } - const [response, templateDeps, nuxtVersion] = await Promise.all([ - modulesPromise, - getTemplateDependencies(template.dir), - getNuxtVersion(template.dir), - ]) + if (wantsUserModules) { + const modulesSpinner = spinner() + modulesSpinner.start('Fetching available modules') - modulesSpinner.stop('Modules loaded') + const [response, templateDeps, nuxtVersion] = await Promise.all([ + modulesPromise, + getTemplateDependencies(template.dir), + getNuxtVersion(template.dir), + ]) - const allModules = response - .filter(module => - module.npm !== '@nuxt/devtools' - && !templateDeps.includes(module.npm) - && (!module.compatibility.nuxt || checkNuxtCompatibility(module, nuxtVersion)), - ) + modulesSpinner.stop('Modules loaded') - if (allModules.length === 0) { - logger.info('All modules are already included in this template.') - } - else { - const result = await selectModulesAutocomplete({ modules: allModules }) + const allModules = response + .filter(module => + module.npm !== '@nuxt/devtools' + && !templateDeps.includes(module.npm) + && (!module.compatibility.nuxt || checkNuxtCompatibility(module, nuxtVersion)), + ) - if (result.selected.length > 0) { - const modules = result.selected + if (allModules.length === 0) { + logger.info('All modules are already included in this template.') + } + else { + const result = await selectModulesAutocomplete({ modules: allModules }) - const allDependencies = Object.fromEntries( - await Promise.all(modules.map(async module => - [module, await getModuleDependencies(module)] as const, - )), - ) + 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 { 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) } } } From 924418065cde8f97ba0738bd0f3187c098547ccc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 18:43:25 +0000 Subject: [PATCH 2/5] fix(init): consolidate non-interactive output before project creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move detectCurrentPackageManager() before template load so package manager is known upfront - Compute all effective defaults (template, dir, pm, gitInit, install, modules) before any action is taken and display them together in a 'Proceeding with:' section at the bottom of the options note - Show all available templates with the default marked (← default) - Fix module examples to use @nuxt/content,@nuxt/ui,@nuxt/image - Remove scattered 'Auto-selected X' log lines — they are now covered by the single consolidated note - Simplify the non-interactive module-skip branch https://claude.ai/code/session_01LsDZBSg7peDmxh6QW33ag8 --- packages/nuxi/src/commands/init.ts | 59 ++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index a456a587..9094f784 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -145,6 +145,9 @@ export default defineCommand({ logger.info(`Running in non-interactive mode (${reason}). Prompts will use defaults.`) } + // Detect current package manager early so it can be shown in the options note + const currentPackageManager = detectCurrentPackageManager() + let availableTemplates: Record = {} if (!ctx.args.template || !ctx.args.dir || isNonInteractive) { @@ -168,30 +171,56 @@ export default defineCommand({ } } - // In non-interactive mode, print all available options so agents/scripts - // can discover flags and re-run with custom settings. + // In non-interactive mode, print all available options and the exact values + // that will be used this run, so agents can discover flags and re-run with + // custom settings. if (isNonInteractive) { + // Compute every effective value upfront so the agent sees the full picture + // before any action is taken. + const effectiveTemplate = ctx.args.template || DEFAULT_TEMPLATE_NAME + const effectiveDir = ctx.args.dir || availableTemplates[effectiveTemplate]?.defaultDir || 'nuxt-app' + const effectivePM: PackageManagerName + = (packageManagerOptions.includes(ctx.args.packageManager as PackageManagerName) + ? ctx.args.packageManager as PackageManagerName + : undefined) + ?? currentPackageManager + ?? 'npm' + const effectiveGitInit = (ctx.args.gitInit as unknown) === 'false' ? false : (ctx.args.gitInit ?? false) + const effectiveInstall = ctx.args.install !== false && (ctx.args.install as unknown) !== 'false' + const effectiveModules = ctx.args.modules === undefined + ? '(none)' + : !ctx.args.modules + ? '(skipped)' + : ctx.args.modules as string + const templateLines = Object.entries(availableTemplates).map(([name, data]) => - ` ${colors.cyan(name.padEnd(18))} ${data?.description ?? ''}`, + ` ${colors.cyan(name.padEnd(18))} ${data?.description ?? ''}${name === DEFAULT_TEMPLATE_NAME ? colors.dim(' ← default') : ''}`, ) note( [ - colors.bold('Options (re-run with any of these to customise):'), + colors.bold('Re-run with any of these flags to customise:'), '', `${colors.cyan('--template')} `, ...templateLines, '', - `${colors.cyan('')} Project directory ${colors.dim('(default: nuxt-app)')}`, - `${colors.cyan('--packageManager')} npm | pnpm | yarn | bun | deno ${colors.dim('(default: auto-detected)')}`, - `${colors.cyan('--gitInit')} / ${colors.cyan('--no-gitInit')} Initialise git repo ${colors.dim('(default: false)')}`, - `${colors.cyan('--install')} / ${colors.cyan('--no-install')} Install dependencies ${colors.dim('(default: true)')}`, - `${colors.cyan('--modules')} Nuxt modules to install ${colors.dim('(comma-separated npm names)')}`, - `${colors.dim(' e.g. @nuxtjs/tailwindcss,@nuxt/image,@nuxt/content')}`, + `${colors.cyan('')} Project directory`, + `${colors.cyan('--packageManager')} npm | pnpm | yarn | bun | deno`, + `${colors.cyan('--gitInit')} / ${colors.cyan('--no-gitInit')} Initialise git repo`, + `${colors.cyan('--install')} / ${colors.cyan('--no-install')} Install dependencies`, + `${colors.cyan('--modules')} e.g. ${colors.dim('@nuxt/content,@nuxt/ui,@nuxt/image')}`, `${colors.dim(' Full list: https://nuxt.com/modules')}`, `${colors.cyan('--no-modules')} Skip module prompt`, `${colors.cyan('--force')} Override existing directory`, `${colors.cyan('--offline')} Use cached templates`, + '', + colors.bold('Proceeding with:'), + ` template: ${colors.cyan(effectiveTemplate)}`, + ` directory: ${colors.cyan(effectiveDir)}`, + ` packageManager: ${colors.cyan(effectivePM)}`, + ` gitInit: ${colors.cyan(String(effectiveGitInit))}`, + ` install: ${colors.cyan(String(effectiveInstall))}`, + ` modules: ${colors.cyan(effectiveModules)}`, ].join('\n'), 'Available options', ) @@ -201,7 +230,6 @@ export default defineCommand({ if (!templateName) { if (isNonInteractive) { templateName = DEFAULT_TEMPLATE_NAME - logger.info(`Auto-selected template: ${colors.cyan(templateName)}`) } else { const result = await select({ @@ -238,7 +266,6 @@ export default defineCommand({ const defaultDir = availableTemplates[templateName]?.defaultDir || 'nuxt-app' if (isNonInteractive) { dir = defaultDir - logger.info(`Auto-selected directory: ${colors.cyan(dir)}`) } else { const result = await text({ @@ -397,7 +424,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 => ({ @@ -412,7 +438,6 @@ export default defineCommand({ } else if (isNonInteractive) { selectedPackageManager = currentPackageManager ?? 'npm' - logger.info(`Auto-selected package manager: ${colors.cyan(selectedPackageManager)}`) } else { const result = await select({ @@ -434,7 +459,6 @@ export default defineCommand({ if (gitInit === undefined) { if (isNonInteractive) { gitInit = false - logger.info(`Auto-selected git init: ${colors.cyan('false')}`) } else { const result = await confirm({ @@ -521,10 +545,7 @@ export default defineCommand({ // ...or offer to browse and install modules (if not offline) else if (!ctx.args.offline && !ctx.args.preferOffline) { - if (isNonInteractive) { - logger.info(`Auto-skipping module browser. Use ${colors.cyan('--modules=')} to install modules.`) - } - else { + if (!isNonInteractive) { const modulesPromise = fetchModules() const wantsUserModules = await confirm({ message: `Would you like to browse and install modules?`, From 0f5863c50d66864835b9b3715eb83e711c862c36 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 18:45:08 +0000 Subject: [PATCH 3/5] fix(init): exit after showing options when agent provides no directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When running non-interactively (agent/CI/no-TTY) without a project directory, show the available options note then exit cleanly instead of proceeding with defaults. The agent reads the output, chooses the right flags, and re-runs with an explicit . nuxi init → shows options, exits (no project created) nuxi init my-app → creates my-app with defaults nuxi init my-app -t v3 → creates my-app with v3 template https://claude.ai/code/session_01LsDZBSg7peDmxh6QW33ag8 --- packages/nuxi/src/commands/init.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index 9094f784..d1533bff 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -224,6 +224,13 @@ export default defineCommand({ ].join('\n'), 'Available options', ) + + // No project directory was given — the agent now has all the information + // it needs to re-run with explicit flags. Exit without creating anything. + if (!ctx.args.dir) { + outro('Re-run with a project directory to proceed, e.g: nuxi init ') + process.exit(0) + } } let templateName = ctx.args.template From fce0445d0bdda910be88a9bab07796580828de44 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 18:53:36 +0000 Subject: [PATCH 4/5] fix(init): remove redundant isNonInteractive from template loading condition The original condition (!template || !dir) already covers every case where templates need to be loaded for prompts or the options note. Adding isNonInteractive caused an unnecessary network fetch when an agent provided both --template and dir (only to populate a template list in the note that wasn't needed). The 'Proceeding with:' section in the note still works correctly without it. https://claude.ai/code/session_01LsDZBSg7peDmxh6QW33ag8 --- packages/nuxi/src/commands/init.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index d1533bff..d163398f 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -150,7 +150,7 @@ export default defineCommand({ let availableTemplates: Record = {} - if (!ctx.args.template || !ctx.args.dir || isNonInteractive) { + if (!ctx.args.template || !ctx.args.dir) { const defaultTemplates = await import('../data/templates').then(r => r.templates) if (ctx.args.offline || ctx.args.preferOffline) { // In offline mode, use static templates directly From 11bcdb10a61b2ea201633e07556c86e861fd853f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:07:56 +0000 Subject: [PATCH 5/5] [autofix.ci] apply automated fixes --- packages/nuxi/src/commands/init.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index d163398f..24768be7 100644 --- a/packages/nuxi/src/commands/init.ts +++ b/packages/nuxi/src/commands/init.ts @@ -129,10 +129,10 @@ export default defineCommand({ // Detect non-interactive environments: agent, CI, no TTY, or explicit flags const isNonInteractive = ctx.args.defaults === true - || ctx.args.interactive === false - || isAgent // AI coding agent (Claude, Cursor, Copilot, Devin, Gemini…) - || isCI // CI/CD pipeline - || !hasTTY // no terminal attached (piped, subprocess, etc.) + || ctx.args.interactive === false + || isAgent // AI coding agent (Claude, Cursor, Copilot, Devin, Gemini…) + || isCI // CI/CD pipeline + || !hasTTY // no terminal attached (piped, subprocess, etc.) if (isNonInteractive) { const reason = ctx.args.defaults || ctx.args.interactive === false @@ -190,8 +190,8 @@ export default defineCommand({ const effectiveModules = ctx.args.modules === undefined ? '(none)' : !ctx.args.modules - ? '(skipped)' - : ctx.args.modules as string + ? '(skipped)' + : ctx.args.modules as string const templateLines = Object.entries(availableTemplates).map(([name, data]) => ` ${colors.cyan(name.padEnd(18))} ${data?.description ?? ''}${name === DEFAULT_TEMPLATE_NAME ? colors.dim(' ← default') : ''}`,