UNPKG

buildy-ui

Version:

A CLI for adding UI components to your Vite React projects (UI8Kit core/form registries)

1,328 lines (1,310 loc) 70.6 kB
#!/usr/bin/env // src/index.ts import { Command } from "commander"; import chalk7 from "chalk"; // src/commands/add.ts import chalk3 from "chalk"; import ora from "ora"; import path3 from "path"; import fs4 from "fs-extra"; import { execa } from "execa"; import fetch3 from "node-fetch"; // src/registry/api.ts import fetch from "node-fetch"; // src/registry/schema.ts import { z } from "zod"; // src/utils/schema-config.ts var SCHEMA_CONFIG = { // Base schema information schemaVersion: "http://json-schema.org/draft-07/schema#", baseUrl: "https://buildy.tw/schema", // Framework configuration supportedFrameworks: ["vite-react"], // Default aliases for path mapping defaultAliases: { "@": "./src", "@/components": "./src/components", "@/ui": "./src/components/ui", "@/layouts": "./src/layouts", "@/blocks": "./src/blocks", "@/lib": "./src/lib", "@/variants": "./src/variants" }, // Registry configuration defaultRegistry: "@ui8kit", registryTypes: ["ui"], // Default registry type defaultRegistryType: "ui", // CDN base URLs (registryName will be substituted) cdnBaseUrls: [ "https://cdn.jsdelivr.net/npm/@ui8kit/registry@latest/r", "https://unpkg.com/@ui8kit/registry@latest/r", "https://raw.githubusercontent.com/buildy-ui/ui/main/packages/@ui8kit/registry/r" ], // Component categories componentCategories: ["ui", "composite", "components", "layouts", "lib", "blocks", "variants"], // Component types (should match registryItemTypeSchema) componentTypes: [ "registry:lib", "registry:block", "registry:component", "registry:ui", "registry:composite", "registry:layout", "registry:variants" ], // Default directories structure // Source directories (where CLI scans for components) defaultDirectories: { components: "./src/components", lib: "./src/lib", layouts: "./src/layouts", blocks: "./src/blocks", variants: "./src/variants" }, // Schema descriptions and titles descriptions: { config: { title: "Buildy Configuration", description: "Configuration file for buildy CLI (UI8Kit structure)" }, registry: { title: "Buildy Registry", description: "Registry schema for UI8Kit component system" }, registryItem: { title: "Buildy Registry Item", description: "Schema for individual registry items in the UI8Kit component system" } }, // Field descriptions fieldDescriptions: { schema: "JSON Schema URL", framework: "Target framework", typescript: "Whether the project uses TypeScript", aliases: "Path aliases for imports in UI8Kit structure", registry: "Default component registry", componentsDir: "Directory where utility components will be installed", libDir: "Directory for utility libraries", registryName: "Registry name", registryHomepage: "Registry homepage URL", registryType: "Registry type (e.g., ui)", registryVersion: "Registry version", lastUpdated: "Last update timestamp", categories: "Available component categories", components: "Component metadata for quick lookup", items: "Full component definitions" } }; var TYPE_TO_FOLDER = { "registry:ui": "components/ui", "registry:composite": "components", "registry:block": "blocks", "registry:component": "components", "registry:lib": "lib", "registry:layout": "layouts", "registry:variants": "variants" }; function getCdnUrls(_registryType) { return [...SCHEMA_CONFIG.cdnBaseUrls]; } function isExternalDependency(moduleName) { return !moduleName.startsWith(".") && !moduleName.startsWith("@/") && !moduleName.startsWith("~/") && !moduleName.startsWith("@ui8kit/") && !moduleName.includes("\\") && moduleName !== "" && !moduleName.startsWith("file:"); } function getSchemaRef(schemaName) { return `${SCHEMA_CONFIG.baseUrl}/${schemaName}.json`; } // src/registry/schema.ts var componentFileSchema = z.object({ path: z.string(), content: z.string(), target: z.string().optional() }); var componentSchema = z.object({ name: z.string(), type: z.enum(SCHEMA_CONFIG.componentTypes), title: z.string().optional(), description: z.string().optional(), dependencies: z.array(z.string()).default([]), devDependencies: z.array(z.string()).default([]), registryDependencies: z.array(z.string()).optional(), files: z.array(componentFileSchema), tailwind: z.object({ config: z.object({ content: z.array(z.string()).optional(), theme: z.record(z.string(), z.any()).optional(), plugins: z.array(z.string()).optional() }).optional() }).optional(), cssVars: z.object({ theme: z.record(z.string(), z.string()).optional(), light: z.record(z.string(), z.string()).optional(), dark: z.record(z.string(), z.string()).optional() }).optional(), meta: z.record(z.string(), z.any()).optional() }); var configSchema = z.object({ $schema: z.string().optional(), framework: z.literal(SCHEMA_CONFIG.supportedFrameworks[0]), typescript: z.boolean().default(true), aliases: z.record(z.string(), z.string()).default(SCHEMA_CONFIG.defaultAliases), registry: z.string().default(SCHEMA_CONFIG.defaultRegistry), componentsDir: z.string().default(SCHEMA_CONFIG.defaultDirectories.components), libDir: z.string().default(SCHEMA_CONFIG.defaultDirectories.lib) }); // src/registry/api.ts var registryCache = /* @__PURE__ */ new Map(); function isUrl(path7) { try { new URL(path7); return true; } catch { return false; } } function getRegistryCache(registryType) { if (!registryCache.has(registryType)) { registryCache.set(registryType, { workingCDN: null, registryIndex: null }); } return registryCache.get(registryType); } async function findWorkingCDN(registryType) { const cache = getRegistryCache(registryType); if (cache.workingCDN) { return cache.workingCDN; } const cdnUrls = getCdnUrls(registryType); for (const baseUrl of cdnUrls) { try { console.log(`\u{1F50D} Testing ${registryType} CDN: ${baseUrl}`); const response = await fetch(`${baseUrl}/index.json`); if (response.ok) { cache.workingCDN = baseUrl; console.log(`\u2705 Using ${registryType} CDN: ${baseUrl}`); return baseUrl; } } catch (error) { console.log(`\u274C ${registryType} CDN failed: ${baseUrl}`); } } throw new Error(`No working ${registryType} CDN found`); } async function fetchFromWorkingCDN(path7, registryType) { const baseUrl = await findWorkingCDN(registryType); const url = `${baseUrl}/${path7}`; console.log(`\u{1F3AF} Fetching from ${registryType}: ${url}`); const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } async function getRegistryIndex(registryType) { const cache = getRegistryCache(registryType); if (cache.registryIndex) { return cache.registryIndex; } console.log(`\u{1F310} Fetching ${registryType} registry index`); cache.registryIndex = await fetchFromWorkingCDN("index.json", registryType); return cache.registryIndex; } async function getComponentByType(name, registryType) { try { const index = await getRegistryIndex(registryType); const componentInfo = index.components?.find((c) => c.name === name); if (!componentInfo) { console.log(`\u274C Component ${name} not found in ${registryType} registry`); return null; } const folder = TYPE_TO_FOLDER[componentInfo.type]; if (!folder) { console.log(`\u274C Unknown component type: ${componentInfo.type}`); return null; } console.log(`\u{1F3AF} Loading ${name} from /${folder}/ (type: ${componentInfo.type})`); const data = await fetchFromWorkingCDN(`${folder}/${name}.json`, registryType); return componentSchema.parse(data); } catch (error) { console.log(`\u274C Failed to get component by type: ${error.message}`); return null; } } async function getComponent(name, registryType = SCHEMA_CONFIG.defaultRegistryType) { try { if (isUrl(name)) { return await fetchFromUrl(name); } return await getComponentByType(name, registryType); } catch (error) { console.error(`\u274C Failed to fetch ${name} from ${registryType}:`, error.message); return null; } } async function fetchFromUrl(url) { console.log(`\u{1F310} Fetching component from: ${url}`); const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return componentSchema.parse(data); } async function getAllComponents(registryType = SCHEMA_CONFIG.defaultRegistryType) { try { console.log(`\u{1F310} Fetching all ${registryType} components using optimized approach`); const indexData = await getRegistryIndex(registryType); const components = []; if (indexData.components && Array.isArray(indexData.components)) { for (const componentInfo of indexData.components) { const component = await getComponent(componentInfo.name, registryType); if (component) { components.push(component); } } } return components; } catch (error) { console.error(`\u274C Failed to fetch all ${registryType} components:`, error.message); return []; } } // src/registry/retry-api.ts import fetch2 from "node-fetch"; var MAX_RETRIES = 3; var RETRY_DELAY = 2e3; var registryCache2 = /* @__PURE__ */ new Map(); function isUrl2(path7) { try { new URL(path7); return true; } catch { return false; } } function getRegistryCache2(registryType) { if (!registryCache2.has(registryType)) { registryCache2.set(registryType, { workingCDN: null, registryIndex: null }); } return registryCache2.get(registryType); } async function checkInternetConnection() { try { console.log("\u{1F310} Checking internet connection..."); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5e3); const response = await fetch2("https://www.google.com", { method: "HEAD", signal: controller.signal }); clearTimeout(timeoutId); return response.ok; } catch (error) { console.log("\u274C No internet connection detected"); return false; } } async function fetchWithRetry(url, retries = MAX_RETRIES) { for (let attempt = 1; attempt <= retries; attempt++) { try { console.log(`\u{1F504} Attempt ${attempt}/${retries}: ${url}`); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1e4); const response = await fetch2(url, { signal: controller.signal }); clearTimeout(timeoutId); if (response.ok) { return await response.json(); } else if (response.status === 404) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } else { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { console.log(`\u274C Attempt ${attempt} failed: ${error.message}`); if (attempt === retries) { throw error; } console.log(`\u23F3 Waiting ${RETRY_DELAY / 1e3}s before retry...`); await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); } } } async function findWorkingCDNWithRetry(registryType) { const cache = getRegistryCache2(registryType); if (cache.workingCDN) { return cache.workingCDN; } if (!await checkInternetConnection()) { throw new Error("No internet connection available"); } const cdnUrls = getCdnUrls(registryType); for (const baseUrl of cdnUrls) { try { console.log(`\u{1F50D} Testing ${registryType} CDN with retry: ${baseUrl}`); await fetchWithRetry(`${baseUrl}/index.json`, 2); cache.workingCDN = baseUrl; console.log(`\u2705 Using ${registryType} CDN: ${baseUrl}`); return baseUrl; } catch (error) { console.log(`\u274C ${registryType} CDN failed: ${baseUrl}`); } } throw new Error(`No working ${registryType} CDN found`); } async function fetchFromWorkingCDNWithRetry(path7, registryType) { const baseUrl = await findWorkingCDNWithRetry(registryType); const url = `${baseUrl}/${path7}`; console.log(`\u{1F3AF} Fetching from ${registryType} with retry: ${url}`); return await fetchWithRetry(url); } async function getRegistryIndexWithRetry(registryType) { const cache = getRegistryCache2(registryType); if (cache.registryIndex) { return cache.registryIndex; } console.log(`\u{1F310} Fetching ${registryType} registry index with retry`); cache.registryIndex = await fetchFromWorkingCDNWithRetry("index.json", registryType); return cache.registryIndex; } async function getComponentByTypeWithRetry(name, registryType) { try { const index = await getRegistryIndexWithRetry(registryType); const componentInfo = index.components?.find((c) => c.name === name); if (!componentInfo) { console.log(`\u274C Component ${name} not found in ${registryType} registry`); return null; } const folder = TYPE_TO_FOLDER[componentInfo.type]; if (!folder) { console.log(`\u274C Unknown component type: ${componentInfo.type}`); return null; } console.log(`\u{1F3AF} Loading ${name} from /${registryType}/${folder}/ (type: ${componentInfo.type})`); const data = await fetchFromWorkingCDNWithRetry(`${folder}/${name}.json`, registryType); return componentSchema.parse(data); } catch (error) { console.log(`\u274C Failed to get component by type: ${error.message}`); return null; } } async function getComponentWithRetry(name, registryType = SCHEMA_CONFIG.defaultRegistryType) { try { if (isUrl2(name)) { return await fetchFromUrlWithRetry(name); } if (!await checkInternetConnection()) { throw new Error("No internet connection available"); } return await getComponentByTypeWithRetry(name, registryType); } catch (error) { console.error(`\u274C Failed to fetch ${name} from ${registryType}:`, error.message); return null; } } async function fetchFromUrlWithRetry(url) { console.log(`\u{1F310} Fetching component from URL with retry: ${url}`); const data = await fetchWithRetry(url); return componentSchema.parse(data); } async function getAllComponentsWithRetry(registryType = SCHEMA_CONFIG.defaultRegistryType) { try { console.log(`\u{1F310} Fetching all ${registryType} components with retry logic`); if (!await checkInternetConnection()) { throw new Error("No internet connection available"); } const indexData = await getRegistryIndexWithRetry(registryType); const components = []; if (indexData.components && Array.isArray(indexData.components)) { for (const componentInfo of indexData.components) { const component = await getComponentWithRetry(componentInfo.name, registryType); if (component) { components.push(component); } } } return components; } catch (error) { console.error(`\u274C Failed to fetch all ${registryType} components:`, error.message); return []; } } // src/utils/project.ts import fs from "fs-extra"; import path from "path"; async function isViteProject() { const viteConfigFiles = [ "vite.config.ts", "vite.config.js", "vite.config.mts", "vite.config.mjs" ]; for (const file of viteConfigFiles) { if (await fs.pathExists(file)) { return true; } } return false; } async function hasReact() { const packageJsonPath = path.join(process.cwd(), "package.json"); if (!await fs.pathExists(packageJsonPath)) { return false; } const packageJson = await fs.readJson(packageJsonPath); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; return "react" in deps; } async function findConfig(_registryType) { const srcConfig = await getConfig("./src"); if (srcConfig) return srcConfig; if (_registryType) { const registryConfig = await getConfig(`./${_registryType}`); if (registryConfig) return registryConfig; } return await getConfig(); } async function getConfig(registryPath) { const configPath = registryPath ? path.join(process.cwd(), registryPath, "buildy.config.json") : path.join(process.cwd(), "buildy.config.json"); if (!await fs.pathExists(configPath)) { return null; } try { const config = await fs.readJson(configPath); return configSchema.parse(config); } catch (error) { console.error("\u274C Invalid buildy.config.json:", error.message); return null; } } async function saveConfig(config, registryPath) { const configPath = registryPath ? path.join(process.cwd(), registryPath, "buildy.config.json") : path.join(process.cwd(), "buildy.config.json"); await fs.ensureDir(path.dirname(configPath)); await fs.writeJson(configPath, config, { spaces: 2 }); } async function ensureDir(dirPath) { await fs.ensureDir(dirPath); } // src/utils/registry-validator.ts import fs2 from "fs-extra"; import chalk from "chalk"; async function validateComponentInstallation(components, registryType) { return { isValid: true }; } function handleValidationError(result) { console.error(chalk.red("\u274C Registry Validation Error:")); console.error(chalk.red(result.message)); if (result.missingComponents && result.missingComponents.length > 0) { console.log(chalk.yellow("\n\u{1F4A1} Suggestion:")); console.log(`Install missing components first: ${chalk.cyan(`npx buildy-ui add ${result.missingComponents.join(" ")}`)} `); } process.exit(1); } // src/utils/dependency-checker.ts import fs3 from "fs-extra"; import path2 from "path"; import chalk2 from "chalk"; async function checkProjectDependencies(requiredDeps) { const packageJsonPath = path2.join(process.cwd(), "package.json"); if (!await fs3.pathExists(packageJsonPath)) { return { installed: [], missing: requiredDeps, workspace: [] }; } try { const packageJson = await fs3.readJson(packageJsonPath); const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; const installed = []; const missing = []; const workspace = []; for (const dep of requiredDeps) { const version = allDeps[dep]; if (!version) { missing.push(dep); } else if (version.startsWith("workspace:")) { workspace.push(dep); } else { installed.push(dep); } } return { installed, missing, workspace }; } catch (error) { return { installed: [], missing: requiredDeps, workspace: [] }; } } function showDependencyStatus(deps) { if (deps.installed.length > 0) { console.log(chalk2.green(` \u2705 Already installed: ${deps.installed.join(", ")}`)); } if (deps.workspace.length > 0) { console.log(chalk2.blue(` \u{1F517} Workspace dependencies: ${deps.workspace.join(", ")}`)); } if (deps.missing.length > 0) { console.log(chalk2.yellow(` \u{1F4E6} Will install: ${deps.missing.join(", ")}`)); } } async function filterMissingDependencies(dependencies) { const status = await checkProjectDependencies(dependencies); return status.missing; } function isWorkspaceError(errorMessage) { return errorMessage.includes("EUNSUPPORTEDPROTOCOL") || errorMessage.includes("workspace:") || errorMessage.includes('Unsupported URL Type "workspace:"'); } // src/utils/cli-messages.ts var CLI_MESSAGES = { // Error messages errors: { noComponentsSpecified: "Please specify at least one component to add.", notInitialized: "buildy is not initialized in this project.", notViteProject: "This doesn't appear to be a Vite project.", reactNotInstalled: "React is not installed in this project.", buildFailed: "Build failed:", scanFailed: "Scan failed:", registryTempUnavailable: "registry temporarily unavailable", noCDNFound: (registryType) => `No working ${registryType} CDN found`, componentNotFound: (name, registryType) => `Component "${name}" not found in ${registryType} registry`, unknownComponentType: (type) => `Unknown component type: ${type}`, fileNotFound: (path7) => `File not found: ${path7}`, invalidConfig: "Invalid buildy.config.json:", failedToInstall: (name, registryType) => `Failed to install ${name} from ${registryType}`, failedToFetch: (registryType) => `Failed to fetch components from ${registryType}`, failedToFetchComponent: (name, registryType) => `Failed to fetch ${name} from ${registryType}:`, failedToAnalyzeDeps: (path7) => `Warning: Could not analyze dependencies for ${path7}:`, dependenciesFailed: "Failed to install dependencies", couldNotInstallDeps: (name) => `Warning: Could not install some dependencies for ${name}` }, // Success messages success: { initialized: (registryName) => `UI8Kit ${registryName} structure initialized successfully!`, setupComplete: (registryName) => `UI8Kit ${registryName} Setup complete!`, installed: (name, registryType) => `Installed ${name} from ${registryType}`, allInstalled: (registryType) => `All ${registryType} components installed successfully!`, componentsInstalled: "Components installed successfully!", depsInstalled: "Dependencies installed", registryBuilt: "Registry built successfully!", schemasGenerated: "Schema files generated successfully!", registryGenerated: (registryName) => `${registryName} registry generated successfully!`, depsAvailable: "All dependencies already available" }, // Info messages info: { initializing: (registryName) => `Initializing UI8Kit buildy in your project (${registryName} registry)...`, installing: (registryName) => `Installing from ${registryName} registry...`, installingAll: (registryName) => `Installing all available components from ${registryName} registry...`, retryEnabled: "Retry mode enabled - using enhanced connection logic", fetchingComponentList: (registryType) => `Fetching component list from ${registryType}...`, fetchingRegistry: (registryType) => `Fetching ${registryType} registry index`, scanningComponents: (registryName) => `Scanning ${registryName} components...`, building: "Building registry...", processingComponents: "Processing components...", scanningDirectories: "Scanning directories...", analyzingDeps: "Found {count} components, analyzing dependencies...", installationCancelled: "Initialization cancelled.", workspaceDepsDetected: "Workspace dependency detected. Installing individually...", checkingConnection: "Checking internet connection...", testing: (registryType, baseUrl) => `Testing ${registryType} CDN: ${baseUrl}`, using: (registryType, baseUrl) => `Using ${registryType} CDN: ${baseUrl}`, loading: (name, registryType, folder, type) => `Loading ${name} from /${registryType}/${folder}/ (type: ${type})`, fetching: (registryType, url) => `Fetching from ${registryType}: ${url}`, fetchingUrl: "Fetching component from:", fetchingUrlWithRetry: "Fetching component from URL with retry:" }, // Prompts prompts: { typescript: "Are you using TypeScript?", overwrite: (registryName) => `UI8Kit buildy is already initialized for ${registryName} registry. Overwrite configuration?` }, // Examples and help text examples: { add: [ "Example: npx buildy-ui@latest add button card", "Example: npx buildy-ui@latest add button --registry ui", "Example: npx buildy-ui@latest add all # Install all components", "Example: npx buildy-ui@latest add --all --registry ui # Install all ui components", "Example: npx buildy-ui@latest add --retry # Enable retry for unreliable connections", 'Example: npx buildy-ui@latest add "https://example.com/component.json"' ], init: [ `npx buildy-ui@latest add button --registry ui - Add a button component`, `npx buildy-ui@latest add card input --registry ui - Add multiple components`, `npx buildy-ui@latest add --all --registry ui - Add all components`, `npx buildy-ui@latest add "https://example.com/component.json" - Add from external URL` ], troubleshooting: [ "Check your internet connection", "Use --retry flag: npx buildy-ui@latest add --all --retry", "Use VPN if available", "Install from URL: npx buildy-ui@latest add 'https://...'", "Check https://buildy.tw for manual download" ] }, // Directory descriptions directories: { lib: "Utils, helpers, functions", variants: "CVA variant configurations", "components/ui": "UI components", components: "Complex components", layouts: "Page layouts and structures", blocks: "Component blocks" }, // Spinners and status status: { installing: (name, registryType) => `Installing ${name} from ${registryType}...`, wouldInstall: (name, registryType) => `Would install: ${name} from ${registryType}`, foundComponents: (count, registryType) => `Found ${count} components in ${registryType}`, wouldInstallFrom: (registryType) => `Would install from ${registryType}:`, builtComponents: (count) => `Built ${count} components`, scannedComponents: (count) => `Scanned ${count} components`, skipped: (fileName) => `Skipped ${fileName} (already exists, use --force to overwrite)` } }; // src/commands/add.ts async function addCommand(components, options) { const registryType = resolveRegistryType(options.registry); if (options.all || components.includes("all")) { return await addAllComponents(options, registryType); } if (components.length === 0) { console.error(chalk3.red(`\u274C ${CLI_MESSAGES.errors.noComponentsSpecified}`)); CLI_MESSAGES.examples.add.forEach((example) => console.log(example)); process.exit(1); } const validation = await validateComponentInstallation(components, registryType); if (!validation.isValid) { handleValidationError(validation); } const config = await findConfig(registryType); if (!config) { console.error(chalk3.red(`\u274C ${CLI_MESSAGES.errors.notInitialized}`)); console.log(`Run 'npx buildy-ui@latest init' first.`); console.log(`For ${registryType} registry, run: npx buildy-ui@latest init --registry ${registryType}`); process.exit(1); } const getComponentFn = options.retry ? getComponentWithRetry : getComponent; if (options.retry) { console.log(chalk3.blue(`\u{1F504} ${CLI_MESSAGES.info.retryEnabled}`)); } console.log(chalk3.blue(`\u{1F4E6} ${CLI_MESSAGES.info.installing(registryType)}`)); const results = await processComponents(components, registryType, config, getComponentFn, options); displayInstallationSummary(registryType, results); } async function addAllComponents(options, registryType) { console.log(chalk3.blue(`\u{1F680} ${CLI_MESSAGES.info.installingAll(registryType)}`)); const config = await findConfig(registryType); if (!config) { console.error(chalk3.red(`\u274C ${CLI_MESSAGES.errors.notInitialized}`)); console.log(`Run 'npx buildy-ui@latest init' first.`); console.log(`For ${registryType} registry, run: npx buildy-ui@latest init --registry ${registryType}`); process.exit(1); } const getAllComponentsFn = options.retry ? getAllComponentsWithRetry : getAllComponents; if (options.retry) { console.log(chalk3.blue(`\u{1F504} ${CLI_MESSAGES.info.retryEnabled}`)); } const spinner = ora(CLI_MESSAGES.info.fetchingComponentList(registryType)).start(); try { const allComponents = await getAllComponentsFn(registryType); if (allComponents.length === 0) { spinner.fail(`No components found in ${registryType} registry`); console.log(chalk3.yellow(` \u26A0\uFE0F ${registryType} ${CLI_MESSAGES.errors.registryTempUnavailable}`)); console.log("Try these alternatives:"); CLI_MESSAGES.examples.troubleshooting.forEach((alt) => console.log(` \u2022 ${alt}`)); return; } spinner.succeed(CLI_MESSAGES.status.foundComponents(allComponents.length, registryType)); if (options.dryRun) { console.log(chalk3.blue(` \u{1F4CB} ${CLI_MESSAGES.status.wouldInstallFrom(registryType)}`)); allComponents.forEach((comp) => { console.log(` - ${comp.name} (${comp.type})`); }); return; } const results = await processComponents( allComponents.map((c) => c.name), registryType, config, options.retry ? getComponentWithRetry : getComponent, options, allComponents ); await installComponentsIndex(registryType, config); displayInstallationSummary(registryType, results); } catch (error) { spinner.fail(CLI_MESSAGES.errors.failedToFetch(registryType)); console.error(chalk3.red("\u274C Error:"), error.message); console.log(chalk3.yellow(` \u26A0\uFE0F ${registryType} ${CLI_MESSAGES.errors.registryTempUnavailable}`)); console.log("Try these alternatives:"); CLI_MESSAGES.examples.troubleshooting.forEach((alt) => console.log(` \u2022 ${alt}`)); process.exit(1); } } async function processComponents(componentNames, registryType, config, getComponentFn, options, preloadedComponents) { const results = []; const componentMap = new Map(preloadedComponents?.map((c) => [c.name, c])); for (const componentName of componentNames) { const spinner = ora(CLI_MESSAGES.status.installing(componentName, registryType)).start(); try { let component = componentMap?.get(componentName); if (!component) { component = await getComponentFn(componentName, registryType); } if (!component) { throw new Error(CLI_MESSAGES.errors.componentNotFound(componentName, registryType)); } if (options.dryRun) { spinner.succeed(CLI_MESSAGES.status.wouldInstall(component.name, registryType)); console.log(` Type: ${component.type}`); console.log(` Files: ${component.files.length}`); console.log(` Dependencies: ${component.dependencies.join(", ") || "none"}`); if (component.dependencies.length > 0) { const depStatus = await checkProjectDependencies(component.dependencies); showDependencyStatus(depStatus); } continue; } await installComponentFiles(component, config, options.force); if (component.dependencies.length > 0) { try { await installDependencies(component.dependencies); } catch (error) { console.log(chalk3.yellow(` \u26A0\uFE0F ${CLI_MESSAGES.errors.couldNotInstallDeps(component.name)}`)); console.log(chalk3.yellow(` Dependencies: ${component.dependencies.join(", ")}`)); console.log(chalk3.yellow(` Please install them manually if needed`)); } } spinner.succeed(CLI_MESSAGES.status.installing(component.name, registryType)); results.push({ name: component.name, status: "success" }); } catch (error) { spinner.fail(CLI_MESSAGES.errors.failedToInstall(componentName, registryType)); console.error(chalk3.red(` Error: ${error.message}`)); results.push({ name: componentName, status: "error", error: error.message }); } } return results; } function displayInstallationSummary(registryType, results) { const successful = results.filter((r) => r.status === "success"); const failed = results.filter((r) => r.status === "error"); console.log(chalk3.blue("\n\u{1F4CA} Installation Summary:")); console.log(` Registry: ${registryType}`); console.log(` \u2705 Successful: ${successful.length}`); console.log(` \u274C Failed: ${failed.length}`); if (successful.length > 0) { console.log(chalk3.green(` \u{1F389} ${CLI_MESSAGES.success.componentsInstalled}`)); console.log("You can now import and use them in your project."); } if (failed.length > 0) { process.exit(1); } } async function installComponentFiles(component, config, force = false) { for (const file of component.files) { const fileName = path3.basename(file.path); const target = file.target || inferTargetFromType(component.type); const installDir = resolveInstallDir(target, config); const targetPath = path3.join(process.cwd(), installDir, fileName); if (!force && await fs4.pathExists(targetPath)) { console.log(` \u26A0\uFE0F ${CLI_MESSAGES.status.skipped(fileName)}`); continue; } await fs4.ensureDir(path3.dirname(targetPath)); await fs4.writeFile(targetPath, file.content, "utf-8"); } } function inferTargetFromType(componentType) { switch (componentType) { case "registry:ui": return "ui"; case "registry:composite": return "components"; case "registry:block": return "blocks"; case "registry:component": return "components"; case "registry:layout": return "layouts"; case "registry:lib": return "lib"; case "registry:variants": return "variants"; default: return "components"; } } function resolveInstallDir(target, config) { const normalizedTarget = target.replace(/\\/g, "/").replace(/^\/?src\//i, ""); if (normalizedTarget === "lib") { return normalizeDir(config.libDir || SCHEMA_CONFIG.defaultDirectories.lib); } if (normalizedTarget === "variants") { return normalizeDir(SCHEMA_CONFIG.defaultDirectories.variants); } const baseComponentsDir = normalizeDir(config.componentsDir || SCHEMA_CONFIG.defaultDirectories.components); if (normalizedTarget.includes("/")) { const parentRoot = baseComponentsDir.replace(/[/\\]components$/i, "") || "src"; return path3.join(parentRoot, normalizedTarget).replace(/\\/g, "/"); } if (normalizedTarget === "ui") return path3.join(baseComponentsDir, "ui").replace(/\\/g, "/"); if (normalizedTarget === "components") return baseComponentsDir; switch (normalizedTarget) { case "blocks": return normalizeDir(SCHEMA_CONFIG.defaultDirectories.blocks); case "layouts": return normalizeDir(SCHEMA_CONFIG.defaultDirectories.layouts); default: return baseComponentsDir; } } function normalizeDir(dir) { return dir.replace(/^\.\//, "").replace(/\\/g, "/"); } function resolveRegistryType(registryInput) { if (!registryInput) { return SCHEMA_CONFIG.defaultRegistryType; } if (SCHEMA_CONFIG.registryTypes.includes(registryInput)) { return registryInput; } console.warn(chalk3.yellow(`\u26A0\uFE0F Unknown registry type: ${registryInput}`)); console.log(`Available registries: ${SCHEMA_CONFIG.registryTypes.join(", ")}`); console.log(`Using default: ${SCHEMA_CONFIG.defaultRegistryType}`); return SCHEMA_CONFIG.defaultRegistryType; } async function installDependencies(dependencies) { const spinner = ora(CLI_MESSAGES.status.installing("dependencies", "")).start(); try { const depStatus = await checkProjectDependencies(dependencies); const missingDependencies = await filterMissingDependencies(dependencies); if (missingDependencies.length === 0) { spinner.succeed(CLI_MESSAGES.success.depsAvailable); if (depStatus.workspace.length > 0) { console.log(chalk3.blue(` \u{1F517} Using workspace dependencies: ${depStatus.workspace.join(", ")}`)); } return; } showDependencyStatus(depStatus); const packageManager = await detectPackageManager(); const installCommand = packageManager === "npm" ? ["install", ...missingDependencies] : ["add", ...missingDependencies]; await execa(packageManager, installCommand, { cwd: process.cwd(), stdio: "pipe" }); spinner.succeed(CLI_MESSAGES.success.depsInstalled); } catch (error) { spinner.fail(CLI_MESSAGES.errors.dependenciesFailed); const errorMessage = error.stderr || error.message; if (isWorkspaceError(errorMessage)) { console.log(chalk3.yellow(` \u{1F4A1} ${CLI_MESSAGES.info.workspaceDepsDetected}`)); const results = await installDependenciesIndividually(dependencies); if (results.some((r) => r.success)) { console.log(chalk3.green("\u2705 Some dependencies installed successfully")); return; } } throw new Error(`${CLI_MESSAGES.errors.dependenciesFailed}: ${errorMessage}`); } } async function installDependenciesIndividually(dependencies) { const packageManager = await detectPackageManager(); const results = []; const missingDeps = await filterMissingDependencies(dependencies); for (const dep of missingDeps) { try { const installCommand = packageManager === "npm" ? ["install", dep] : ["add", dep]; await execa(packageManager, installCommand, { cwd: process.cwd(), stdio: "pipe" }); console.log(chalk3.green(` \u2705 Installed ${dep}`)); results.push({ dep, success: true }); } catch (error) { console.log(chalk3.yellow(` \u26A0\uFE0F Skipped ${dep} (may already be available)`)); results.push({ dep, success: false }); } } return results; } async function detectPackageManager() { if (await fs4.pathExists("bun.lock")) return "bun"; if (await fs4.pathExists("pnpm-lock.yaml")) return "pnpm"; if (await fs4.pathExists("yarn.lock")) return "yarn"; return "npm"; } async function installComponentsIndex(registryType, config) { const spinner = ora("Installing components index...").start(); try { const cdnUrls = SCHEMA_CONFIG.cdnBaseUrls; for (const baseUrl of cdnUrls) { try { const url = `${baseUrl}/components/index.json`; const response = await fetch3(url); if (response.ok) { const component = await response.json(); for (const file of component.files) { const fileName = path3.basename(file.path); const targetDir = config.componentsDir; const targetPath = path3.join(process.cwd(), targetDir, fileName); await fs4.ensureDir(path3.dirname(targetPath)); await fs4.writeFile(targetPath, file.content || "", "utf-8"); } spinner.succeed("Installed components index"); return; } } catch { continue; } } spinner.info("Components index not found in registry (optional)"); } catch (error) { spinner.fail("Could not install components index"); } } // src/commands/init.ts import chalk4 from "chalk"; import prompts from "prompts"; import ora2 from "ora"; import path4 from "path"; import fs5 from "fs-extra"; import fetch4 from "node-fetch"; async function initCommand(options) { const registryName = options.registry || SCHEMA_CONFIG.defaultRegistryType; const registryPath = `./${registryName}`; console.log(chalk4.blue(`\u{1F680} ${CLI_MESSAGES.info.initializing(registryName)}`)); if (!await isViteProject()) { console.error(chalk4.red(`\u274C ${CLI_MESSAGES.errors.notViteProject}`)); console.log("Please run this command in a Vite project directory."); process.exit(1); } if (!await hasReact()) { console.error(chalk4.red(`\u274C ${CLI_MESSAGES.errors.reactNotInstalled}`)); console.log("Please install React first: npm install react react-dom"); process.exit(1); } const existingConfig = await getConfig("./src"); if (existingConfig && !options.yes) { const { overwrite } = await prompts({ type: "confirm", name: "overwrite", message: CLI_MESSAGES.prompts.overwrite(registryName), initial: false }); if (!overwrite) { console.log(chalk4.yellow(`\u26A0\uFE0F ${CLI_MESSAGES.info.installationCancelled}`)); return; } } const aliases = SCHEMA_CONFIG.defaultAliases; let config; if (options.yes) { config = { $schema: "https://buildy.tw/schema.json", framework: "vite-react", typescript: true, aliases, registry: SCHEMA_CONFIG.defaultRegistry, componentsDir: SCHEMA_CONFIG.defaultDirectories.components, libDir: SCHEMA_CONFIG.defaultDirectories.lib }; } else { const responses = await prompts([ { type: "confirm", name: "typescript", message: CLI_MESSAGES.prompts.typescript, initial: true } ]); config = { $schema: "https://buildy.tw/schema.json", framework: "vite-react", typescript: responses.typescript, aliases, registry: SCHEMA_CONFIG.defaultRegistry, componentsDir: SCHEMA_CONFIG.defaultDirectories.components, libDir: SCHEMA_CONFIG.defaultDirectories.lib }; } const spinner = ora2(CLI_MESSAGES.info.initializing(registryName)).start(); try { await saveConfig(config, "./src"); await ensureDir(config.libDir); await ensureDir(config.componentsDir); await ensureDir(path4.join(config.componentsDir, "ui")); await ensureDir(SCHEMA_CONFIG.defaultDirectories.blocks); await ensureDir(SCHEMA_CONFIG.defaultDirectories.layouts); await ensureDir(SCHEMA_CONFIG.defaultDirectories.variants); spinner.text = "Installing core utilities and variants..."; await installCoreFiles(registryName, config, spinner); spinner.succeed(CLI_MESSAGES.success.initialized(registryName)); console.log(chalk4.green(` \u2705 ${CLI_MESSAGES.success.setupComplete(registryName)}`)); console.log("\nDirectories created:"); console.log(` ${chalk4.cyan("src/lib/")} - Utils, helpers, functions`); console.log(` ${chalk4.cyan("src/variants/")} - CVA variant configurations`); console.log(` ${chalk4.cyan("src/components/ui/")} - UI components`); console.log(` ${chalk4.cyan("src/components/")} - Complex components`); console.log(` ${chalk4.cyan("src/layouts/")} - Page layouts and structures`); console.log(` ${chalk4.cyan("src/blocks/")} - Component blocks`); console.log("\nNext steps:"); CLI_MESSAGES.examples.init.forEach((example) => console.log(` ${chalk4.cyan(example)}`)); } catch (error) { spinner.fail(CLI_MESSAGES.errors.buildFailed); console.error(chalk4.red("\u274C Error:"), error.message); process.exit(1); } } async function installCoreFiles(registryType, config, spinner) { const cdnUrls = getCdnUrls(registryType); let registryIndex = null; for (const baseUrl of cdnUrls) { try { const indexUrl = `${baseUrl}/index.json`; const response = await fetch4(indexUrl); if (response.ok) { registryIndex = await response.json(); break; } } catch { continue; } } if (!registryIndex) { spinner.text = "\u26A0\uFE0F Could not fetch registry index, creating local utils..."; await createUtilsFile(config.libDir, config.typescript); return; } const variantItems = registryIndex.components.filter((c) => c.type === "registry:variants"); const libItems = registryIndex.components.filter((c) => c.type === "registry:lib"); for (const item of libItems) { spinner.text = `Installing ${item.name}...`; await installComponentFromRegistry(item.name, "registry:lib", cdnUrls, config); } for (const item of variantItems) { spinner.text = `Installing variant: ${item.name}...`; await installComponentFromRegistry(item.name, "registry:variants", cdnUrls, config); } spinner.text = `Installing variants index...`; await installVariantsIndex(cdnUrls, config); spinner.text = `\u2705 Installed ${libItems.length} utilities and ${variantItems.length} variants`; } async function installComponentFromRegistry(name, type, cdnUrls, config) { const folder = type === "registry:lib" ? "lib" : type === "registry:variants" ? "components/variants" : "components/ui"; for (const baseUrl of cdnUrls) { try { const url = `${baseUrl}/${folder}/${name}.json`; const response = await fetch4(url); if (response.ok) { const component = await response.json(); for (const file of component.files) { const fileName = path4.basename(file.path); let targetDir; if (type === "registry:lib") { targetDir = config.libDir; } else if (type === "registry:variants") { targetDir = SCHEMA_CONFIG.defaultDirectories.variants; } else { targetDir = path4.join(config.componentsDir, "ui"); } const targetPath = path4.join(process.cwd(), targetDir, fileName); await fs5.ensureDir(path4.dirname(targetPath)); await fs5.writeFile(targetPath, file.content || "", "utf-8"); } return; } } catch { continue; } } } async function installVariantsIndex(cdnUrls, config) { for (const baseUrl of cdnUrls) { try { const url = `${baseUrl}/components/variants/index.json`; const response = await fetch4(url); if (response.ok) { const component = await response.json(); for (const file of component.files) { const fileName = path4.basename(file.path); const targetDir = SCHEMA_CONFIG.defaultDirectories.variants; const targetPath = path4.join(process.cwd(), targetDir, fileName); await fs5.ensureDir(path4.dirname(targetPath)); await fs5.writeFile(targetPath, file.content || "", "utf-8"); } return; } } catch { continue; } } } async function createUtilsFile(libDir, typescript) { const utilsContent = `import { type ClassValue, clsx } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) }`; const fileName = typescript ? "utils.ts" : "utils.js"; const filePath = path4.join(process.cwd(), libDir, fileName); await fs5.writeFile(filePath, utilsContent, "utf-8"); } // src/commands/build.ts import fs6 from "fs-extra"; import path5 from "path"; import chalk5 from "chalk"; import ora3 from "ora"; // src/registry/build-schema.ts import { z as z2 } from "zod"; var registryItemTypeSchema = z2.enum([ "registry:lib", "registry:block", "registry:component", "registry:ui", "registry:composite", "registry:layout", "registry:variants" ]); var registryItemFileSchema = z2.object({ path: z2.string(), content: z2.string().optional(), // Populated during build target: z2.string().optional() }); var registryItemTailwindSchema = z2.object({ config: z2.object({ content: z2.array(z2.string()).optional(), theme: z2.record(z2.string(), z2.any()).optional(), plugins: z2.array(z2.string()).optional() }).optional() }); var registryItemCssVarsSchema = z2.object({ theme: z2.record(z2.string(), z2.string()).optional(), light: z2.record(z2.string(), z2.string()).optional(), dark: z2.record(z2.string(), z2.string()).optional() }); var registryItemSchema = z2.object({ $schema: z2.string().optional(), name: z2.string(), type: registryItemTypeSchema, title: z2.string().optional(), description: z2.string().optional(), dependencies: z2.array(z2.string()).default([]), devDependencies: z2.array(z2.string()).default([]), registryDependencies: z2.array(z2.string()).optional(), files: z2.array(registryItemFileSchema), tailwind: registryItemTailwindSchema.optional(), cssVars: registryItemCssVarsSchema.optional(), meta: z2.record(z2.string(), z2.any()).optional() }); var registrySchema = z2.object({ $schema: z2.string().optional(), items: z2.array(registryItemSchema) }); // src/utils/schema-generator.ts import { zodToJsonSchema } from "zod-to-json-schema"; import { z as z3 } from "zod"; var fullRegistrySchema = z3.object({ $schema: z3.string().optional(), name: z3.string().optional(), homepage: z3.string().optional(), registry: z3.enum(SCHEMA_CONFIG.registryTypes).optional(), version: z3.string().optional(), lastUpdated: z3.string().optional(), categories: z3.array(z3.enum(SCHEMA_CONFIG.componentCategories)).optional(), components: z3.array(z3.object({ name: z3.string(), type: registryItemTypeSchema, description: z3.string().optional() })).optional(), items: z3.array(registryItemSchema) }); function generateConfigSchema() { const baseSchema = zodToJsonSchema(configSchema, { name: "BuildyConfiguration", $refStrategy: "none" }); const actualSchema = baseSchema.definitions?.BuildyConfiguration || baseSchema; return { "$schema": SCHEMA_CONFIG.schemaVersion, "title": SCHEMA_CONFIG.descriptions.config.title, "description": SCHEMA_CONFIG.descriptions.config.description, "type": "object", "properties": { "$schema": { "type": "string", "description": SCHEMA_CONFIG.fieldDescriptions.schema }, "framework": { "type": "string", "enum": SCHEMA_CONFIG.supportedFrameworks, "description": SCHEMA_CONFIG.fieldDescriptions.framework }, "typescript": { "type": "boolean", "default": true, "description": SCHEMA_CONFIG.fieldDescriptions.typescript }, "aliases": { "type": "object", "additionalProperties": { "type": "string" }, "default": SCHEMA_CONFIG.defaultAliases, "description": SCHEMA_CONFIG.fieldDescriptions.aliases }, "registry": { "type": "string", "default": SCHEMA_CONFIG.defaultRegistry, "description": SCHEMA_CONFIG.fieldDescriptions.registry }, "componentsDir": { "type": "string", "default": SCHEMA_CONFIG.defaultDirectories.components, "description": SCHEMA_CONFIG.fieldDescriptions.componentsDir }, "libDir": { "type": "string", "default": SCHEMA_CONFIG.defaultDirectories.lib, "description": SCHEMA_CONFIG.fieldDescriptions.libDir } }, "required": ["framework"], "additionalProperties": false }; } function generateRegistrySchema() { const ba