import { existsSync, promises as fs } from 'node:fs' import path from 'node:path' import process from 'node:process' import chalk from 'chalk' import { Command } from 'commander' import { execa } from 'execa' import ora from 'ora' import prompts from 'prompts' import * as z from 'zod' import { transformImport } from '../utils/transformers/transform-import' import { getConfig } from '@/src/utils/get-config' import { getPackageManager } from '@/src/utils/get-package-manager' import { handleError } from '@/src/utils/handle-error' import { logger } from '@/src/utils/logger' import { fetchTree, getItemTargetPath, getRegistryBaseColor, getRegistryIndex, resolveTree, } from '@/src/utils/registry' const addOptionsSchema = z.object({ components: z.array(z.string()).optional(), yes: z.boolean(), overwrite: z.boolean(), cwd: z.string(), path: z.string().optional(), }) export const add = new Command() .name('add') .description('add a component to your project') .argument('[components...]', 'the components to add') .option('-y, --yes', 'skip confirmation prompt.', false) .option('-o, --overwrite', 'overwrite existing files.', false) .option( '-c, --cwd ', 'the working directory. defaults to the current directory.', process.cwd(), ) .option('-p, --path ', 'the path to add the component to.') .action(async (components, opts) => { try { const options = addOptionsSchema.parse({ components, ...opts, }) const cwd = path.resolve(options.cwd) if (!existsSync(cwd)) { logger.error(`The path ${cwd} does not exist. Please try again.`) process.exit(1) } const config = await getConfig(cwd) if (!config) { logger.warn( `Configuration is missing. Please run ${chalk.green('init')} to create a components.json file.`, ) process.exit(1) } const registryIndex = await getRegistryIndex() let selectedComponents = options.components if (!options.components?.length) { const { components } = await prompts({ type: 'autocompleteMultiselect', name: 'components', message: 'Which components would you like to add?', hint: 'Space to select. A to toggle all. Enter to submit.', instructions: false, choices: registryIndex.map(entry => ({ title: entry.name, value: entry.name, })), }) selectedComponents = components } if (!selectedComponents?.length) { logger.warn('No components selected. Exiting.') process.exit(0) } const tree = await resolveTree(registryIndex, selectedComponents) const payload = await fetchTree(config.style, tree) const baseColor = await getRegistryBaseColor(config.tailwind.baseColor) if (!payload.length) { logger.warn('Selected components not found. Exiting.') process.exit(0) } if (!options.yes) { const { proceed } = await prompts({ type: 'confirm', name: 'proceed', message: 'Ready to install components and dependencies. Proceed?', initial: true, }) if (!proceed) process.exit(0) } const spinner = ora('Installing components...').start() const skippedDeps = new Set() for (const item of payload) { spinner.text = `Installing ${item.name}...` const targetDir = getItemTargetPath( config, item, options.path ? path.resolve(cwd, options.path) : undefined, ) if (!targetDir) continue if (!existsSync(targetDir)) await fs.mkdir(targetDir, { recursive: true }) const existingComponent = item.files.filter(file => existsSync(path.resolve(targetDir, item.name, file.name)), ) if (existingComponent.length && !options.overwrite) { if (selectedComponents.includes(item.name)) { logger.warn( `\nComponent ${ item.name } already exists. Use ${chalk.green( '--overwrite', )} to overwrite.`, ) spinner.stop() process.exitCode = 1 return } continue } for (const file of item.files) { const componentDir = path.resolve(targetDir, item.name) const filePath = path.resolve( targetDir, item.name, file.name, ) // Run transformers. const content = transformImport(file.content, config) if (!existsSync(componentDir)) await fs.mkdir(componentDir, { recursive: true }) await fs.writeFile(filePath, content) } // Install dependencies. if (item.dependencies?.length) { item.dependencies.forEach(dep => skippedDeps.add(dep), ) const packageManager = await getPackageManager(cwd) await execa( packageManager, [ packageManager === 'npm' ? 'install' : 'add', ...item.dependencies, ], { cwd, }, ) } } spinner.succeed('Done.') } catch (error) { handleError(error) } })