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
JavaScript
#!/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