UNPKG

buildy-ui

Version:

A CLI for adding UI components to your Vite React projects (UI8Kit utility components)

1,342 lines (1,324 loc) 62.5 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 path4 from "path"; import fs4 from "fs-extra"; import { execa } from "execa"; // src/registry/api.ts import fetch from "node-fetch"; // src/registry/schema.ts import { z } from "zod"; var componentFileSchema = z.object({ path: z.string(), content: z.string(), target: z.string().optional() }); var componentSchema = z.object({ name: z.string(), type: z.enum(["registry:ui", "registry:block", "registry:component", "registry:lib", "registry:template"]), description: z.string().optional(), dependencies: z.array(z.string()).default([]), devDependencies: z.array(z.string()).default([]), files: z.array(componentFileSchema) }); var configSchema = z.object({ $schema: z.string().optional(), framework: z.literal("vite-react"), typescript: z.boolean().default(true), aliases: z.record(z.string()).default({ "@": "./src", "@/components": "./utility/components", "@/ui": "./utility/ui", "@/blocks": "./utility/blocks", "@/lib": "./lib", "@/utility": "./utility", "@/semantic": "./semantic", "@/theme": "./theme" }), registry: z.string().default("@ui8kit"), componentsDir: z.string().default("./utility/ui"), libDir: z.string().default("./lib") }); // 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": "./utility/components", "@/ui": "./utility/ui", "@/blocks": "./utility/blocks", "@/lib": "./lib", "@/utility": "./utility", "@/semantic": "./semantic", "@/theme": "./theme" }, // Registry configuration defaultRegistry: "@ui8kit", registryTypes: ["utility", "semantic", "theme"], // Default registry type defaultRegistryType: "utility", // CDN base URLs (registryName will be substituted) cdnBaseUrls: [ "https://cdn.jsdelivr.net/npm/ui8kit@latest/r", "https://unpkg.com/ui8kit@latest/r", "https://raw.githubusercontent.com/buildy-ui/ui/main/packages/ui/packages/registry/r" ], // Component categories componentCategories: ["ui", "components", "blocks", "lib", "templates"], // Component types (should match registryItemTypeSchema) componentTypes: [ "registry:lib", "registry:block", "registry:component", "registry:ui", "registry:template" ], // Default directories defaultDirectories: { components: "./utility/ui", lib: "./lib" }, // 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 in UI8Kit architecture", 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": "ui", "registry:block": "blocks", "registry:component": "components", "registry:lib": "lib", "registry:template": "templates" }; function getCdnUrls(registryType) { return SCHEMA_CONFIG.cdnBaseUrls.map((baseUrl) => `${baseUrl}/${registryType}`); } function getInstallPath(registryType, componentType) { const folder = TYPE_TO_FOLDER[componentType]; if (componentType === "registry:lib") { return "lib"; } return `${registryType}/${folder}`; } 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/api.ts var registryCache = /* @__PURE__ */ new Map(); function isUrl(path8) { try { new URL(path8); 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(path8, registryType) { const baseUrl = await findWorkingCDN(registryType); const url = `${baseUrl}/${path8}`; 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 /${registryType}/${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(path8) { try { new URL(path8); 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(path8, registryType) { const baseUrl = await findWorkingCDNWithRetry(registryType); const url = `${baseUrl}/${path8}`; 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 fs2 from "fs-extra"; import path2 from "path"; // src/utils/registry-validator.ts import fs from "fs-extra"; import path from "path"; import chalk from "chalk"; async function isUtilityRegistryInitialized() { const utilityConfigPath = path.join(process.cwd(), "utility", "buildy.config.json"); return await fs.pathExists(utilityConfigPath); } async function canUseRegistry(registryType) { if (registryType === "utility") { return { isValid: true }; } if (!await isUtilityRegistryInitialized()) { return { isValid: false, message: `Cannot use ${registryType} registry without utility registry. Please run: npx buildy-ui init` }; } return { isValid: true }; } async function getUtilityComponents() { const utilityPath = path.join(process.cwd(), "utility"); const allComponents = /* @__PURE__ */ new Set(); for (const category of SCHEMA_CONFIG.componentCategories) { const categoryPath = path.join(utilityPath, category); if (await fs.pathExists(categoryPath)) { try { const files = await fs.readdir(categoryPath); const componentNames = files.filter((file) => file.endsWith(".tsx") || file.endsWith(".ts")).map((file) => path.basename(file, path.extname(file))).filter((name) => name !== "index" && !name.startsWith("_")); componentNames.forEach((name) => allComponents.add(name)); } catch (error) { console.warn(`Warning: Could not read ${categoryPath}:`, error.message); } } } const libPath = path.join(process.cwd(), "lib"); if (await fs.pathExists(libPath)) { try { const files = await fs.readdir(libPath); const libComponents = files.filter((file) => file.endsWith(".tsx") || file.endsWith(".ts")).map((file) => path.basename(file, path.extname(file))).filter((name) => name !== "index" && !name.startsWith("_")); libComponents.forEach((name) => allComponents.add(name)); } catch (error) { console.warn(`Warning: Could not read ${libPath}:`, error.message); } } return Array.from(allComponents); } async function getUtilityComponentsByCategory() { const utilityPath = path.join(process.cwd(), "utility"); const componentsByCategory = {}; for (const category of SCHEMA_CONFIG.componentCategories) { const categoryPath = path.join(utilityPath, category); if (await fs.pathExists(categoryPath)) { try { const files = await fs.readdir(categoryPath); const componentNames = files.filter((file) => file.endsWith(".tsx") || file.endsWith(".ts")).map((file) => path.basename(file, path.extname(file))).filter((name) => name !== "index" && !name.startsWith("_")); if (componentNames.length > 0) { componentsByCategory[category] = componentNames; } } catch (error) { console.warn(`Warning: Could not read ${categoryPath}:`, error.message); } } } const libPath = path.join(process.cwd(), "lib"); if (await fs.pathExists(libPath)) { try { const files = await fs.readdir(libPath); const libComponents = files.filter((file) => file.endsWith(".tsx") || file.endsWith(".ts")).map((file) => path.basename(file, path.extname(file))).filter((name) => name !== "index" && !name.startsWith("_")); if (libComponents.length > 0) { componentsByCategory["lib"] = libComponents; } } catch (error) { console.warn(`Warning: Could not read ${libPath}:`, error.message); } } return componentsByCategory; } async function validateComponentInstallation(components, registryType) { if (registryType === "utility") { return { isValid: true }; } const registryCheck = await canUseRegistry(registryType); if (!registryCheck.isValid) { return registryCheck; } const utilityComponents = await getUtilityComponents(); if (utilityComponents.length === 0) { return { isValid: false, message: `No components found in utility registry. Please install utility components first: npx buildy-ui add --all` }; } const missingComponents = components.filter((comp) => !utilityComponents.includes(comp)); if (missingComponents.length > 0) { return { isValid: false, message: `Components not found in utility registry: ${missingComponents.join(", ")}. Install them first: npx buildy-ui add ${missingComponents.join(" ")}`, missingComponents }; } 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); } async function showUtilityComponentsSummary() { const componentsByCategory = await getUtilityComponentsByCategory(); const totalComponents = Object.values(componentsByCategory).flat().length; if (totalComponents === 0) { console.log(chalk.yellow("\u26A0\uFE0F No utility components found")); console.log("Install utility components first: npx buildy-ui add --all"); return; } console.log(chalk.blue(` \u{1F4E6} Available utility components (${totalComponents} total):`)); Object.entries(componentsByCategory).forEach(([category, components]) => { console.log(` ${chalk.cyan(category)}: ${components.join(", ")}`); }); } // src/utils/project.ts async function isViteProject() { const viteConfigFiles = [ "vite.config.ts", "vite.config.js", "vite.config.mts", "vite.config.mjs" ]; for (const file of viteConfigFiles) { if (await fs2.pathExists(file)) { return true; } } return false; } async function hasReact() { const packageJsonPath = path2.join(process.cwd(), "package.json"); if (!await fs2.pathExists(packageJsonPath)) { return false; } const packageJson = await fs2.readJson(packageJsonPath); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; return "react" in deps; } async function findConfig(registryType) { const utilityConfig = await getConfig("./utility"); if (utilityConfig) { return utilityConfig; } if (registryType && registryType !== "utility") { const registryConfig = await getConfig(`./${registryType}`); if (registryConfig) { return registryConfig; } } return await getConfig(); } async function getConfig(registryPath) { if (registryPath && registryPath !== "./utility") { if (!await isUtilityRegistryInitialized()) { console.error("\u274C Utility registry must be initialized first"); console.log("Run: npx buildy-ui init"); return null; } } const configPath = registryPath ? path2.join(process.cwd(), registryPath, "buildy.config.json") : path2.join(process.cwd(), "buildy.config.json"); if (!await fs2.pathExists(configPath)) { return null; } try { const config = await fs2.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 ? path2.join(process.cwd(), registryPath, "buildy.config.json") : path2.join(process.cwd(), "buildy.config.json"); await fs2.ensureDir(path2.dirname(configPath)); await fs2.writeJson(configPath, config, { spaces: 2 }); } async function ensureDir(dirPath) { await fs2.ensureDir(dirPath); } // src/utils/dependency-checker.ts import fs3 from "fs-extra"; import path3 from "path"; import chalk2 from "chalk"; async function checkProjectDependencies(requiredDeps) { const packageJsonPath = path3.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/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 Please specify at least one component to add.")); console.log("Example: npx buildy-ui@latest add button card"); console.log("Example: npx buildy-ui@latest add button --registry semantic"); console.log("Example: npx buildy-ui@latest add all # Install all components"); console.log("Example: npx buildy-ui@latest add --all --registry semantic # Install all semantic components"); console.log("Example: npx buildy-ui@latest add --retry # Enable retry for unreliable connections"); console.log('Example: npx buildy-ui@latest add "https://example.com/component.json"'); process.exit(1); } const validation = await validateComponentInstallation(components, registryType); if (!validation.isValid) { if (registryType !== "utility") { await showUtilityComponentsSummary(); } handleValidationError(validation); } const config = await findConfig(registryType); if (!config) { console.error(chalk3.red("\u274C buildy is not initialized in this project.")); console.log(`Run 'npx buildy-ui@latest init' first.`); if (registryType !== "utility") { 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} Retry mode enabled - using enhanced connection logic")); } console.log(chalk3.blue(`\u{1F4E6} Installing from ${registryType} registry...`)); const results = []; for (const componentName of components) { const spinner = ora(`Installing ${componentName} from ${registryType}...`).start(); try { const component = await getComponentFn(componentName, registryType); if (!component) { throw new Error(`Component "${componentName}" not found in ${registryType} registry`); } if (options.dryRun) { spinner.succeed(`Would install: ${component.name} from ${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.componentsDir, options.force, registryType); if (component.dependencies.length > 0) { try { await installDependencies(component.dependencies); } catch (error) { console.log(chalk3.yellow(` \u26A0\uFE0F Warning: Could not install some dependencies for ${component.name}`)); console.log(chalk3.yellow(` Dependencies: ${component.dependencies.join(", ")}`)); console.log(chalk3.yellow(` Please install them manually if needed`)); } } spinner.succeed(`Installed ${component.name} from ${registryType}`); results.push({ name: component.name, status: "success" }); } catch (error) { spinner.fail(`Failed to install ${componentName} from ${registryType}`); console.error(chalk3.red(` Error: ${error.message}`)); results.push({ name: componentName, status: "error", error: error.message }); } } 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("\n\u{1F389} Components installed successfully!")); console.log("You can now import and use them in your project."); } if (failed.length > 0) { process.exit(1); } } async function addAllComponents(options, registryType) { console.log(chalk3.blue(`\u{1F680} Installing all available components from ${registryType} registry...`)); if (registryType !== "utility") { const validation = await validateComponentInstallation([], registryType); if (!validation.isValid) { await showUtilityComponentsSummary(); handleValidationError(validation); } } const config = await findConfig(registryType); if (!config) { console.error(chalk3.red("\u274C buildy is not initialized in this project.")); console.log(`Run 'npx buildy-ui@latest init' first.`); if (registryType !== "utility") { 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} Retry mode enabled - using enhanced connection logic")); } const spinner = ora(`Fetching component list from ${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} registry temporarily unavailable`)); console.log("Try these alternatives:"); console.log(" \u2022 Check your internet connection"); console.log(` \u2022 Use --retry flag: npx buildy-ui@latest add --all --registry ${registryType} --retry`); console.log(" \u2022 Use VPN if available"); console.log(" \u2022 Install from URL: npx buildy-ui@latest add 'https://...'"); console.log(" \u2022 Check https://buildy.tw for manual download"); return; } spinner.succeed(`Found ${allComponents.length} components in ${registryType}`); if (options.dryRun) { console.log(chalk3.blue(` \u{1F4CB} Would install from ${registryType}:`)); allComponents.forEach((comp) => { console.log(` - ${comp.name} (${comp.type})`); }); return; } const results = []; for (const component of allComponents) { const componentSpinner = ora(`Installing ${component.name} from ${registryType}...`).start(); try { await installComponentFiles(component, config.componentsDir, options.force, registryType); if (component.dependencies.length > 0) { try { await installDependencies(component.dependencies); } catch (error) { console.log(chalk3.yellow(` \u26A0\uFE0F Warning: Could not install some dependencies for ${component.name}`)); console.log(chalk3.yellow(` Dependencies: ${component.dependencies.join(", ")}`)); console.log(chalk3.yellow(` Please install them manually if needed`)); } } componentSpinner.succeed(`Installed ${component.name} from ${registryType}`); results.push({ name: component.name, status: "success" }); } catch (error) { componentSpinner.fail(`Failed to install ${component.name}`); console.error(chalk3.red(` Error: ${error.message}`)); results.push({ name: component.name, status: "error", error: error.message }); } } 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} All ${registryType} components installed successfully!`)); console.log("You can now import and use them in your project."); } if (failed.length > 0) { process.exit(1); } } catch (error) { spinner.fail(`Failed to fetch components from ${registryType}`); console.error(chalk3.red("\u274C Error:"), error.message); console.log(chalk3.yellow(` \u26A0\uFE0F ${registryType} registry temporarily unavailable`)); console.log("Try these alternatives:"); console.log(" \u2022 Check your internet connection"); console.log(` \u2022 Use --retry flag: npx buildy-ui@latest add --all --registry ${registryType} --retry`); console.log(" \u2022 Use VPN if available"); console.log(" \u2022 Install from URL: npx buildy-ui@latest add 'https://...'"); console.log(" \u2022 Check https://buildy.tw for manual download"); process.exit(1); } } async function installComponentFiles(component, componentsDir, force = false, registryType = "utility") { for (const file of component.files) { const fileName = path4.basename(file.path); const installPath = getInstallPath(registryType, component.type); const targetPath = path4.join(process.cwd(), installPath, fileName); if (!force && await fs4.pathExists(targetPath)) { console.log(` \u26A0\uFE0F Skipped ${fileName} (already exists, use --force to overwrite)`); continue; } await fs4.ensureDir(path4.dirname(targetPath)); await fs4.writeFile(targetPath, file.content, "utf-8"); } } 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("Installing dependencies...").start(); try { const depStatus = await checkProjectDependencies(dependencies); const missingDependencies = await filterMissingDependencies(dependencies); if (missingDependencies.length === 0) { spinner.succeed("All dependencies already available"); 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("Dependencies installed"); } catch (error) { spinner.fail("Failed to install dependencies"); const errorMessage = error.stderr || error.message; if (isWorkspaceError(errorMessage)) { console.log(chalk3.yellow("\n\u{1F4A1} Workspace dependency detected. Installing individually...")); const results = await installDependenciesIndividually(dependencies); if (results.some((r) => r.success)) { console.log(chalk3.green("\u2705 Some dependencies installed successfully")); return; } } throw new Error(`Failed to install dependencies: ${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"; } // src/commands/init.ts import chalk4 from "chalk"; import prompts from "prompts"; import ora2 from "ora"; import path5 from "path"; import fs5 from "fs/promises"; async function initCommand(options) { const registryName = options.registry || SCHEMA_CONFIG.defaultRegistryType; const registryPath = `./${registryName}`; console.log(chalk4.blue(`\u{1F680} Initializing UI8Kit buildy in your project (${registryName} registry)...`)); const validation = await canUseRegistry(registryName); if (!validation.isValid) { handleValidationError(validation); } if (!await isViteProject()) { console.error(chalk4.red("\u274C This doesn't appear to be a Vite project.")); console.log("Please run this command in a Vite project directory."); process.exit(1); } if (!await hasReact()) { console.error(chalk4.red("\u274C React is not installed in this project.")); console.log("Please install React first: npm install react react-dom"); process.exit(1); } const existingConfig = await getConfig(registryPath); if (existingConfig && !options.yes) { const { overwrite } = await prompts({ type: "confirm", name: "overwrite", message: `UI8Kit buildy is already initialized for ${registryName} registry. Overwrite configuration?`, initial: false }); if (!overwrite) { console.log(chalk4.yellow("\u26A0\uFE0F Initialization cancelled.")); return; } } const aliases = { "@": "./src", "@/components": `${registryPath}/components`, "@/ui": `${registryPath}/ui`, "@/blocks": `${registryPath}/blocks`, "@/lib": "./lib", "@/utility": "./utility", "@/semantic": "./semantic", "@/theme": "./theme" }; let config; if (options.yes) { config = { $schema: "https://buildy.tw/schema.json", framework: "vite-react", typescript: true, aliases, registry: "@ui8kit", componentsDir: `${registryPath}/ui`, libDir: "./lib" }; } else { const responses = await prompts([ { type: "confirm", name: "typescript", message: "Are you using TypeScript?", initial: true } ]); config = { $schema: "https://buildy.tw/schema.json", framework: "vite-react", typescript: responses.typescript, aliases, registry: "@ui8kit", componentsDir: `${registryPath}/ui`, libDir: "./lib" }; } const spinner = ora2(`Setting up UI8Kit ${registryName} structure...`).start(); try { await saveConfig(config, registryPath); await ensureDir(config.libDir); await ensureDir(config.componentsDir); await ensureDir(`${registryPath}/components`); await ensureDir(`${registryPath}/blocks`); if (registryName === "utility") { await createUtilsFile(config.libDir, config.typescript); } spinner.succeed(`UI8Kit ${registryName} structure initialized successfully!`); console.log(chalk4.green(` \u2705 UI8Kit ${registryName} Setup complete!`)); console.log("\nDirectories created:"); if (registryName === "utility") { console.log(` ${chalk4.cyan("lib/")} - Utils, helpers, functions`); } console.log(` ${chalk4.cyan(`${registryPath}/ui/`)} - UI components`); console.log(` ${chalk4.cyan(`${registryPath}/components/`)} - Complex components`); console.log(` ${chalk4.cyan(`${registryPath}/blocks/`)} - Component blocks`); console.log("\nNext steps:"); console.log(` ${chalk4.cyan(`npx buildy-ui@latest add button --registry ${registryName}`)} - Add a button component`); console.log(` ${chalk4.cyan(`npx buildy-ui@latest add card input --registry ${registryName}`)} - Add multiple components`); console.log(` ${chalk4.cyan(`npx buildy-ui@latest add --all --registry ${registryName}`)} - Add all components`); console.log(` ${chalk4.cyan('npx buildy-ui@latest add "https://example.com/component.json"')} - Add from external URL`); } catch (error) { spinner.fail(`Failed to initialize UI8Kit ${registryName}`); console.error(chalk4.red("\u274C Error:"), error.message); process.exit(1); } } 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 = path5.join(process.cwd(), libDir, fileName); await fs5.writeFile(filePath, utilsContent, "utf-8"); } // src/commands/build.ts import fs6 from "fs-extra"; import path6 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:template" ]); var registryItemFileSchema = z2.object({ path: z2.string(), content: z2.string().optional(), // Populated during build target: z2.string().optional() }); var registryItemSchema = z2.object({ $schema: z2.string().optional(), name: z2.string(), type: registryItemTypeSchema, description: z2.string().optional(), dependencies: z2.array(z2.string()).default([]), devDependencies: z2.array(z2.string()).default([]), files: z2.array(registryItemFileSchema) }); 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 baseSchema = zodToJsonSchema(fullRegistrySchema, { name: "BuildyRegistry", $refStrategy: "none" }); const actualSchema = baseSchema.definitions?.BuildyRegistry || baseSchema; return { "$schema": SCHEMA_CONFIG.schemaVersion, "title": SCHEMA_CONFIG.descriptions.registry.title, "description": SCHEMA_CONFIG.descriptions.registry.description, "type": "object", "properties": { "$schema": { "type": "string", "description": SCHEMA_CONFIG.fieldDescriptions.schema }, "name": { "type": "string", "description": SCHEMA_CONFIG.fieldDescriptions.registryName }, "homepage": { "type": "string", "description": SCHEMA_CONFIG.fieldDescriptions.registryHomepage }, "registry": { "type": "string", "enum": SCHEMA_CONFIG.registryTypes, "description": SCHEMA_CONFIG.fieldDescriptions.registryType }, "version": { "type": "string", "description": SCHEMA_CONFIG.fieldDescriptions.registryVersion }, "lastUpdated": { "type": "string", "format": "date-time", "description": SCHEMA_CONFIG.fieldDescriptions.lastUpdated }, "categories": { "type": "array", "items": { "type": "string", "enum": SCHEMA_CONFIG.componentCategories }, "description": SCHEMA_CONFIG.fieldDescriptions.categories }, "components": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "type": { "type": "string", "enum": actualSchema.properties?.components?.items?.properties?.type?.enum || [ "registry:lib", "registry:block", "registry:component", "registry:ui", "registry:template" ] }, "description": { "type": "string" } }, "required": ["name", "type"], "additionalProperties": false }, "description": SCHEMA_CONFIG.fieldDescriptions.components }, "items": { "type": "array", "items": { "$ref": getSchemaRef("registry-item") }, "description": SCHEMA_CONFIG.fieldDescriptions.items } }, "required": ["items"], "additionalProperties": false }; } function generateRegistryItemSchema() { const baseSchema = zodToJsonSchema(registryItemSchema, { name: "BuildyRegistryItem", $refStrategy: "none" }); const actualSchema = baseSchema.definitions?.BuildyRegistryItem || baseSchema; return { "$schema": SCHEMA_CONFIG.schemaVersion, "title": SCHEMA_CONFIG.descriptions.registryItem.title, "description": SCHEMA_CONFIG.descriptions.registryItem.description, "type": "object", "properties": actualSchema.properties, "required": actualSchema.required || ["name", "type", "files"], "additionalProperties": false }; } // src/commands/build.ts async function buildCommand(registryPath = "./utility/registry.json", options = {}) { const buildOptions = { cwd: path6.resolve(options.cwd || process.cwd()), registryFile: path6.resolve(registryPath), outputDir: path6.resolve(options.output || "./packages/registry/r/utility") }; console.log(chalk5.blue("\u{1F528} Building utility registry...")); try { const registryContent = await fs6.readFile(buildOptions.registryFile, "utf-8"); const registryData = JSON.parse(registryContent); const registry = registrySchema.parse(registryData); await fs6.ensureDir(buildOptions.outputDir); await generateSchemaFiles(buildOptions.outputDir); const spinner = ora3("Processing utility components...").start(); for (const item of registry.items) { spinner.text = `Building ${item.name}...`; item.$schema = "https://buildy.tw/schema/registry-item.json"; for (const file of item.files) { const filePath = path6.resolve(buildOptions.cwd, file.path); if (await fs6.pathExists(filePath)) { file.content = await fs6.readFile(filePath, "utf-8"); } else { throw new Error(`File not found: ${file.path}`); } } const validatedItem = registryItemSchema.parse(item); const typeDir = getOutputDir(validatedItem.type); const outputPath = path6.join(buildOptions.outputDir, typeDir); await fs6.ensureDir(outputPath); const outputFile = path6.join(outputPath, `${validatedItem.name}.json`); await fs6.writeFile(outputFile, JSON.stringify(validatedItem, null, 2)); } spinner.succeed(`Built ${registry.items.length} utility components`); await createIndexFile(registry, buildOptions.outputDir); console.log(chalk5.green("\u2705 Utility registry built successfully!")); console.log(`Output: ${buildOptions.outputDir}`); console.log(chalk5.green("\u2705 Schema files generated successfully!")); } catch (error) { console.error(chalk5.red("\u274C Build failed:"), error.message