diff --git a/packages/nuxi/package.json b/packages/nuxi/package.json index a6dea31cd..cf564959e 100644 --- a/packages/nuxi/package.json +++ b/packages/nuxi/package.json @@ -37,6 +37,7 @@ "@nuxt/kit": "^4.2.2", "@nuxt/schema": "^4.2.2", "@nuxt/test-utils": "^3.23.0", + "@posva/prompts": "^2.4.4", "@types/copy-paste": "^2.1.0", "@types/debug": "^4.1.12", "@types/node": "^24.10.7", @@ -50,6 +51,7 @@ "defu": "^6.1.4", "exsolve": "^1.0.8", "fuse.js": "^7.1.0", + "fzf": "^0.5.2", "giget": "^2.0.0", "h3": "^1.15.4", "h3-next": "npm:h3@^2.0.1-rc.7", diff --git a/packages/nuxi/src/commands/init.ts b/packages/nuxi/src/commands/init.ts index 3c85ba72f..0b53f5174 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, multiselect, outro, select, spinner, tasks, text } from '@clack/prompts' +import { box, cancel, confirm, intro, isCancel, outro, select, spinner, tasks, text } from '@clack/prompts' import { defineCommand } from 'citty' import { colors } from 'consola/utils' import { downloadTemplate, startShell } from 'giget' @@ -23,6 +23,7 @@ import { relativeToProcess } from '../utils/paths' import { getTemplates } from '../utils/starter-templates' import { getNuxtVersion } from '../utils/versions' import { cwdArgs, logLevelArgs } from './_shared' +import { selectModulesAutocomplete } from './module/_autocomplete' import { checkNuxtCompatibility, fetchModules } from './module/_utils' import addModuleCommand from './module/add' @@ -423,11 +424,11 @@ export default defineCommand({ } } - // ...or offer to install official modules (if not offline) + // ...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 install any of the official modules?`, + message: `Would you like to browse and install modules?`, initialValue: false, }) @@ -448,33 +449,21 @@ export default defineCommand({ modulesSpinner.stop('Modules loaded') - const officialModules = response + const allModules = response .filter(module => - module.type === 'official' - && module.npm !== '@nuxt/devtools' + module.npm !== '@nuxt/devtools' && !templateDeps.includes(module.npm) && (!module.compatibility.nuxt || checkNuxtCompatibility(module, nuxtVersion)), ) - if (officialModules.length === 0) { - logger.info('All official modules are already included in this template.') + if (allModules.length === 0) { + logger.info('All modules are already included in this template.') } else { - const selectedOfficialModules = await multiselect({ - message: 'Pick the modules to install:', - options: officialModules.map(module => ({ - label: `${colors.bold(colors.greenBright(module.npm))} – ${module.description.replace(/\.$/, '')}`, - value: module.npm, - })), - required: false, - }) - - if (isCancel(selectedOfficialModules)) { - process.exit(1) - } + const result = await selectModulesAutocomplete({ modules: allModules }) - if (selectedOfficialModules.length > 0) { - const modules = selectedOfficialModules as unknown as string[] + if (result.selected.length > 0) { + const modules = result.selected const allDependencies = Object.fromEntries( await Promise.all(modules.map(async module => diff --git a/packages/nuxi/src/commands/module/_autocomplete.ts b/packages/nuxi/src/commands/module/_autocomplete.ts new file mode 100644 index 000000000..2ac67f6d8 --- /dev/null +++ b/packages/nuxi/src/commands/module/_autocomplete.ts @@ -0,0 +1,150 @@ +import type { Choice } from '@posva/prompts' +import type { NuxtModule } from './_utils' + +import process from 'node:process' +import prompts from '@posva/prompts' +import { colors } from 'consola/utils' +import { byLengthAsc, Fzf } from 'fzf' +import { hasTTY } from 'std-env' + +import { logger } from '../../utils/logger' + +export interface AutocompleteOptions { + modules: NuxtModule[] + message?: string +} + +export interface AutocompleteResult { + selected: string[] + cancelled: boolean +} + +/** + * Interactive fuzzy search for selecting Nuxt modules + * Returns object with selected module npm package names and cancellation status + */ +export async function selectModulesAutocomplete(options: AutocompleteOptions): Promise { + const { modules, message = 'Search modules (Esc to finish):' } = options + + if (!hasTTY) { + logger.warn('Interactive module selection requires a TTY. Skipping.') + return { selected: [], cancelled: false } + } + + // Sort: official modules first, then alphabetically + const sortedModules = [...modules].sort((a, b) => { + if (a.type === 'official' && b.type !== 'official') + return -1 + if (a.type !== 'official' && b.type === 'official') + return 1 + return a.npm.localeCompare(b.npm) + }) + + // Setup fzf for fast fuzzy search + const fzf = new Fzf(sortedModules, { + selector: m => `${m.npm} ${m.name} ${m.category}`, + casing: 'case-insensitive', + tiebreakers: [byLengthAsc], + }) + + // Truncate description to fit terminal + const terminalWidth = process.stdout?.columns || 80 + const maxDescLength = Math.max(40, terminalWidth - 35) + const truncate = (str: string, max: number) => + str.length > max ? `${str.slice(0, max - 1)}…` : str + + // Track selected modules + const selectedModules = new Set() + + // Build choices with checkbox prefix + const buildChoices = () => sortedModules.map((m) => { + const isSelected = selectedModules.has(m.npm) + const check = isSelected ? colors.green('✔') : colors.dim('○') + return { + title: `${check} ${m.npm}`, + value: m.npm, + description: truncate(m.description.replace(/\.$/, ''), maxDescLength), + } + }) + + // Loop for multi-select via autocomplete with checkboxes + let isExited = false + let isDone = false + let lastQuery = '' + + // ANSI escapes for terminal control + const clearLines = (n: number) => { + if (!hasTTY) + return + for (let i = 0; i < n; i++) { + process.stdout.write('\x1B[1A\x1B[2K') + } + } + + // Show summary line + const showSummary = () => { + if (!hasTTY || selectedModules.size === 0) + return + const names = Array.from(selectedModules).map(m => colors.cyan(m.replace(/^@nuxt(js)?\//, ''))).join(', ') + process.stdout.write(`${colors.dim('Selected:')} ${names}\n`) + } + + while (!isDone) { + const choices = buildChoices() + + // Clear previous prompt and show fresh summary + if (lastQuery !== '' || selectedModules.size > 0) { + clearLines(selectedModules.size > 0 ? 2 : 1) + } + showSummary() + + try { + const result = await prompts({ + type: 'autocomplete', + name: 'module', + message, + initial: lastQuery, + choices, + limit: 10, + suggest: async (input: string, choices: Choice[]) => { + lastQuery = input + if (!input) + return choices + const results = fzf.find(input) + return results.map((r) => { + const isSelected = selectedModules.has(r.item.npm) + const check = isSelected ? colors.green('✔') : colors.dim('○') + return { + title: `${check} ${r.item.npm}`, + value: r.item.npm, + description: truncate(r.item.description.replace(/\.$/, ''), maxDescLength), + } + }) + }, + onState(state: { exited?: boolean }) { + if (state.exited) + isExited = true + }, + }) + + if (isExited || !result.module) { + isDone = true + } + else { + // Toggle selection + if (selectedModules.has(result.module)) { + selectedModules.delete(result.module) + } + else { + selectedModules.add(result.module) + } + } + isExited = false + } + catch { + isDone = true + } + } + + return { selected: Array.from(selectedModules), cancelled: false } +} diff --git a/packages/nuxi/src/commands/module/add.ts b/packages/nuxi/src/commands/module/add.ts index b4286535f..c2dafcba9 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 } from '@clack/prompts' +import { cancel, confirm, isCancel, select, spinner } from '@clack/prompts' import { updateConfig } from 'c12/update' import { defineCommand } from 'citty' import { colors } from 'consola/utils' @@ -25,6 +25,7 @@ import { relativeToProcess } from '../../utils/paths' import { getNuxtVersion } from '../../utils/versions' import { cwdArgs, logLevelArgs } from '../_shared' import prepareCommand from '../prepare' +import { selectModulesAutocomplete } from './_autocomplete' import { checkNuxtCompatibility, fetchModules, getRegistryFromContent } from './_utils' interface RegistryMeta { @@ -68,7 +69,7 @@ export default defineCommand({ }, async setup(ctx) { const cwd = resolve(ctx.args.cwd) - const modules = ctx.args._.map(e => e.trim()).filter(Boolean) + let modules = ctx.args._.map(e => e.trim()).filter(Boolean) const projectPkg = await readPackageJSON(cwd).catch(() => ({} as PackageJson)) if (!projectPkg.dependencies?.nuxt && !projectPkg.devDependencies?.nuxt) { @@ -84,6 +85,35 @@ export default defineCommand({ } } + // If no modules specified, show interactive search + if (modules.length === 0) { + const modulesSpinner = spinner() + modulesSpinner.start('Fetching available modules') + + const [allModules, nuxtVersion] = await Promise.all([ + fetchModules(), + getNuxtVersion(cwd), + ]) + + const compatibleModules = allModules.filter(m => + !m.compatibility.nuxt || checkNuxtCompatibility(m, nuxtVersion), + ) + + modulesSpinner.stop('Modules loaded') + + const result = await selectModulesAutocomplete({ + modules: compatibleModules, + message: 'Search modules to add (Esc to finish):', + }) + + if (result.selected.length === 0) { + cancel('No modules selected.') + process.exit(0) + } + + modules = result.selected + } + const resolvedModules: ResolvedModule[] = [] for (const moduleName of modules) { const resolvedModule = await resolveModule(moduleName, cwd) diff --git a/packages/nuxi/test/unit/commands/_utils.test.ts b/packages/nuxi/test/unit/commands/_utils.spec.ts similarity index 100% rename from packages/nuxi/test/unit/commands/_utils.test.ts rename to packages/nuxi/test/unit/commands/_utils.spec.ts diff --git a/packages/nuxi/test/unit/commands/module/_autocomplete.spec.ts b/packages/nuxi/test/unit/commands/module/_autocomplete.spec.ts new file mode 100644 index 000000000..9677d6172 --- /dev/null +++ b/packages/nuxi/test/unit/commands/module/_autocomplete.spec.ts @@ -0,0 +1,386 @@ +import type { NuxtModule } from '../../../../src/commands/module/_utils' + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock std-env before importing the module +const mockHasTTY = vi.hoisted(() => ({ value: true })) + +vi.mock('std-env', () => ({ + hasTTY: mockHasTTY.value, +})) + +// Mock @posva/prompts +const mockPrompts = vi.hoisted(() => vi.fn()) +vi.mock('@posva/prompts', () => ({ + default: mockPrompts, +})) + +// Mock logger +const mockLogger = vi.hoisted(() => ({ + warn: vi.fn(), + info: vi.fn(), +})) +vi.mock('../../../../src/utils/logger', () => ({ + logger: mockLogger, +})) + +// Helper to create mock modules +function createMockModule(overrides: Partial = {}): NuxtModule { + return { + name: 'test-module', + npm: '@nuxt/test-module', + compatibility: { + nuxt: '^3.0.0', + requires: {}, + versionMap: {}, + }, + description: 'A test module', + repo: '', + github: '', + website: '', + learn_more: '', + category: 'UI', + type: 'community', + maintainers: [], + stats: { + downloads: 0, + stars: 0, + maintainers: 0, + contributors: 0, + modules: 0, + }, + ...overrides, + } +} + +describe('selectModulesAutocomplete', () => { + beforeEach(() => { + vi.clearAllMocks() + mockHasTTY.value = true + + // Reset process.stdout.isTTY for each test + Object.defineProperty(process.stdout, 'isTTY', { + value: true, + writable: true, + configurable: true, + }) + + // Reset process.stdout.columns + Object.defineProperty(process.stdout, 'columns', { + value: 120, + writable: true, + configurable: true, + }) + }) + + afterEach(() => { + vi.resetModules() + }) + + describe('tTY handling', () => { + it('should return empty result when not in TTY environment', async () => { + // Re-mock std-env with hasTTY = false + vi.doMock('std-env', () => ({ + hasTTY: false, + })) + + // Re-import the module to get the new mock + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + const modules = [createMockModule()] + const result = await selectModulesAutocomplete({ modules }) + + expect(result).toEqual({ selected: [], cancelled: false }) + expect(mockLogger.warn).toHaveBeenCalledWith('Interactive module selection requires a TTY. Skipping.') + }) + + it('should proceed with prompts when in TTY environment', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + // Mock prompts to simulate user pressing Esc immediately + mockPrompts.mockResolvedValueOnce({ module: undefined }) + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + const modules = [createMockModule()] + const result = await selectModulesAutocomplete({ modules }) + + expect(result).toEqual({ selected: [], cancelled: false }) + expect(mockPrompts).toHaveBeenCalled() + }) + }) + + describe('module sorting', () => { + it('should sort official modules before community modules', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + let capturedChoices: any[] = [] + + mockPrompts.mockImplementation(async (options: any) => { + capturedChoices = options.choices + return { module: undefined } + }) + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + const modules = [ + createMockModule({ npm: '@community/z-module', type: 'community', name: 'z-module' }), + createMockModule({ npm: '@nuxt/a-module', type: 'official', name: 'a-module' }), + createMockModule({ npm: '@community/b-module', type: 'community', name: 'b-module' }), + createMockModule({ npm: '@nuxt/c-module', type: 'official', name: 'c-module' }), + ] + + await selectModulesAutocomplete({ modules }) + + // Official modules should come first, then sorted alphabetically + const npmNames = capturedChoices.map((c: any) => c.value) + expect(npmNames[0]).toBe('@nuxt/a-module') + expect(npmNames[1]).toBe('@nuxt/c-module') + expect(npmNames[2]).toBe('@community/b-module') + expect(npmNames[3]).toBe('@community/z-module') + }) + }) + + describe('module selection', () => { + it('should return selected modules when user selects and exits', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + // Simulate: user selects first module, then presses Esc + mockPrompts + .mockResolvedValueOnce({ module: '@nuxt/ui' }) + .mockResolvedValueOnce({ module: undefined }) + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + const modules = [ + createMockModule({ npm: '@nuxt/ui', name: 'ui' }), + createMockModule({ npm: '@nuxt/icon', name: 'icon' }), + ] + + const result = await selectModulesAutocomplete({ modules }) + + expect(result.selected).toContain('@nuxt/ui') + expect(result.selected).toHaveLength(1) + }) + + it('should allow toggling module selection (select then deselect)', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + // Simulate: select, deselect same module, then exit + mockPrompts + .mockResolvedValueOnce({ module: '@nuxt/ui' }) // Select + .mockResolvedValueOnce({ module: '@nuxt/ui' }) // Deselect (toggle) + .mockResolvedValueOnce({ module: undefined }) // Exit + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + const modules = [createMockModule({ npm: '@nuxt/ui', name: 'ui' })] + + const result = await selectModulesAutocomplete({ modules }) + + expect(result.selected).toHaveLength(0) + }) + + it('should allow selecting multiple modules', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + mockPrompts + .mockResolvedValueOnce({ module: '@nuxt/ui' }) + .mockResolvedValueOnce({ module: '@nuxt/icon' }) + .mockResolvedValueOnce({ module: '@nuxt/image' }) + .mockResolvedValueOnce({ module: undefined }) + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + const modules = [ + createMockModule({ npm: '@nuxt/ui', name: 'ui' }), + createMockModule({ npm: '@nuxt/icon', name: 'icon' }), + createMockModule({ npm: '@nuxt/image', name: 'image' }), + ] + + const result = await selectModulesAutocomplete({ modules }) + + expect(result.selected).toHaveLength(3) + expect(result.selected).toContain('@nuxt/ui') + expect(result.selected).toContain('@nuxt/icon') + expect(result.selected).toContain('@nuxt/image') + }) + }) + + describe('error handling', () => { + it('should handle prompt errors gracefully', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + mockPrompts.mockRejectedValueOnce(new Error('User interrupted')) + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + const modules = [createMockModule()] + const result = await selectModulesAutocomplete({ modules }) + + // Should return empty array on error, not throw + expect(result).toEqual({ selected: [], cancelled: false }) + }) + + it('should not throw when prompt is interrupted', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + mockPrompts.mockRejectedValueOnce(new Error('Interrupted')) + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + const modules = [createMockModule()] + + // Should not throw, should return gracefully + await expect(selectModulesAutocomplete({ modules })).resolves.toEqual({ + selected: [], + cancelled: false, + }) + }) + }) + + describe('custom message', () => { + it('should use custom message when provided', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + let capturedMessage = '' + mockPrompts.mockImplementation(async (options: any) => { + capturedMessage = options.message + return { module: undefined } + }) + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + const customMessage = 'Custom search message:' + await selectModulesAutocomplete({ + modules: [createMockModule()], + message: customMessage, + }) + + expect(capturedMessage).toBe(customMessage) + }) + + it('should use default message when not provided', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + let capturedMessage = '' + mockPrompts.mockImplementation(async (options: any) => { + capturedMessage = options.message + return { module: undefined } + }) + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + await selectModulesAutocomplete({ + modules: [createMockModule()], + }) + + expect(capturedMessage).toBe('Search modules (Esc to finish):') + }) + }) + + describe('description truncation', () => { + it('should truncate long descriptions', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + // Set narrow terminal + Object.defineProperty(process.stdout, 'columns', { + value: 60, + writable: true, + configurable: true, + }) + + let capturedChoices: any[] = [] + mockPrompts.mockImplementation(async (options: any) => { + capturedChoices = options.choices + return { module: undefined } + }) + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + const longDescription = 'This is a very long description that should be truncated because it exceeds the maximum allowed length for the terminal width' + await selectModulesAutocomplete({ + modules: [createMockModule({ description: longDescription })], + }) + + // Description should be truncated and end with ellipsis + const choice = capturedChoices[0] + expect(choice.description.length).toBeLessThan(longDescription.length) + expect(choice.description.endsWith('…')).toBe(true) + }) + }) + + describe('fuzzy search suggest function', () => { + it('should pass suggest function to prompts', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + let capturedSuggest: ((input: string, choices: any[]) => Promise) | undefined + mockPrompts.mockImplementation(async (options: any) => { + capturedSuggest = options.suggest + return { module: undefined } + }) + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + await selectModulesAutocomplete({ + modules: [ + createMockModule({ npm: '@nuxt/tailwind', name: 'tailwind' }), + createMockModule({ npm: '@nuxt/ui', name: 'ui' }), + ], + }) + + expect(capturedSuggest).toBeDefined() + expect(typeof capturedSuggest).toBe('function') + }) + + it('should return all choices when input is empty', async () => { + vi.doMock('std-env', () => ({ + hasTTY: true, + })) + + let capturedSuggest: ((input: string, choices: any[]) => Promise) | undefined + let capturedChoices: any[] = [] + + mockPrompts.mockImplementation(async (options: any) => { + capturedSuggest = options.suggest + capturedChoices = options.choices + return { module: undefined } + }) + + const { selectModulesAutocomplete } = await import('../../../../src/commands/module/_autocomplete') + + await selectModulesAutocomplete({ + modules: [ + createMockModule({ npm: '@nuxt/a', name: 'a' }), + createMockModule({ npm: '@nuxt/b', name: 'b' }), + ], + }) + + // Test the suggest function with empty input + const result = await capturedSuggest!('', capturedChoices) + expect(result).toEqual(capturedChoices) + }) + }) +}) diff --git a/packages/nuxi/test/unit/commands/module/_utils.test.ts b/packages/nuxi/test/unit/commands/module/_utils.spec.ts similarity index 100% rename from packages/nuxi/test/unit/commands/module/_utils.test.ts rename to packages/nuxi/test/unit/commands/module/_utils.spec.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 83414107d..a0882b6f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: '@nuxt/test-utils': specifier: ^3.23.0 version: 3.23.0(crossws@0.4.1(srvx@0.10.0))(magicast@0.5.1)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.7)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) + '@posva/prompts': + specifier: ^2.4.4 + version: 2.4.4 '@types/copy-paste': specifier: ^2.1.0 version: 2.1.0 @@ -170,6 +173,9 @@ importers: fuse.js: specifier: ^7.1.0 version: 7.1.0 + fzf: + specifier: ^0.5.2 + version: 0.5.2 giget: specifier: ^2.0.0 version: 2.0.0 @@ -193,7 +199,7 @@ importers: version: 3.0.1-alpha.1(chokidar@4.0.3)(ioredis@5.9.1)(lru-cache@11.2.4)(rolldown@1.0.0-beta.59)(rollup@4.55.1)(vite@7.3.0(@types/node@24.10.7)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) nitropack: specifier: latest - version: 2.13.0(rolldown@1.0.0-beta.59) + version: 2.13.1(rolldown@1.0.0-beta.59) nypm: specifier: ^0.6.2 version: 0.6.2 @@ -362,7 +368,7 @@ importers: version: 3.0.1-alpha.1(chokidar@4.0.3)(ioredis@5.9.1)(lru-cache@11.2.4)(rolldown@1.0.0-beta.59)(rollup@4.55.1)(vite@7.3.0(@types/node@24.10.7)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)) nitropack: specifier: latest - version: 2.13.0(rolldown@1.0.0-beta.59) + version: 2.13.1(rolldown@1.0.0-beta.59) rollup: specifier: ^4.55.1 version: 4.55.1 @@ -628,8 +634,8 @@ packages: '@clack/prompts@1.0.0-alpha.9': resolution: {integrity: sha512-sKs0UjiHFWvry4SiRfBi5Qnj0C/6AYx8aKkFPZQSuUZXgAram25ZDmhQmP7vj1aFyLpfHWtLQjWvOvcat0TOLg==} - '@cloudflare/kv-asset-handler@0.4.1': - resolution: {integrity: sha512-Nu8ahitGFFJztxUml9oD/DLb7Z28C8cd8F46IVQ7y5Btz575pvMY8AqZsXkX7Gds29eCKdMgIHjIvzskHgPSFg==} + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} engines: {node: '>=18.0.0'} '@codspeed/core@5.0.1': @@ -1798,6 +1804,10 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@posva/prompts@2.4.4': + resolution: {integrity: sha512-8aPwklhbSV2VN/NQMBNFkuo8+hlJVdcFRXp4NCIfdcahh3qNEcaSoD8qXjru0OlN1sONJ7le7p6+YUbALaG6Mg==} + engines: {node: '>= 14'} + '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} @@ -3501,6 +3511,9 @@ packages: resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} engines: {node: '>=10'} + fzf@0.5.2: + resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -4135,11 +4148,6 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} - hasBin: true - mime@4.1.0: resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} engines: {node: '>=16'} @@ -4245,8 +4253,8 @@ packages: xml2js: optional: true - nitropack@2.13.0: - resolution: {integrity: sha512-31H9EgJNsJqfa5f6775ksZlKH+Fk8Kv3CV2wF6v9+KY57DexH8+qCLrcOXgM72vKB/j/7dVmOtuiVY8Jy8+8nw==} + nitropack@2.13.1: + resolution: {integrity: sha512-2dDj89C4wC2uzG7guF3CnyG+zwkZosPEp7FFBGHB3AJo11AywOolWhyQJFHDzve8COvGxJaqscye9wW2IrUsNw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5254,6 +5262,9 @@ packages: ufo@1.6.2: resolution: {integrity: sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==} + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + ultrahtml@1.6.0: resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} @@ -5413,6 +5424,68 @@ packages: uploadthing: optional: true + unstorage@1.17.4: + resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + unstorage@2.0.0-alpha.4: resolution: {integrity: sha512-ywXZMZRfrvmO1giJeMTCw6VUn0ALYxVl8pFqJPStiyQUvgJImejtAHrKvXPj4QGJAoS/iLGcVGF6ljN/lkh1bw==} peerDependencies: @@ -5495,8 +5568,8 @@ packages: resolution: {integrity: sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==} hasBin: true - unwasm@0.5.2: - resolution: {integrity: sha512-uWhB7IXQjMC4530uVAeu0lzvYK6P3qHVnmmdQniBi48YybOLN/DqEzcP9BRGk1YTDG3rRWRD8me55nIYoTHyMg==} + unwasm@0.5.3: + resolution: {integrity: sha512-keBgTSfp3r6+s9ZcSma+0chwxQdmLbB5+dAD9vjtB21UTMYuKAxHXCU1K2CbCtnP09EaWeRvACnXk0EJtUx+hw==} update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} @@ -6085,9 +6158,7 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@cloudflare/kv-asset-handler@0.4.1': - dependencies: - mime: 3.0.0 + '@cloudflare/kv-asset-handler@0.4.2': {} '@codspeed/core@5.0.1': dependencies: @@ -6587,7 +6658,7 @@ snapshots: impound: 1.0.0 klona: 2.0.6 mocked-exports: 0.1.1 - nitropack: 2.13.0(rolldown@1.0.0-beta.59) + nitropack: 2.13.1(rolldown@1.0.0-beta.59) nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@24.10.7)(@vue/compiler-sfc@3.5.26)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.1)(magicast@0.5.1)(optionator@0.9.4)(rolldown@1.0.0-beta.59)(rollup@4.55.1)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.7)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2) pathe: 2.0.3 pkg-types: 2.3.0 @@ -7214,6 +7285,11 @@ snapshots: '@poppinss/exception@1.2.3': {} + '@posva/prompts@2.4.4': + dependencies: + kleur: 4.1.5 + sisteransi: 1.0.5 + '@quansync/fs@1.0.0': dependencies: quansync: 1.0.0 @@ -8936,6 +9012,8 @@ snapshots: fuse.js@7.1.0: {} + fzf@0.5.2: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -9756,8 +9834,6 @@ snapshots: dependencies: mime-db: 1.54.0 - mime@3.0.0: {} - mime@4.1.0: {} mimic-fn@4.0.0: {} @@ -9868,9 +9944,9 @@ snapshots: - sqlite3 - uploadthing - nitropack@2.13.0(rolldown@1.0.0-beta.59): + nitropack@2.13.1(rolldown@1.0.0-beta.59): dependencies: - '@cloudflare/kv-asset-handler': 0.4.1 + '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.55.1) '@rollup/plugin-commonjs': 29.0.0(rollup@4.55.1) '@rollup/plugin-inject': 5.0.5(rollup@4.55.1) @@ -9928,16 +10004,16 @@ snapshots: serve-static: 2.2.1 source-map: 0.7.6 std-env: 3.10.0 - ufo: 1.6.2 + ufo: 1.6.3 ultrahtml: 1.6.0 uncrypto: 0.1.3 unctx: 2.5.0 unenv: 2.0.0-rc.24 unimport: 5.6.0 unplugin-utils: 0.3.1 - unstorage: 1.17.3(db0@0.3.4)(ioredis@5.9.1) + unstorage: 1.17.4(db0@0.3.4)(ioredis@5.9.1) untyped: 2.0.0 - unwasm: 0.5.2 + unwasm: 0.5.3 youch: 4.1.0-beta.13 youch-core: 0.3.3 transitivePeerDependencies: @@ -11133,6 +11209,8 @@ snapshots: ufo@1.6.2: {} + ufo@1.6.3: {} + ultrahtml@1.6.0: {} unconfig-core@7.4.2: @@ -11302,6 +11380,20 @@ snapshots: db0: 0.3.4 ioredis: 5.9.1 + unstorage@1.17.4(db0@0.3.4)(ioredis@5.9.1): + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.4 + lru-cache: 11.2.4 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.3 + optionalDependencies: + db0: 0.3.4 + ioredis: 5.9.1 + unstorage@2.0.0-alpha.4(chokidar@4.0.3)(db0@0.3.4)(ioredis@5.9.1)(lru-cache@11.2.4)(ofetch@2.0.0-alpha.3): optionalDependencies: chokidar: 4.0.3 @@ -11324,7 +11416,7 @@ snapshots: knitwork: 1.3.0 scule: 1.3.0 - unwasm@0.5.2: + unwasm@0.5.3: dependencies: exsolve: 1.0.8 knitwork: 1.3.0