diff --git a/code/core/package.json b/code/core/package.json index a59563db5512..592540e826f7 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -223,6 +223,7 @@ "@emotion/styled": "^11.14.0", "@fal-works/esbuild-plugin-global-externals": "^2.1.2", "@happy-dom/global-registrator": "^18.0.1", + "@hipster/sb-utils": "0.0.8-canary.15.285ba09.0", "@ngard/tiny-isequal": "^1.1.0", "@polka/compression": "^1.0.0-next.28", "@radix-ui/react-scroll-area": "1.2.0-rc.7", @@ -262,6 +263,7 @@ "bundle-require": "^5.1.0", "camelcase": "^8.0.0", "chai": "^5.1.1", + "clack-tree-select": "^1.0.4", "commander": "^14.0.1", "comment-parser": "^1.4.1", "copy-to-clipboard": "^3.3.1", diff --git a/code/core/src/core-server/utils/get-new-story-file.ts b/code/core/src/core-server/utils/get-new-story-file.ts index f43f9e588c19..d217af6b91f2 100644 --- a/code/core/src/core-server/utils/get-new-story-file.ts +++ b/code/core/src/core-server/utils/get-new-story-file.ts @@ -44,6 +44,10 @@ export async function getNewStoryFile( const basenameWithoutExtension = base.replace(extension, ''); const dir = dirname(componentFilePath); + // Ensure dir is relative to project root + const projectRoot = getProjectRoot(); + const relativeDir = dir.startsWith(projectRoot) ? relative(projectRoot, dir) : dir; + const { storyFileName, isTypescript, storyFileExtension } = getStoryMetadata(componentFilePath); const storyFileNameWithExtension = `${storyFileName}.${storyFileExtension}`; const alternativeStoryFileNameWithExtension = `${basenameWithoutExtension}.${componentExportName}.stories.${storyFileExtension}`; @@ -95,7 +99,7 @@ export async function getNewStoryFile( if (previewConfigPath) { const hasImportsMap = await checkForImportsMap(options.configDir); if (!hasImportsMap) { - const storyFilePath = join(getProjectRoot(), dir); + const storyFilePath = join(projectRoot, relativeDir); const relPath = relative(storyFilePath, previewConfigPath); const pathWithoutExt = relPath.replace(/\.(ts|js|mts|cts|tsx|jsx)$/, ''); previewImportPath = pathWithoutExt.startsWith('.') ? pathWithoutExt : `./${pathWithoutExt}`; @@ -133,9 +137,9 @@ export async function getNewStoryFile( storyFileContent = replaceArgsPlaceholders(storyFileContent); const storyFilePath = - doesStoryFileExist(join(getProjectRoot(), dir), storyFileName) && componentExportCount > 1 - ? join(getProjectRoot(), dir, alternativeStoryFileNameWithExtension) - : join(getProjectRoot(), dir, storyFileNameWithExtension); + doesStoryFileExist(join(projectRoot, relativeDir), storyFileName) && componentExportCount > 1 + ? join(projectRoot, relativeDir, alternativeStoryFileNameWithExtension) + : join(projectRoot, relativeDir, storyFileNameWithExtension); const formattedStoryFileContent = await formatFileContent(storyFilePath, storyFileContent); diff --git a/code/core/src/node-logger/index.ts b/code/core/src/node-logger/index.ts index ae2de0410ed1..7fb3c7967f05 100644 --- a/code/core/src/node-logger/index.ts +++ b/code/core/src/node-logger/index.ts @@ -6,7 +6,11 @@ import * as newLogger from './logger/logger'; export { prompt } from './prompts'; export { logTracker } from './logger/log-tracker'; -export type { SpinnerInstance, TaskLogInstance } from './prompts/prompt-provider-base'; +export type { + SpinnerInstance, + TaskLogInstance, + FileSystemTreeSelectPromptOptions, +} from './prompts/prompt-provider-base'; export { protectUrls, createHyperlink } from './wrap-utils'; export { CLI_COLORS } from './logger/colors'; export { ConsoleLogger, StyledConsoleLogger } from './logger/console'; diff --git a/code/core/src/node-logger/prompts/prompt-functions.ts b/code/core/src/node-logger/prompts/prompt-functions.ts index 731f557a11bf..2c72aad3398c 100644 --- a/code/core/src/node-logger/prompts/prompt-functions.ts +++ b/code/core/src/node-logger/prompts/prompt-functions.ts @@ -4,6 +4,7 @@ import { getPromptProvider } from './prompt-config'; import type { BasePromptOptions, ConfirmPromptOptions, + FileSystemTreeSelectPromptOptions, MultiSelectPromptOptions, Option, PromptOptions, @@ -23,6 +24,7 @@ export type { ConfirmPromptOptions, SelectPromptOptions, MultiSelectPromptOptions, + FileSystemTreeSelectPromptOptions, PromptOptions, SpinnerInstance, TaskLogInstance, @@ -108,6 +110,13 @@ export const multiselect = async ( ); }; +export const fileSystemTreeSelect = async ( + options: FileSystemTreeSelectPromptOptions, + promptOptions?: PromptOptions +): Promise => { + return getPromptProvider().fileSystemTreeSelect(options, promptOptions); +}; + export const spinner = (options: SpinnerOptions): SpinnerInstance => { if (isInteractiveTerminal()) { const spinnerInstance = getPromptProvider().spinner(options); diff --git a/code/core/src/node-logger/prompts/prompt-provider-base.ts b/code/core/src/node-logger/prompts/prompt-provider-base.ts index 54a1615e0890..51f96bf7e258 100644 --- a/code/core/src/node-logger/prompts/prompt-provider-base.ts +++ b/code/core/src/node-logger/prompts/prompt-provider-base.ts @@ -39,6 +39,23 @@ export interface MultiSelectPromptOptions extends BasePromptOptions { required?: boolean; } +export interface FileSystemTreeSelectPromptOptions extends BasePromptOptions { + /** The root directory to start browsing from */ + root?: string; + /** Whether to include files in the selection (default: true) */ + includeFiles?: boolean; + /** Whether to include hidden files and directories (default: false) */ + includeHidden?: boolean; + /** Maximum depth to traverse (default: Infinity) */ + maxDepth?: number; + /** Optional filter function to exclude certain paths */ + filter?: (path: string) => boolean; + /** Glob pattern(s) to filter files (based on picomatch) */ + glob?: string | string[]; + /** Whether to allow multiple selections (default: false) */ + multiple?: boolean; +} + export interface PromptOptions { onCancel?: () => void; } @@ -87,6 +104,11 @@ export abstract class PromptProvider { promptOptions?: PromptOptions ): Promise; + abstract fileSystemTreeSelect( + options: FileSystemTreeSelectPromptOptions, + promptOptions?: PromptOptions + ): Promise; + abstract spinner(options: SpinnerOptions): SpinnerInstance; abstract taskLog(options: TaskLogOptions): TaskLogInstance; diff --git a/code/core/src/node-logger/prompts/prompt-provider-clack.ts b/code/core/src/node-logger/prompts/prompt-provider-clack.ts index a103c3db1ff4..9c54a12a4d4d 100644 --- a/code/core/src/node-logger/prompts/prompt-provider-clack.ts +++ b/code/core/src/node-logger/prompts/prompt-provider-clack.ts @@ -1,9 +1,13 @@ import * as clack from '@clack/prompts'; +import { buildFileSystemTree, treeSelect } from 'clack-tree-select'; +import type { TreeItem } from 'clack-tree-select'; +import picomatch from 'picomatch'; import { logTracker } from '../logger/log-tracker'; import { wrapTextForClackHint } from '../wrap-utils'; import type { ConfirmPromptOptions, + FileSystemTreeSelectPromptOptions, MultiSelectPromptOptions, PromptOptions, SelectPromptOptions, @@ -15,6 +19,43 @@ import type { } from './prompt-provider-base'; import { PromptProvider } from './prompt-provider-base'; +// Filter a file system tree so that only the directories that contain files matching the globs are shown +function filterTreeByGlob( + tree: TreeItem[], + glob: string | string[], + root: string +): TreeItem[] { + const globMatchers = Array.isArray(glob) + ? glob.map((g) => picomatch(g, { cwd: root })) + : [picomatch(glob, { cwd: root })]; + + function filterItem(item: TreeItem): TreeItem | null { + // For files (items without children), check if they match any of the glob patterns + if (!item.children || item.children.length === 0) { + // Convert absolute path to relative path for matching + const relativePath = item.value.startsWith(root) + ? item.value.slice(root.length).replace(/^\/+/, '') + : item.value; + return globMatchers.some((matcher) => matcher(relativePath)) ? item : null; + } + + // For directories, recursively filter children + const filteredChildren = filterTreeByGlob(item.children as TreeItem[], glob, root); + + // Keep the directory only if it has any children after filtering + if (filteredChildren.length > 0) { + return { + ...item, + children: filteredChildren, + }; + } + + return null; + } + + return tree.map(filterItem).filter((item): item is TreeItem => item !== null); +} + export const getCurrentTaskLog = (): ReturnType | null => { if (globalThis.STORYBOOK_CURRENT_TASK_LOG) { return globalThis.STORYBOOK_CURRENT_TASK_LOG[globalThis.STORYBOOK_CURRENT_TASK_LOG.length - 1]; @@ -88,6 +129,39 @@ export class ClackPromptProvider extends PromptProvider { return result as T[]; } + async fileSystemTreeSelect( + options: FileSystemTreeSelectPromptOptions, + promptOptions?: PromptOptions + ): Promise { + const filter = + options.filter ?? + ((path: string) => { + const excludedDirs = ['node_modules', '.git', 'dist']; + return excludedDirs.every((dir) => !path.includes(dir)); + }); + + let fileTree = buildFileSystemTree(options.root || process.cwd(), { + includeFiles: options.includeFiles ?? true, + includeHidden: options.includeHidden ?? false, + maxDepth: options.maxDepth, + filter, + }); + + if (options.glob) { + // Filter the tree to only include directories that contain files matching the glob + fileTree = filterTreeByGlob(fileTree, options.glob, options.root || process.cwd()); + } + + const result = await treeSelect({ + message: wrapTextForClackHint(options.message, undefined, undefined, 2), + tree: fileTree, + multiple: options.multiple ?? true, + }); + this.handleCancel(result, promptOptions); + logTracker.addLog('prompt', options.message, { choice: result }); + return result as string | string[]; + } + spinner(options: SpinnerOptions): SpinnerInstance { const task = clack.spinner(); const spinnerId = `${options.id}-spinner`; diff --git a/code/lib/cli-storybook/src/automigrate/index.ts b/code/lib/cli-storybook/src/automigrate/index.ts index 040f427ba5ec..4250dff342f6 100644 --- a/code/lib/cli-storybook/src/automigrate/index.ts +++ b/code/lib/cli-storybook/src/automigrate/index.ts @@ -126,6 +126,18 @@ export const automigrate = async ({ fixResults: Record; preCheckFailure?: PreCheckFailure; } | null> => { + const files = await prompt.fileSystemTreeSelect({ + message: 'Select a file to migrate', + root: process.cwd(), + includeFiles: true, + includeHidden: false, + glob: ['**/*.tsx', '**/*.json'], + maxDepth: 3, + filter: (path) => { + return !path.includes('node_modules') && !path.includes('.git'); + }, + }); + console.log({ files }); if (list) { logAvailableMigrations(); return null; diff --git a/code/lib/cli-storybook/src/bin/run.ts b/code/lib/cli-storybook/src/bin/run.ts index cab65aa2eedf..b972ef38ef1e 100644 --- a/code/lib/cli-storybook/src/bin/run.ts +++ b/code/lib/cli-storybook/src/bin/run.ts @@ -21,6 +21,7 @@ import { version } from '../../package.json'; import { add } from '../add'; import { doAutomigrate } from '../automigrate'; import { doctor } from '../doctor'; +import { generateStories } from '../generate-stories'; import { link } from '../link'; import { migrate } from '../migrate'; import { sandbox } from '../sandbox'; @@ -302,6 +303,47 @@ command('doctor') }).catch(handleCommandFailure(options.logfile)); }); +command('generate-stories [glob]') + .description('Generate stories for components matching a glob pattern (picomatch syntax)') + .option('-i, --interactive', 'Interactively select which components to generate stories for') + .option('-f, --force', 'Force generation of stories even if they already exist') + .option( + '-s, --sample [num]', + 'Only select a subset of N components to generate stories for (default: 10)' + ) + .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') + .action(async (globPattern = 'src/**/*.{jsx,tsx}', options) => { + withTelemetry( + // @ts-expect-error - TODO: Add and enable telemetry if we decide to keep this command + 'generate-stories', + { cliOptions: { ...options, disableTelemetry: true } }, + async () => { + logger.intro(`Generating stories for components matching: ${globPattern}`); + + let sampleComponents: number | undefined = undefined; + if (options.sample) { + const n = parseInt(options.sample, 10); + sampleComponents = isNaN(n) ? 10 : n; + } + + const result = await generateStories({ + glob: globPattern, + interactive: options.interactive, + configDir: options.configDir, + force: !!options.force, + sampleComponents, + }); + + if (result.success) { + logger.outro('Story generation completed successfully!'); + } else { + logger.outro('Story generation completed with errors'); + process.exit(1); + } + } + ).catch(handleCommandFailure(options.logfile)); + }); + program.on('command:*', ([invalidCmd]) => { let errorMessage = ` Invalid command: ${picocolors.bold(invalidCmd)}.\n See --help for a list of available commands.`; const availableCommands = program.commands.map((cmd) => cmd.name()); diff --git a/code/lib/cli-storybook/src/generate-stories.ts b/code/lib/cli-storybook/src/generate-stories.ts new file mode 100644 index 000000000000..4b8ad6902897 --- /dev/null +++ b/code/lib/cli-storybook/src/generate-stories.ts @@ -0,0 +1,388 @@ +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; + +import { JsPackageManagerFactory } from 'storybook/internal/common'; +import { experimental_loadStorybook, generateStoryFile } from 'storybook/internal/core-server'; +import { logger } from 'storybook/internal/node-logger'; +import { prompt } from 'storybook/internal/node-logger'; +import type { Options } from 'storybook/internal/types'; + +import { getComponentComplexity } from '@hipster/sb-utils/component-analyzer'; +// eslint-disable-next-line depend/ban-dependencies +import { glob } from 'glob'; + +async function findEasyToStorybookComponents(files: string[], sampleComponents: number) { + const candidates = []; + + for (const file of files) { + try { + const analysis = await getComponentComplexity(file); + const { low, high } = analysis.features; + + // 2. APPLY FILTERS + // We want components that are "Pure" and isolated. + + // CRITICAL BLOCKERS: These almost always require Storybook decorators with providers + if ( + high.hasAuthIntegration || + high.hasDataFetching || + high.hasRouting || + high.hasComplexState + ) { + continue; + } + + // PREFERENCE: 'Design System' components are usually just props -> UI + // But 'Feature' components might be okay if they are simple enough + // Pages are too big + if (analysis.type === 'page') { + continue; + } + + // METRIC CHECKS: + // If it imports 10+ internal files, it's probably complex + if (low.imports.internal.length > 10) { + continue; + } + + // If it's "Ultra" complex, it probably has hidden side effects + if (analysis.level === 'very-high') { + continue; + } + + logger.debug(`Found easy to Storybook component: ${file}`); + logger.debug(`Factors: ${analysis.factors.join(', ')}`); + + // If we got here, it's a great candidate! + candidates.push({ + file, + score: analysis.score, + }); + } catch (e) { + logger.error(`Failed to analyze ${file}: ${e}`); + } + } + + // Get top 10 simplest components, easiest first + return candidates + .sort((a, b) => a.score - b.score) + .slice(0, sampleComponents) + .map((c) => c.file); +} + +interface GenerateStoriesOptions { + glob: string; + interactive?: boolean; + configDir?: string; + sampleComponents?: number; + force?: boolean; +} + +interface ComponentInfo { + filePath: string; + exportName: string; + isDefaultExport: boolean; + exportCount: number; +} + +export const generateStories = async ({ + glob: globPattern, + interactive = false, + configDir = '.storybook', + sampleComponents, + force = false, +}: GenerateStoriesOptions) => { + logger.debug(`Starting story generation with glob: ${globPattern}`); + logger.debug(`Interactive mode: ${interactive}`); + logger.debug(`Config dir: ${configDir}`); + + try { + // Load Storybook configuration + logger.debug('Loading Storybook configuration...'); + const options = await experimental_loadStorybook({ + configDir, + packageJson: JsPackageManagerFactory.getPackageManager({ + configDir, + }).primaryPackageJson, + }); + + logger.debug('Storybook configuration loaded successfully'); + + let files: string[] = []; + + if (interactive) { + logger.debug('Starting interactive component selection...'); + files = (await prompt.fileSystemTreeSelect({ + message: 'Select components to generate stories for:', + multiple: true, + glob: globPattern, + root: process.cwd(), + })) as string[]; + logger.debug(`User selected ${files.length} files`); + } else { + // Find files matching the glob pattern + logger.debug('Finding files matching glob pattern...'); + files = await glob(globPattern, { + cwd: process.cwd(), + absolute: true, + ignore: [ + '**/node_modules/**', + '**/.git/**', + '**/dist/**', + '**/build/**', + '**/storybook-static/**', + '**/*.stories.*', + '**/*.test.*', + '**/*.spec.*', + ], + }); + + logger.debug(`Found ${files.length} files matching glob pattern`); + } + + // Filter out barrel files that only export other files + files = await filterOutBarrelFiles(files); + + if (files.length === 0) { + logger.warn(`No files found matching glob pattern: ${globPattern}`); + return { + success: true, + generated: 0, + skipped: 0, + failed: 0, + }; + } + + if (sampleComponents) { + logger.debug('Filtering out easy to Storybook components...'); + files = await findEasyToStorybookComponents(files, sampleComponents); + logger.debug(`Found ${files.length} easy to Storybook components`); + } + + // Extract component information from files + logger.debug('Extracting component information from files...'); + const components = await extractComponentsFromFiles(files); + + logger.debug(`Extracted ${components.length} components from files`); + + if (components.length === 0) { + logger.warn('No components found in the matched files'); + return { + success: true, + generated: 0, + skipped: 0, + failed: 0, + }; + } + + // Filter components interactively if requested + const selectedComponents = components; + + // Generate stories for selected components + logger.debug('Generating stories for selected components...'); + const results = await generateStoriesForComponents(selectedComponents, options, force); + + // Report results + const { generated, skipped, failed } = results; + logger.info(`Story generation completed:`); + logger.info(` ✅ Generated: ${generated}`); + logger.info(` ⏭️ Skipped: ${skipped}`); + logger.info(` ❌ Failed: ${failed}`); + + return { + success: failed === 0, + generated, + skipped, + failed, + }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Failed to generate stories: ${errorMessage}`); + logger.debug(`Full error: ${error}`); + return { + success: false, + generated: 0, + skipped: 0, + failed: 1, + error: errorMessage, + }; + } +}; + +async function filterOutBarrelFiles(files: string[]) { + const filteredFiles = []; + for (const file of files) { + if (file.includes('index')) { + const content = await readFile(file, 'utf-8'); + if (!content.includes('export * from')) { + filteredFiles.push(file); + } + } else { + filteredFiles.push(file); + } + } + return filteredFiles; +} + +async function extractComponentsFromFiles(files: string[]): Promise { + const components: ComponentInfo[] = []; + + for (const file of files) { + logger.debug(`Analyzing file: ${file}`); + try { + const componentInfo = await analyzeComponentFile(file); + if (componentInfo) { + components.push(...componentInfo); + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.debug(`Failed to analyze ${file}: ${errorMessage}`); + } + } + + return components; +} + +async function analyzeComponentFile(filePath: string): Promise { + const { readFile } = await import('node:fs/promises'); + const { basename, extname } = await import('node:path'); + + try { + const content = await readFile(filePath, 'utf-8'); + + // Simple regex-based analysis for component exports + const components: ComponentInfo[] = []; + + // Check for default export + const defaultExportMatch = content.match( + /export\s+default\s+(?:function\s+|const\s+|class\s+)?([A-Z][a-zA-Z0-9]*)/ + ); + if (defaultExportMatch) { + components.push({ + filePath, + exportName: defaultExportMatch[1], + isDefaultExport: true, + exportCount: 1, // We'll update this below + }); + } + + // Check for named exports + const namedExportMatches = content.matchAll( + /export\s+(?:const|function|class)\s+([A-Z][a-zA-Z0-9]*)/g + ); + for (const match of namedExportMatches) { + const exportName = match[1]; + // Skip if we already have this as a default export + if (!components.some((c) => c.exportName === exportName && c.isDefaultExport)) { + components.push({ + filePath, + exportName, + isDefaultExport: false, + exportCount: 1, // We'll update this below + }); + } + } + + // Count total exports + const exportCount = ( + content.match(/export\s+(?:default|const|function|class|{|interface|type)/g) || [] + ).length; + + // Update export count for all components in this file + components.forEach((comp) => { + comp.exportCount = exportCount; + }); + + // If no components found but it's a component file (based on naming convention), assume a default export + if (components.length === 0) { + const fileName = basename(filePath, extname(filePath)); + // Check if it looks like a component file (starts with capital letter) + if (/^[A-Z]/.test(fileName)) { + components.push({ + filePath, + exportName: fileName, + isDefaultExport: true, + exportCount: 1, + }); + } + } + + return components.length > 0 ? components : null; + } catch (error) { + logger.debug(`Error reading file ${filePath}: ${error}`); + return null; + } +} + +// We don't want to generate story files for files which already have stories for +function doesAnyStoryFileExist(componentDir: string, componentName: string): boolean { + const extensions = ['ts', 'tsx', 'js', 'jsx']; + + for (const ext of extensions) { + if (existsSync(join(componentDir, `${componentName}.stories.${ext}`))) { + return true; + } + if (existsSync(join(componentDir, `${componentName}.story.${ext}`))) { + return true; + } + } + + return false; +} + +async function generateStoriesForComponents( + components: ComponentInfo[], + options: Options, + force?: boolean +): Promise<{ generated: number; skipped: number; failed: number }> { + let generated = 0; + let skipped = 0; + let failed = 0; + + for (const component of components) { + logger.debug(`Generating story for ${component.filePath} - ${component.exportName}`); + + const componentDir = dirname(component.filePath); + + // Check if any story file already exists for this component + if (doesAnyStoryFileExist(componentDir, component.exportName) && !force) { + skipped++; + logger.info( + `⏭️ Skipped (story already exists for ${component.exportName}): ${component.filePath}` + ); + continue; + } + + try { + const result = await generateStoryFile( + { + componentFilePath: component.filePath, + componentExportName: component.exportName, + componentIsDefaultExport: component.isDefaultExport, + componentExportCount: component.exportCount, + }, + options, + { checkFileExists: true } + ); + + if (result.success) { + generated++; + logger.info(`✅ Generated story: ${result.storyFilePath}`); + } else if (result.errorType === 'STORY_FILE_EXISTS') { + skipped++; + logger.info(`⏭️ Skipped (already exists): ${result.storyFilePath}`); + } else { + failed++; + logger.error(`❌ Failed to generate story for ${component.filePath}: ${result.error}`); + } + } catch (error: unknown) { + failed++; + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`❌ Failed to generate story for ${component.filePath}: ${errorMessage}`); + logger.debug(`Full error: ${error}`); + } + } + + return { generated, skipped, failed }; +} diff --git a/code/lib/cli-storybook/test/default/cli.test.cjs b/code/lib/cli-storybook/test/default/cli.test.cjs index 90fd0a704a52..e42d088e14d3 100755 --- a/code/lib/cli-storybook/test/default/cli.test.cjs +++ b/code/lib/cli-storybook/test/default/cli.test.cjs @@ -54,5 +54,8 @@ test('help command', () => { 'Check Storybook for known problems and provide suggestions or fixes' ); + expect(stdoutString).toContain('generate-stories'); + expect(stdoutString).toContain('Generate stories for components matching a glob pattern'); + expect(status).toBe(0); }); diff --git a/yarn.lock b/yarn.lock index 54881d5359c2..355ef4b90d71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -436,6 +436,26 @@ __metadata: languageName: node linkType: hard +"@antfu/ni@npm:^24.3.0": + version: 24.4.0 + resolution: "@antfu/ni@npm:24.4.0" + dependencies: + ansis: "npm:^4.0.0" + fzf: "npm:^0.5.2" + package-manager-detector: "npm:^1.3.0" + tinyexec: "npm:^1.0.1" + bin: + na: bin/na.mjs + nci: bin/nci.mjs + ni: bin/ni.mjs + nlx: bin/nlx.mjs + nr: bin/nr.mjs + nu: bin/nu.mjs + nun: bin/nun.mjs + checksum: 10c0/91daffd5e0c005de946cb372c91248dc55d8e4ab8c92cde12d62f2c9e92683e20bd158f903d1672f5c7ec85c9def03d7d4ef88682c8c7ca1345474bf37c613b3 + languageName: node + linkType: hard + "@aw-web-design/x-default-browser@npm:1.4.126": version: 1.4.126 resolution: "@aw-web-design/x-default-browser@npm:1.4.126" @@ -2140,7 +2160,17 @@ __metadata: languageName: node linkType: hard -"@clack/core@npm:1.0.0-alpha.7": +"@clack/core@npm:0.5.0": + version: 0.5.0 + resolution: "@clack/core@npm:0.5.0" + dependencies: + picocolors: "npm:^1.0.0" + sisteransi: "npm:^1.0.5" + checksum: 10c0/ef55dce4b0a4802171b71fe595865a6452c7cf823d162df7fa9afe2ea5a594b9d97e0b8e2880c2a805f2ce1d2f782cb1637d9f8d2ab8b99010af3a20816fae5a + languageName: node + linkType: hard + +"@clack/core@npm:1.0.0-alpha.7, @clack/core@npm:^1.0.0-alpha.1": version: 1.0.0-alpha.7 resolution: "@clack/core@npm:1.0.0-alpha.7" dependencies: @@ -2161,6 +2191,17 @@ __metadata: languageName: node linkType: hard +"@clack/prompts@npm:^0.11.0": + version: 0.11.0 + resolution: "@clack/prompts@npm:0.11.0" + dependencies: + "@clack/core": "npm:0.5.0" + picocolors: "npm:^1.0.0" + sisteransi: "npm:^1.0.5" + checksum: 10c0/4c573f2adec3b9109fe861e36312be8ae7cc6e80a5128aa784b9aeafeda5001b23f66c08eca50f4491119b435d9587ec9862956be8c5be472ec3373275003ba8 + languageName: node + linkType: hard + "@cypress/request@npm:3.0.1": version: 3.0.1 resolution: "@cypress/request@npm:3.0.1" @@ -3303,6 +3344,19 @@ __metadata: languageName: node linkType: hard +"@hipster/sb-utils@npm:0.0.8-canary.15.285ba09.0": + version: 0.0.8-canary.15.285ba09.0 + resolution: "@hipster/sb-utils@npm:0.0.8-canary.15.285ba09.0" + dependencies: + "@antfu/ni": "npm:^24.3.0" + "@clack/prompts": "npm:^0.11.0" + commander: "npm:^14.0.2" + bin: + sb-utils: dist/bin.mjs + checksum: 10c0/2478e0e2dd5a5e7409905b5d5d60b5ce31ce20be283bf1db3497733954e0c821c2f9e46af39e71d12144ff686b1535299adf7c7a43e755d9fab9b6e940ea3d31 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.13.0": version: 0.13.0 resolution: "@humanwhocodes/config-array@npm:0.13.0" @@ -11977,7 +12031,7 @@ __metadata: languageName: node linkType: hard -"ansis@npm:^4.1.0": +"ansis@npm:^4.0.0, ansis@npm:^4.1.0": version: 4.2.0 resolution: "ansis@npm:4.2.0" checksum: 10c0/cd6a7a681ecd36e72e0d79c1e34f1f3bcb1b15bcbb6f0f8969b4228062d3bfebbef468e09771b00d93b2294370b34f707599d4a113542a876de26823b795b5d2 @@ -13859,6 +13913,20 @@ __metadata: languageName: node linkType: hard +"clack-tree-select@npm:^1.0.4": + version: 1.0.4 + resolution: "clack-tree-select@npm:1.0.4" + dependencies: + "@clack/core": "npm:^1.0.0-alpha.1" + is-unicode-supported: "npm:^1.3.0" + picocolors: "npm:^1.0.0" + sisteransi: "npm:^1.0.5" + peerDependencies: + "@clack/prompts": ">=0.8.0" + checksum: 10c0/92015e17b95d26bb2412cb3a566325a5972ffc5bfd9ecd351b24d554769fd6165c53e4b8d88f12160f777f1fb5c71fc26f6b964138e0a27e4bab4770595370c8 + languageName: node + linkType: hard + "clean-css@npm:^5.2.2": version: 5.3.3 resolution: "clean-css@npm:5.3.3" @@ -14196,7 +14264,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^14.0.1": +"commander@npm:^14.0.1, commander@npm:^14.0.2": version: 14.0.2 resolution: "commander@npm:14.0.2" checksum: 10c0/245abd1349dbad5414cb6517b7b5c584895c02c4f7836ff5395f301192b8566f9796c82d7bd6c92d07eba8775fe4df86602fca5d86d8d10bcc2aded1e21c2aeb @@ -18392,6 +18460,13 @@ __metadata: languageName: node linkType: hard +"fzf@npm:^0.5.2": + version: 0.5.2 + resolution: "fzf@npm:0.5.2" + checksum: 10c0/5b1f945b289855891c4e3cb03db35381f8d85464dceb15b6d32f0fc74e43d7d2b9a13554cf78a86760ba762de39134d40644ccb54e60668a4bc5b15c4765d36e + languageName: node + linkType: hard + "gauge@npm:^4.0.3": version: 4.0.4 resolution: "gauge@npm:4.0.4" @@ -20654,6 +20729,13 @@ __metadata: languageName: node linkType: hard +"is-unicode-supported@npm:^1.3.0": + version: 1.3.0 + resolution: "is-unicode-supported@npm:1.3.0" + checksum: 10c0/b8674ea95d869f6faabddc6a484767207058b91aea0250803cbf1221345cb0c56f466d4ecea375dc77f6633d248d33c47bd296fb8f4cdba0b4edba8917e83d8a + languageName: node + linkType: hard + "is-unicode-supported@npm:^2.0.0": version: 2.1.0 resolution: "is-unicode-supported@npm:2.1.0" @@ -24700,6 +24782,13 @@ __metadata: languageName: node linkType: hard +"package-manager-detector@npm:^1.3.0": + version: 1.6.0 + resolution: "package-manager-detector@npm:1.6.0" + checksum: 10c0/6419d0b840be64fd45bcdcb7a19f09b81b65456d5e7f7a3daac305a4c90643052122f6ac0308afe548ffee75e36148532a2002ea9d292754f1e385aa2e1ea03b + languageName: node + linkType: hard + "pako@npm:~0.2.0": version: 0.2.9 resolution: "pako@npm:0.2.9" @@ -29158,6 +29247,7 @@ __metadata: "@emotion/styled": "npm:^11.14.0" "@fal-works/esbuild-plugin-global-externals": "npm:^2.1.2" "@happy-dom/global-registrator": "npm:^18.0.1" + "@hipster/sb-utils": "npm:0.0.8-canary.15.285ba09.0" "@ngard/tiny-isequal": "npm:^1.1.0" "@polka/compression": "npm:^1.0.0-next.28" "@radix-ui/react-scroll-area": "npm:1.2.0-rc.7" @@ -29203,6 +29293,7 @@ __metadata: bundle-require: "npm:^5.1.0" camelcase: "npm:^8.0.0" chai: "npm:^5.1.1" + clack-tree-select: "npm:^1.0.4" commander: "npm:^14.0.1" comment-parser: "npm:^1.4.1" copy-to-clipboard: "npm:^3.3.1" @@ -30116,6 +30207,13 @@ __metadata: languageName: node linkType: hard +"tinyexec@npm:^1.0.1": + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: 10c0/1261a8e34c9b539a9aae3b7f0bb5372045ff28ee1eba035a2a059e532198fe1a182ec61ac60fa0b4a4129f0c4c4b1d2d57355b5cb9aa2d17ac9454ecace502ee + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.10, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.9": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15"