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
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 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