-
Notifications
You must be signed in to change notification settings - Fork 113
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
Draft
atinux
wants to merge
10
commits into
main
Choose a base branch
from
claude/nuxt-cli-agent-support-sTRzI
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
110a396
feat(init): detect agent/CI environments and skip interactive prompts
claude 9244180
fix(init): consolidate non-interactive output before project creation
claude 0f5863c
fix(init): exit after showing options when agent provides no directory
claude fce0445
fix(init): remove redundant isNonInteractive from template loading co…
claude 11bcdb1
[autofix.ci] apply automated fixes
autofix-ci[bot] 23e27c9
Merge branch 'main' into claude/nuxt-cli-agent-support-sTRzI
atinux 4896282
chore: remove dead code
atinux 10c2b57
chore: update based on feedback
atinux d1285e2
Merge branch 'main' into claude/nuxt-cli-agent-support-sTRzI
atinux 0aca128
Update packages/nuxi/src/commands/init.ts
atinux File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 and skip interactive 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<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) | ||
| } | ||
| } | ||
| } | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Rather than defaulting immediately to
--yesin non-TTY mode, if the option--yesisn't set, show the help so it can be invoked with the proper arguments.Actually, it's a bit more complex, ideally the CLI should detect if it has all the information it needs to run without interactivity, if not, show the help and exit with non-zero (like 2)