From ae143a37c389290559b2acc1901ff21f01111e73 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 21 Apr 2026 17:31:36 +0200 Subject: [PATCH] feat(cli): agent-mode support via once() and batch() Alias @clack/prompts to @posva/clack-prompts so interactive commands work under AI-agent harnesses (Claude Code, Cursor, Aider) that replay the script to answer each prompt via a JSON session file. - Wrap non-idempotent side effects with once(id, fn) so they run a single time across replays: template download + slug rename, nightly version setup, install + git init, module install + nuxt.config update, and the target-dir existence check (otherwise the second replay would see the folder created by the first run). - Collapse independent prompt pairs into batch({...}) so agents answer them in one round-trip: package-manager + git-init in init, nightly-channel + upgrade-method in upgrade. - Give every prompt a stable id across batch and non-batch branches; thread an optional id through selectModulesAutocomplete so its two call sites disambiguate. - Drop the !hasTTY early-exit in selectModulesAutocomplete when running under isAgent() - agents handle the question via the session file without a TTY. --- knip.json | 1 + packages/nuxi/package.json | 2 +- packages/nuxi/src/commands/init.ts | 178 ++++++++++------- .../nuxi/src/commands/module/_autocomplete.ts | 8 +- packages/nuxi/src/commands/module/add.ts | 106 +++++----- packages/nuxi/src/commands/upgrade.ts | 184 ++++++++++-------- packages/nuxt-cli/package.json | 2 +- pnpm-lock.yaml | 27 ++- 8 files changed, 305 insertions(+), 203 deletions(-) diff --git a/knip.json b/knip.json index 5a0ee48d9..b3bc7a7a0 100644 --- a/knip.json +++ b/knip.json @@ -31,6 +31,7 @@ "@types/debug", "@bomb.sh/tab", "@clack/prompts", + "@posva/clack-prompts", "c12", "confbox", "debug", diff --git a/packages/nuxi/package.json b/packages/nuxi/package.json index 79face367..9670bf87e 100644 --- a/packages/nuxi/package.json +++ b/packages/nuxi/package.json @@ -34,7 +34,7 @@ }, "devDependencies": { "@bomb.sh/tab": "^0.0.14", - "@clack/prompts": "^1.2.0", + "@clack/prompts": "npm:@posva/clack-prompts@^1.2.2", "@nuxt/kit": "^4.4.2", "@nuxt/schema": "^4.4.2", "@nuxt/test-utils": "^4.0.0", diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index ecae0e43c..cb69748f5 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 { batch, box, cancel, confirm, intro, isCancel, once, outro, select, spinner, tasks, text } from '@clack/prompts' import { defineCommand } from 'citty' import { colors } from 'consola/utils' import { downloadTemplate, startShell } from 'giget' @@ -142,6 +142,7 @@ export default defineCommand({ let templateName = ctx.args.template if (!templateName) { const result = await select({ + id: 'init:template', message: 'Which template would you like to use?', options: Object.entries(availableTemplates).map(([name, data]) => { return { @@ -173,6 +174,7 @@ export default defineCommand({ if (dir === '') { const defaultDir = availableTemplates[templateName]?.defaultDir || 'nuxt-app' const result = await text({ + id: 'init:dir', message: 'Where would you like to create your project?', placeholder: `./${defaultDir}`, defaultValue: defaultDir, @@ -193,10 +195,12 @@ export default defineCommand({ let shouldForce = Boolean(ctx.args.force) // Prompt the user if the template download directory already exists - // when no `--force` flag is provided - const shouldVerify = !shouldForce && existsSync(templateDownloadPath) + // when no `--force` flag is provided. Cache the existence check so agent + // replays don't see the directory created by a previous iteration. + const shouldVerify = !shouldForce && await once('init:target-dir-exists', () => existsSync(templateDownloadPath)) if (shouldVerify) { const selectedAction = await select({ + id: 'init:dir-exists-action', message: `The directory ${colors.cyan(relativeToProcess(templateDownloadPath))} already exists. What would you like to do?`, options: [ { value: 'override', label: 'Override its contents' }, @@ -217,6 +221,7 @@ export default defineCommand({ case 'different': { const result = await text({ + id: 'init:different-dir', message: 'Please specify a different directory:', }) @@ -237,44 +242,46 @@ export default defineCommand({ } // Download template - let template: DownloadTemplateResult - - const downloadSpinner = spinner() - downloadSpinner.start(`Downloading ${colors.cyan(templateName)} template`) + let template: Pick try { - template = await downloadTemplate(templateName, { - dir: templateDownloadPath, - force: shouldForce, - offline: Boolean(ctx.args.offline), - preferOffline: Boolean(ctx.args.preferOffline), - registry: process.env.NUXI_INIT_REGISTRY || DEFAULT_REGISTRY, - }) - - if (dir.length > 0) { - const path = await findFile('package.json', { - startingFrom: join(templateDownloadPath, 'package.json'), - reverse: true, + template = await once('init:download-template', async () => { + const downloadSpinner = spinner() + downloadSpinner.start(`Downloading ${colors.cyan(templateName)} template`) + + const downloaded = await downloadTemplate(templateName, { + dir: templateDownloadPath, + force: shouldForce, + offline: Boolean(ctx.args.offline), + preferOffline: Boolean(ctx.args.preferOffline), + registry: process.env.NUXI_INIT_REGISTRY || DEFAULT_REGISTRY, }) - if (path) { - const pkg = await readPackageJSON(path, { try: true }) - if (pkg && pkg.name) { - const slug = basename(templateDownloadPath) - .replace(NON_WORD_RE, '-') - .replace(MULTI_DASH_RE, '-') - .replace(LEADING_TRAILING_DASH_RE, '') - if (slug) { - pkg.name = slug - await writePackageJSON(path, pkg) + + if (dir.length > 0) { + const path = await findFile('package.json', { + startingFrom: join(templateDownloadPath, 'package.json'), + reverse: true, + }) + if (path) { + const pkg = await readPackageJSON(path, { try: true }) + if (pkg && pkg.name) { + const slug = basename(templateDownloadPath) + .replace(NON_WORD_RE, '-') + .replace(MULTI_DASH_RE, '-') + .replace(LEADING_TRAILING_DASH_RE, '') + if (slug) { + pkg.name = slug + await writePackageJSON(path, pkg) + } } } } - } - downloadSpinner.stop(`Downloaded ${colors.cyan(template.name)} template`) + downloadSpinner.stop(`Downloaded ${colors.cyan(downloaded.name)} template`) + return { name: downloaded.name, dir: downloaded.dir } + }) } catch (err) { - downloadSpinner.error('Template download failed') if (process.env.DEBUG) { throw err } @@ -283,40 +290,43 @@ export default defineCommand({ } if (ctx.args.nightly !== undefined && !ctx.args.offline && !ctx.args.preferOffline) { - const nightlySpinner = spinner() - nightlySpinner.start('Fetching nightly version info') + await once('init:set-nightly', async () => { + const nightlySpinner = spinner() + nightlySpinner.start('Fetching nightly version info') - const response = await $fetch<{ 'dist-tags': Record }>('https://registry.npmjs.org/nuxt-nightly') - const nightlyChannelTag = ctx.args.nightly || 'latest' + const response = await $fetch<{ 'dist-tags': Record }>('https://registry.npmjs.org/nuxt-nightly') + const nightlyChannelTag = ctx.args.nightly || 'latest' - if (!nightlyChannelTag) { - nightlySpinner.error('Failed to get nightly channel tag') - logger.error(`Error getting nightly channel tag.`) - process.exit(1) - } + if (!nightlyChannelTag) { + nightlySpinner.error('Failed to get nightly channel tag') + logger.error(`Error getting nightly channel tag.`) + process.exit(1) + } - const nightlyChannelVersion = response['dist-tags'][nightlyChannelTag] + const nightlyChannelVersion = response['dist-tags'][nightlyChannelTag] - if (!nightlyChannelVersion) { - nightlySpinner.error('Nightly version not found') - logger.error(`Nightly channel version for tag ${colors.cyan(nightlyChannelTag)} not found.`) - process.exit(1) - } + if (!nightlyChannelVersion) { + nightlySpinner.error('Nightly version not found') + logger.error(`Nightly channel version for tag ${colors.cyan(nightlyChannelTag)} not found.`) + process.exit(1) + } - const nightlyNuxtPackageJsonVersion = `npm:nuxt-nightly@${nightlyChannelVersion}` - const packageJsonPath = resolve(cwd, dir) + const nightlyNuxtPackageJsonVersion = `npm:nuxt-nightly@${nightlyChannelVersion}` + const packageJsonPath = resolve(cwd, dir) - const packageJson = await readPackageJSON(packageJsonPath) + const packageJson = await readPackageJSON(packageJsonPath) - if (packageJson.dependencies && 'nuxt' in packageJson.dependencies) { - packageJson.dependencies.nuxt = nightlyNuxtPackageJsonVersion - } - else if (packageJson.devDependencies && 'nuxt' in packageJson.devDependencies) { - packageJson.devDependencies.nuxt = nightlyNuxtPackageJsonVersion - } + if (packageJson.dependencies && 'nuxt' in packageJson.dependencies) { + packageJson.dependencies.nuxt = nightlyNuxtPackageJsonVersion + } + else if (packageJson.devDependencies && 'nuxt' in packageJson.devDependencies) { + packageJson.devDependencies.nuxt = nightlyNuxtPackageJsonVersion + } - await writePackageJSON(join(packageJsonPath, 'package.json'), packageJson) - nightlySpinner.stop(`Updated to nightly version ${colors.cyan(nightlyChannelVersion)}`) + await writePackageJSON(join(packageJsonPath, 'package.json'), packageJson) + nightlySpinner.stop(`Updated to nightly version ${colors.cyan(nightlyChannelVersion)}`) + return nightlyChannelVersion + }) } const currentPackageManager = detectCurrentPackageManager() @@ -328,12 +338,37 @@ export default defineCommand({ hint: currentPackageManager === pm ? 'current' : undefined, })) - let selectedPackageManager: PackageManagerName - if (packageManagerOptions.includes(packageManagerArg)) { - selectedPackageManager = packageManagerArg + let selectedPackageManager: PackageManagerName | undefined = packageManagerOptions.includes(packageManagerArg) + ? packageManagerArg + : undefined + let gitInit: boolean | undefined = ctx.args.gitInit === 'false' as unknown ? false : ctx.args.gitInit + + // Batch the two independent prompts when both are missing so agents answer them in one round-trip + if (selectedPackageManager === undefined && gitInit === undefined) { + const answers = await batch({ + packageManager: batch.select({ + id: 'init:package-manager', + message: 'Which package manager would you like to use?', + options: packageManagerSelectOptions, + initialValue: currentPackageManager, + }), + gitInit: batch.confirm({ + id: 'init:git-init', + message: 'Initialize git repository?', + }), + }) + + if (isCancel(answers.packageManager) || isCancel(answers.gitInit)) { + cancel('Operation cancelled.') + process.exit(1) + } + + selectedPackageManager = answers.packageManager as PackageManagerName + gitInit = answers.gitInit as boolean } - else { + else if (selectedPackageManager === undefined) { const result = await select({ + id: 'init:package-manager', message: 'Which package manager would you like to use?', options: packageManagerSelectOptions, initialValue: currentPackageManager, @@ -346,11 +381,9 @@ 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 - if (gitInit === undefined) { + else if (gitInit === undefined) { const result = await confirm({ + id: 'init:git-init', message: 'Initialize git repository?', }) @@ -407,7 +440,10 @@ export default defineCommand({ } try { - await tasks(setupTasks) + await once('init:setup-tasks', async () => { + await tasks(setupTasks) + return null + }) } catch (err) { if (process.env.DEBUG) { @@ -435,6 +471,7 @@ export default defineCommand({ else if (!ctx.args.offline && !ctx.args.preferOffline) { const modulesPromise = fetchModules() const wantsUserModules = await confirm({ + id: 'init:browse-modules', message: `Would you like to browse and install modules?`, initialValue: false, }) @@ -467,7 +504,7 @@ export default defineCommand({ logger.info('All modules are already included in this template.') } else { - const result = await selectModulesAutocomplete({ modules: allModules }) + const result = await selectModulesAutocomplete({ id: 'init:modules-select', modules: allModules }) if (result.selected.length > 0) { const modules = result.selected @@ -498,7 +535,10 @@ export default defineCommand({ ctx.args.logLevel ? `--logLevel=${ctx.args.logLevel}` : '', ].filter(Boolean) - await runCommand(addModuleCommand, args) + await once('init:add-modules', async () => { + await runCommand(addModuleCommand, args) + return null + }) } outro(`✨ Nuxt project has been created with the ${colors.cyan(template.name)} template.`) diff --git a/packages/nuxi/src/commands/module/_autocomplete.ts b/packages/nuxi/src/commands/module/_autocomplete.ts index de584a342..53f8e2546 100644 --- a/packages/nuxi/src/commands/module/_autocomplete.ts +++ b/packages/nuxi/src/commands/module/_autocomplete.ts @@ -1,7 +1,7 @@ import type { Option } from '@clack/prompts' import type { NuxtModule } from './_utils' -import { autocompleteMultiselect, isCancel } from '@clack/prompts' +import { autocompleteMultiselect, isAgent, isCancel } from '@clack/prompts' import { byLengthAsc, Fzf } from 'fzf' import { hasTTY } from 'std-env' @@ -12,6 +12,7 @@ const TRAILING_DOT_RE = /\.$/ interface AutocompleteOptions { modules: NuxtModule[] message?: string + id?: string } interface AutocompleteResult { @@ -24,9 +25,9 @@ interface AutocompleteResult { * Returns object with selected module npm package names and cancellation status */ export async function selectModulesAutocomplete(options: AutocompleteOptions): Promise { - const { modules, message = 'Search and select modules:' } = options + const { modules, message = 'Search and select modules:', id } = options - if (!hasTTY) { + if (!hasTTY && !isAgent()) { logger.warn('Interactive module selection requires a TTY. Skipping.') return { selected: [], cancelled: false } } @@ -63,6 +64,7 @@ export async function selectModulesAutocomplete(options: AutocompleteOptions): P } const result = await autocompleteMultiselect({ + id, message, options: clackOptions, filter, diff --git a/packages/nuxi/src/commands/module/add.ts b/packages/nuxi/src/commands/module/add.ts index 0620e5db9..cb52e44e6 100644 --- a/packages/nuxi/src/commands/module/add.ts +++ b/packages/nuxi/src/commands/module/add.ts @@ -8,7 +8,7 @@ import { homedir } from 'node:os' import { join } from 'node:path' import process from 'node:process' -import { cancel, confirm, isCancel, select, spinner } from '@clack/prompts' +import { cancel, confirm, isCancel, once, select, spinner } from '@clack/prompts' import { updateConfig } from 'c12/update' import { defineCommand } from 'citty' import { colors } from 'consola/utils' @@ -80,6 +80,7 @@ export default defineCommand({ logger.warn(`No ${colors.cyan('nuxt')} dependency detected in ${colors.cyan(relativeToProcess(cwd))}.`) const shouldContinue = await confirm({ + id: 'module-add:continue-without-nuxt', message: `Do you want to continue anyway?`, initialValue: false, }) @@ -106,6 +107,7 @@ export default defineCommand({ modulesSpinner.stop('Modules loaded') const result = await selectModulesAutocomplete({ + id: 'module-add:modules-select', modules: compatibleModules, message: 'Search modules to add (Esc to finish):', }) @@ -181,32 +183,34 @@ async function addModules(modules: ResolvedModule[], { skipInstall = false, skip const packageManager = await detectPackageManager(cwd) - const res = await addDependency(notInstalledModules.map(module => module.pkg), { - cwd, - dev: isDev, - installPeerDependencies: true, - packageManager, - workspace: packageManager?.name === 'pnpm' && existsSync(resolve(cwd, 'pnpm-workspace.yaml')), - }).then(() => true).catch( - async (error) => { - logger.error(String(error)) - - const failedModulesList = notInstalledModules.map(module => colors.cyan(module.pkg)).join(', ') - const s = notInstalledModules.length > 1 ? 's' : '' - const result = await confirm({ - message: `Install failed for ${failedModulesList}. Do you want to continue adding the module${s} to ${colors.cyan('nuxt.config')}?`, - initialValue: false, + let shouldContinueAfterInstall = true + try { + await once('module-add:install', async () => { + await addDependency(notInstalledModules.map(module => module.pkg), { + cwd, + dev: isDev, + installPeerDependencies: true, + packageManager, + workspace: packageManager?.name === 'pnpm' && existsSync(resolve(cwd, 'pnpm-workspace.yaml')), }) + return null + }) + } + catch (error) { + logger.error(String(error)) + + const failedModulesList = notInstalledModules.map(module => colors.cyan(module.pkg)).join(', ') + const s = notInstalledModules.length > 1 ? 's' : '' + const result = await confirm({ + id: 'module-add:continue-after-install-failure', + message: `Install failed for ${failedModulesList}. Do you want to continue adding the module${s} to ${colors.cyan('nuxt.config')}?`, + initialValue: false, + }) + + shouldContinueAfterInstall = !isCancel(result) && result === true + } - if (isCancel(result)) { - return false - } - - return result - }, - ) - - if (res !== true) { + if (!shouldContinueAfterInstall) { return } } @@ -214,35 +218,36 @@ async function addModules(modules: ResolvedModule[], { skipInstall = false, skip // Update nuxt.config.ts if (!skipConfig) { - await updateConfig({ - cwd, - configFile: 'nuxt.config', - async onCreate() { - logger.info(`Creating ${colors.cyan('nuxt.config.ts')}`) - - return getDefaultNuxtConfig() - }, - async onUpdate(config) { - if (!config.modules) { - config.modules = [] - } - - for (const resolved of modules) { - if (config.modules.includes(resolved.pkgName)) { - logger.info(`${colors.cyan(resolved.pkgName)} is already in the ${colors.cyan('modules')}`) + await once('module-add:update-config', async () => { + await updateConfig({ + cwd, + configFile: 'nuxt.config', + async onCreate() { + logger.info(`Creating ${colors.cyan('nuxt.config.ts')}`) - continue + return getDefaultNuxtConfig() + }, + async onUpdate(config) { + if (!config.modules) { + config.modules = [] } - logger.info(`Adding ${colors.cyan(resolved.pkgName)} to the ${colors.cyan('modules')}`) + for (const resolved of modules) { + if (config.modules.includes(resolved.pkgName)) { + logger.info(`${colors.cyan(resolved.pkgName)} is already in the ${colors.cyan('modules')}`) - config.modules.push(resolved.pkgName) - } - }, - }).catch((error) => { - logger.error(`Failed to update ${colors.cyan('nuxt.config')}: ${error.message}`) - logger.error(`Please manually add ${colors.cyan(modules.map(module => module.pkgName).join(', '))} to the ${colors.cyan('modules')} in ${colors.cyan('nuxt.config.ts')}`) + continue + } + logger.info(`Adding ${colors.cyan(resolved.pkgName)} to the ${colors.cyan('modules')}`) + + config.modules.push(resolved.pkgName) + } + }, + }).catch((error) => { + logger.error(`Failed to update ${colors.cyan('nuxt.config')}: ${error.message}`) + logger.error(`Please manually add ${colors.cyan(modules.map(module => module.pkgName).join(', '))} to the ${colors.cyan('modules')} in ${colors.cyan('nuxt.config.ts')}`) + }) return null }) } @@ -303,6 +308,7 @@ async function resolveModule(moduleName: string, cwd: string): Promise { const nuxtVersion = await select({ + id: 'upgrade:nightly-channel', message: 'Which nightly Nuxt release channel do you want to install?', options: [ { value: '3.x' as const, label: '3.x' }, @@ -133,10 +134,7 @@ export default defineCommand({ const packagesToUpdate = pkg ? corePackages.filter(p => pkg.dependencies?.[p] || pkg.devDependencies?.[p]) : [] - // Install latest version - const { npmPackages, nuxtVersion } = await getRequiredNewVersion(['nuxt', ...packagesToUpdate], ctx.args.channel) - - // Force install + // Compute forceRemovals up-front so it's available when batching the method select const toRemove = ['node_modules'] const lockFile = normaliseLockFile(workspaceDir, lockFileCandidates) @@ -148,92 +146,126 @@ export default defineCommand({ .map(p => colors.cyan(p)) .join(' and ') + const packageNames = ['nuxt', ...packagesToUpdate] let method: 'force' | 'dedupe' | 'skip' | undefined = ctx.args.force ? 'force' : ctx.args.dedupe ? 'dedupe' : undefined - if (!method) { - const result = await select({ - message: `Would you like to dedupe your lockfile, or recreate ${forceRemovals}? This can fix problems with hoisted dependency versions and ensure you have the most up-to-date dependencies.`, - options: [ - { - label: 'dedupe lockfile', - value: 'dedupe' as const, - hint: 'recommended', - }, - { - label: `recreate ${forceRemovals}`, - value: 'force' as const, - }, - { - label: 'skip', - value: 'skip' as const, - }, - ], - initialValue: 'dedupe' as const, + const methodOptions = [ + { label: 'dedupe lockfile', value: 'dedupe' as const, hint: 'recommended' }, + { label: `recreate ${forceRemovals}`, value: 'force' as const }, + { label: 'skip', value: 'skip' as const }, + ] + const methodMessage = `Would you like to dedupe your lockfile, or recreate ${forceRemovals}? This can fix problems with hoisted dependency versions and ensure you have the most up-to-date dependencies.` + + let npmPackages: string[] + let nuxtVersion: NuxtVersionTag + + // When both channel-selection and method-selection would prompt, batch them so agents answer in one round-trip + if (ctx.args.channel === 'nightly' && !method) { + const answers = await batch({ + nuxtVersion: batch.select({ + id: 'upgrade:nightly-channel', + message: 'Which nightly Nuxt release channel do you want to install?', + options: [ + { value: '3.x', label: '3.x' }, + { value: '4.x', label: '4.x' }, + ], + initialValue: '4.x', + }), + method: batch.select<'force' | 'dedupe' | 'skip'>({ + id: 'upgrade:method', + message: methodMessage, + options: methodOptions, + initialValue: 'dedupe', + }), }) - if (isCancel(result)) { + if (isCancel(answers.nuxtVersion) || isCancel(answers.method)) { cancel('Operation cancelled.') process.exit(1) } - method = result + nuxtVersion = answers.nuxtVersion as NuxtVersionTag + method = answers.method as 'force' | 'dedupe' | 'skip' + npmPackages = packageNames.map(p => getNightlyDependency(p, nuxtVersion)) + } + else { + ;({ npmPackages, nuxtVersion } = await getRequiredNewVersion(packageNames, ctx.args.channel)) + + if (!method) { + const result = await select({ + id: 'upgrade:method', + message: methodMessage, + options: methodOptions, + initialValue: 'dedupe' as const, + }) + + if (isCancel(result)) { + cancel('Operation cancelled.') + process.exit(1) + } + + method = result + } } const versionType = ctx.args.channel === 'nightly' ? 'nightly' : `latest ${ctx.args.channel}` - const spin = spinner() - spin.start('Upgrading Nuxt') - - await tasks([ - { - title: `Installing ${versionType} Nuxt ${nuxtVersion} release`, - task: async () => { - await addDependency(npmPackages, { - cwd, - packageManager, - dev: nuxtDependencyType === 'devDependencies', - workspace: packageManager?.name === 'pnpm' && existsSync(resolve(cwd, 'pnpm-workspace.yaml')), - }) - return 'Nuxt packages installed' + await once('upgrade:tasks', async () => { + const spin = spinner() + spin.start('Upgrading Nuxt') + + await tasks([ + { + title: `Installing ${versionType} Nuxt ${nuxtVersion} release`, + task: async () => { + await addDependency(npmPackages, { + cwd, + packageManager, + dev: nuxtDependencyType === 'devDependencies', + workspace: packageManager?.name === 'pnpm' && existsSync(resolve(cwd, 'pnpm-workspace.yaml')), + }) + return 'Nuxt packages installed' + }, }, - }, - ...(method === 'force' - ? [{ - title: `Recreating ${forceRemovals}`, - task: async () => { - await dedupeDependencies({ recreateLockfile: true }) - return 'Lockfile recreated' - }, - }] - : []), - ...(method === 'dedupe' - ? [{ - title: 'Deduping dependencies', - task: async () => { - await dedupeDependencies() - return 'Dependencies deduped' - }, - }] - : []), - { - title: 'Cleaning up build directories', - task: async () => { - let buildDir: string = '.nuxt' - try { - const { loadNuxtConfig } = await loadKit(cwd) - const nuxtOptions = await loadNuxtConfig({ cwd }) - buildDir = nuxtOptions.buildDir - } - catch { - // Use default buildDir (.nuxt) - } - await cleanupNuxtDirs(cwd, buildDir) - return 'Build directories cleaned' + ...(method === 'force' + ? [{ + title: `Recreating ${forceRemovals}`, + task: async () => { + await dedupeDependencies({ recreateLockfile: true }) + return 'Lockfile recreated' + }, + }] + : []), + ...(method === 'dedupe' + ? [{ + title: 'Deduping dependencies', + task: async () => { + await dedupeDependencies() + return 'Dependencies deduped' + }, + }] + : []), + { + title: 'Cleaning up build directories', + task: async () => { + let buildDir: string = '.nuxt' + try { + const { loadNuxtConfig } = await loadKit(cwd) + const nuxtOptions = await loadNuxtConfig({ cwd }) + buildDir = nuxtOptions.buildDir + } + catch { + // Use default buildDir (.nuxt) + } + await cleanupNuxtDirs(cwd, buildDir) + return 'Build directories cleaned' + }, }, - }, - ]) + ]) - spin.stop() + spin.stop() + return null + }) if (method === 'force') { logger.info(`If you encounter any issues, revert the changes and try with ${colors.cyan('--no-force')}`) diff --git a/packages/nuxt-cli/package.json b/packages/nuxt-cli/package.json index afb71c2f6..fabc1b598 100644 --- a/packages/nuxt-cli/package.json +++ b/packages/nuxt-cli/package.json @@ -42,7 +42,7 @@ }, "dependencies": { "@bomb.sh/tab": "^0.0.14", - "@clack/prompts": "^1.2.0", + "@clack/prompts": "npm:@posva/clack-prompts@^1.2.2", "c12": "^3.3.3", "citty": "^0.2.1", "confbox": "^0.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0bd64bda..acdf7a4da 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,8 +124,8 @@ importers: specifier: ^0.0.14 version: 0.0.14(cac@6.7.14)(citty@0.2.2) '@clack/prompts': - specifier: ^1.2.0 - version: 1.2.0 + specifier: npm:@posva/clack-prompts@^1.2.2 + version: '@posva/clack-prompts@1.2.2' '@nuxt/kit': specifier: ^4.4.2 version: 4.4.2(magicast@0.5.2) @@ -262,8 +262,8 @@ importers: specifier: ^0.0.14 version: 0.0.14(cac@6.7.14)(citty@0.2.2) '@clack/prompts': - specifier: ^1.2.0 - version: 1.2.0 + specifier: npm:@posva/clack-prompts@^1.2.2 + version: '@posva/clack-prompts@1.2.2' c12: specifier: ^3.3.3 version: 3.3.4(magicast@0.5.2) @@ -2210,6 +2210,12 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@posva/clack-core@1.2.1': + resolution: {integrity: sha512-+qV0OTeTsiuns+7J0IUW5wdt26ahdUaiNphvndQGGK7XoWkAlAexHamZNj0ZyMKbkAQfWSwon9phbSuHNmhoWg==} + + '@posva/clack-prompts@1.2.2': + resolution: {integrity: sha512-szxQoBTuDvhQxT6QIdnng3OHcUeAL+IVmKlttOkZR2gykokSaaEgvP97l2An+er+646F9Irv/bHbO3xHcLin0Q==} + '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} @@ -7802,6 +7808,19 @@ snapshots: '@poppinss/exception@1.2.3': {} + '@posva/clack-core@1.2.1': + dependencies: + fast-wrap-ansi: 0.1.6 + sisteransi: 1.0.5 + std-env: 4.1.0 + + '@posva/clack-prompts@1.2.2': + dependencies: + '@posva/clack-core': 1.2.1 + fast-string-width: 1.1.0 + fast-wrap-ansi: 0.1.6 + sisteransi: 1.0.5 + '@quansync/fs@1.0.0': dependencies: quansync: 1.0.0