bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
1,127 lines • 71.8 kB
JavaScript
#!/usr/bin/env node
import { execSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import chalk from "chalk";
import { Command } from "commander";
import prompts from "prompts";
// import { execSync } from 'child_process';
// ES module __dirname equivalent
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const program = new Command();
// GitHub raw content base URL
const GITHUB_RAW_BASE = "https://raw.githubusercontent.com/b-open-io/bigblocks/main";
// Load registry from GitHub, blockchain, or local file
async function loadRegistry() {
let registry;
try {
const response = await fetch(`${GITHUB_RAW_BASE}/registry/registry.json`);
if (!response.ok) {
if (response.status === 404) {
throw new Error("Registry not found - ensure you are using the correct repository");
}
if (response.status >= 500) {
throw new Error("GitHub server error - please try again later");
}
throw new Error(`Failed to fetch registry (HTTP ${response.status})`);
}
registry = await response.json();
}
catch (error) {
const err = error;
// Fallback to local registry for development
if (err.code === "ENOTFOUND") {
console.log(chalk.yellow("⚠ No internet connection - using local registry"));
}
else if (err.message.includes("not found")) {
console.log(chalk.yellow("⚠ Remote registry not available - using local registry"));
console.log(chalk.gray(" The BigBlocks repository may not be published yet"));
}
else {
console.log(chalk.yellow("⚠ Using local registry (development mode)"));
console.log(chalk.gray(` ${err.message}`));
}
// Try multiple paths for the registry
const possiblePaths = [
join(__dirname, "..", "registry", "registry.json"), // Built package
join(__dirname, "..", "..", "registry", "registry.json"), // Development
join(process.cwd(), "node_modules", "bigblocks", "registry", "registry.json"), // Local install
join(__dirname, "..", "..", "..", "registry", "registry.json"), // Global install
];
let registryFound = false;
for (const localPath of possiblePaths) {
try {
if (existsSync(localPath)) {
registry = JSON.parse(readFileSync(localPath, "utf-8"));
registryFound = true;
console.log(chalk.gray(` Using local registry from: ${localPath}`));
break;
}
}
catch {
// Continue to next path
}
}
if (!registryFound || !registry) {
console.error(chalk.red("✗ Failed to load local registry"));
console.error(chalk.gray(" Could not find registry.json in any expected location"));
console.error(chalk.gray(" Try reinstalling: npm install -g bigblocks@latest"));
throw new Error("No registry available");
}
}
// At this point, registry should be defined
if (!registry) {
throw new Error("Registry not loaded");
}
// Optional: enhance with blockchain registry if WIF is present
if (process.env.BIGBLOCKS_WIF) {
try {
const blockchainRegistry = await loadBlockchainRegistry();
if (blockchainRegistry?.items?.length > 0) {
console.log(chalk.blue("🔗 Enhanced with blockchain registry"));
// Merge blockchain components with existing registry
registry.items = [...registry.items, ...blockchainRegistry.items];
}
}
catch {
console.log(chalk.yellow("⚠ Blockchain registry unavailable"));
}
}
return registry;
}
// Load components from BSV blockchain
async function loadBlockchainRegistry() {
const wif = process.env.BIGBLOCKS_WIF;
if (!wif) {
throw new Error("No WIF found in environment");
}
// For now, return empty registry - implement BSV indexer query later
// This would query MAP protocol for components with:
// { "app": "bigblocks", "type": "component" }
console.log(chalk.gray(" Blockchain registry feature available but not implemented yet"));
return { items: [] };
// TODO: Implement BSV indexer query
// const components = await queryBSVIndexer({
// app: 'bigblocks',
// type: 'component'
// });
// return { components };
}
// Show latest changes from CHANGELOG.md
async function showLatestChanges() {
try {
const changelogPath = join(__dirname, "..", "CHANGELOG.md");
if (!existsSync(changelogPath)) {
return; // Skip if no changelog
}
const changelog = readFileSync(changelogPath, "utf-8");
// Extract version from package.json or registry
const packagePath = join(__dirname, "..", "package.json");
let currentVersion = "0.0.13"; // fallback
if (existsSync(packagePath)) {
try {
const pkg = JSON.parse(readFileSync(packagePath, "utf-8"));
currentVersion = pkg.version;
}
catch {
// Use fallback version
}
}
// Find the unreleased section or current version section
const unreleasedMatch = changelog.match(/## \[Unreleased\] - (.+?)\n\n### Added\n([\s\S]*?)(?=\n## \[|$)/);
const versionMatch = changelog.match(new RegExp(`## \\[${currentVersion}\\].*?\\n\\n### Added\\n([\\s\\S]*?)(?=\\n## \\[|$)`));
const match = unreleasedMatch || versionMatch;
if (match) {
const version = unreleasedMatch
? `v${unreleasedMatch[1]}`
: `v${currentVersion}`;
const addedSection = match[unreleasedMatch ? 2 : 1];
if (addedSection) {
console.log(chalk.bold(`🚀 New in ${version}:`));
// Extract first few bullet points
const bullets = addedSection
.split("\n")
.filter((line) => line.trim().startsWith("- "))
.slice(0, 4)
.map((line) => line.replace(/^- \*\*(.+?)\*\* -/, "• $1:"))
.map((line) => line.replace(/^- (.+)/, "• $1"));
for (const bullet of bullets) {
console.log(chalk.gray(` ${bullet}`));
}
if (addedSection
.split("\n")
.filter((line) => line.trim().startsWith("- ")).length > 4) {
console.log(chalk.gray(" • And more..."));
}
}
}
}
catch (_error) {
// Silently fail if changelog parsing fails
}
}
// Validate component structure
async function validateComponent(component) {
const errors = [];
const warnings = [];
// Check if component files exist
for (const file of component.files) {
const filePath = typeof file === "string" ? file : file.path;
const fullPath = join(__dirname, "..", filePath);
if (!existsSync(fullPath)) {
errors.push(`File not found: ${filePath}`);
}
}
// Check registry dependencies exist
const registry = await loadRegistry();
const componentNames = new Set(registry.items.map((c) => c.name));
for (const dep of component.registryDependencies) {
if (!componentNames.has(dep)) {
errors.push(`Registry dependency not found: ${dep}`);
}
}
// Check for naming conventions
if (!component.name.match(/^[a-z][a-z0-9-]*[a-z0-9]$/)) {
warnings.push("Component name should use kebab-case (lowercase with hyphens)");
}
// Check description quality
if (component.description && component.description.length < 20) {
warnings.push("Description should be more descriptive (at least 20 characters)");
}
if (component.description === "Bitcoin UI component") {
warnings.push("Using generic description - consider making it more specific");
}
return { errors, warnings };
}
// Analyze component dependencies
function analyzeComponentDependencies(registry) {
const components = new Map(registry.items.map((c) => [c.name, c]));
const dependents = new Map();
const externalDeps = new Map();
// Build dependency graph
for (const component of registry.items) {
for (const dep of component.registryDependencies) {
if (!dependents.has(dep)) {
dependents.set(dep, []);
}
dependents.get(dep)?.push(component.name);
}
for (const dep of component.dependencies) {
externalDeps.set(dep, (externalDeps.get(dep) || 0) + 1);
}
}
// Find circular dependencies
const circularDeps = [];
const visited = new Set();
const recursionStack = new Set();
function findCycles(componentName, path) {
if (recursionStack.has(componentName)) {
const cycleStart = path.indexOf(componentName);
if (cycleStart >= 0) {
circularDeps.push([...path.slice(cycleStart), componentName]);
}
return;
}
if (visited.has(componentName))
return;
visited.add(componentName);
recursionStack.add(componentName);
const component = components.get(componentName);
if (component) {
for (const dep of component.registryDependencies) {
findCycles(dep, [...path, componentName]);
}
}
recursionStack.delete(componentName);
}
for (const componentName of components.keys()) {
findCycles(componentName, []);
}
// Find orphaned components
const orphans = Array.from(components.keys()).filter((name) => !dependents.has(name));
// Most depended on components
const popularComponents = Array.from(dependents.entries())
.map(([name, deps]) => [name, deps.length])
.sort(([, a], [, b]) => b - a);
// Sort external dependencies
const sortedExternalDeps = Array.from(externalDeps.entries()).sort(([, a], [, b]) => b - a);
return {
circularDeps,
orphans,
popularComponents,
externalDeps: sortedExternalDeps,
};
}
// Verify component exports in index.ts
function verifyExports(registry, indexContent) {
const componentNames = registry.items.map((c) => c.name);
const missing = [];
const extra = [];
// Extract exports from index.ts
const exportMatches = indexContent.matchAll(/export\s+\{[^}]*\}\s+from\s+['"][^'"]*['"];?/g);
const exportedNames = new Set();
for (const match of exportMatches) {
const exportBlock = match[0];
const names = exportBlock.match(/\{([^}]*)\}/)?.[1];
if (names) {
const exportNames = names
.split(",")
.map((name) => name.trim().replace(/\s+as\s+.+/, ""))
.filter((name) => name.length > 0);
for (const name of exportNames) {
exportedNames.add(name);
}
}
}
// Find missing exports (components in registry but not exported)
for (const componentName of componentNames) {
// Convert kebab-case to PascalCase for comparison
const pascalName = componentName
.split("-")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join("");
if (!exportedNames.has(pascalName)) {
missing.push(pascalName);
}
}
return { missing, extra };
}
// Fetch component file content from GitHub
async function fetchComponentFile(filePath) {
try {
const response = await fetch(`${GITHUB_RAW_BASE}/${filePath}`);
if (!response.ok) {
throw new Error(`Failed to fetch ${filePath}`);
}
return await response.text();
}
catch (error) {
// For development, try to read local file
const localPath = join(__dirname, "..", "..", filePath);
if (existsSync(localPath)) {
return readFileSync(localPath, "utf-8");
}
throw error;
}
}
// Component counting and registry management functions
const _COMPONENT_PATTERNS = [
/export\s+{\s*([A-Z][A-Za-z0-9]*)/g,
/export\s+function\s+([A-Z][A-Za-z0-9]*)/g,
/export\s+const\s+([A-Z][A-Za-z0-9]*)\s*=/g,
/export\s+default\s+function\s+([A-Z][A-Za-z0-9]*)/g,
];
const categoryDisplayMap = {
"component:auth": { emoji: "🔐", name: "Authentication" },
"component:backup": { emoji: "💾", name: "Backup/Recovery" },
"component:developer": { emoji: "🛠️", name: "Developer Tools" },
"component:example": { emoji: "📦", name: "Examples" },
"component:identity": { emoji: "🔑", name: "Identity" },
"component:integration": { emoji: "🔗", name: "Integrations" },
"component:layout": { emoji: "📐", name: "Layouts" },
"component:market": { emoji: "💰", name: "Market" },
"component:profile": { emoji: "👤", name: "Profile Management" },
"component:provider": { emoji: "🔌", name: "Providers" },
"component:social": { emoji: "💬", name: "Social" },
"component:ui": { emoji: "🎨", name: "UI Components" },
"component:wallet": { emoji: "👛", name: "Wallet" },
"component:inscription": { emoji: "✍️", name: "Inscriptions" },
};
async function countFromRegistry(exportData) {
const registry = await loadRegistry();
const components = registry.items || [];
console.log(`📁 Found ${components.length} components in registry\n`);
const componentsByType = {};
let totalComponents = 0;
for (const component of components) {
const type = component.type || "component:other";
if (!componentsByType[type]) {
componentsByType[type] = [];
}
componentsByType[type].push(component);
totalComponents++;
}
const sortedCategories = Object.entries(componentsByType).sort(([, a], [, b]) => b.length - a.length);
console.log("📊 Registry Component Analysis Results:\n");
console.log("=".repeat(60));
console.log(`🎯 TOTAL REGISTRY COMPONENTS: ${totalComponents}`);
console.log("=".repeat(60));
let _readmeSummary = "";
let actualLibraryCount = 0;
for (const [type, components] of sortedCategories) {
const categoryInfo = categoryDisplayMap[type] || {
emoji: "📦",
name: type,
};
const count = components.length;
if (type !== "component:example") {
actualLibraryCount += count;
}
console.log(`\n${categoryInfo.emoji} ${categoryInfo.name} (${count} components)`);
console.log("-".repeat(40));
for (const comp of components) {
const tags = comp.categories ? ` [${comp.categories.join(", ")}]` : "";
console.log(` 📄 ${comp.name}${tags}`);
}
if (type !== "component:example") {
_readmeSummary += `### ${categoryInfo.emoji} ${categoryInfo.name} (${count} components)\n`;
for (const comp of components) {
_readmeSummary += `- \`${comp.name}\` - ${comp.description || "Component description"}\n`;
}
_readmeSummary += "\n";
}
}
console.log(`\n${"=".repeat(60)}`);
console.log(`🎯 LIBRARY COMPONENTS: ${actualLibraryCount} (excluding examples)`);
console.log("=".repeat(60));
if (exportData) {
const summary = {
total: actualLibraryCount,
totalWithExamples: totalComponents,
categories: Object.fromEntries(sortedCategories
.filter(([type]) => type !== "component:example")
.map(([type, comps]) => [
categoryDisplayMap[type]?.name || type,
{
count: comps.length,
components: comps.map((c) => ({
name: c.name,
description: c.description,
categories: c.categories || [],
})),
},
])),
lastUpdated: new Date().toISOString(),
};
const outputPath = join(__dirname, "..", "generated", "COMPONENT_COUNT.json");
writeFileSync(outputPath, JSON.stringify(summary, null, 2));
console.log("\n✅ Component count saved to generated/COMPONENT_COUNT.json");
}
}
async function countFromFilesystem(exportData) {
console.log("🔍 Scanning filesystem for components...\n");
// Implementation would go here - for now just delegate to registry
console.log(chalk.yellow("⚠ Filesystem scanning temporarily disabled - using registry"));
await countFromRegistry(exportData);
}
async function generateRegistryFromFiles() {
console.log("📝 This command will scan filesystem and generate complete registry");
console.log(chalk.yellow("⚠ Not implemented yet - use existing registry generation"));
}
async function validateRegistryCompleteness() {
console.log("✅ Checking registry against filesystem...");
console.log(chalk.yellow("⚠ Not implemented yet - shows current registry state"));
await countFromRegistry();
}
async function populateRegistryContent() {
console.log("📦 Populating registry content from filesystem...\n");
try {
const registryPath = join(__dirname, "..", "registry", "registry.json");
const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
let updatedComponents = 0;
let emptyComponents = 0;
console.log(`Processing ${registry.items.length} components...\n`);
// Update existing components with content
for (const component of registry.items) {
for (const file of component.files) {
if (typeof file === "object" &&
(!file.content || file.content === "")) {
try {
const fullPath = join(__dirname, "..", file.path);
if (existsSync(fullPath)) {
file.content = readFileSync(fullPath, "utf-8");
updatedComponents++;
console.log(chalk.green(`✓ ${component.name}`));
}
else {
emptyComponents++;
console.log(chalk.yellow(`⚠ ${component.name} - File not found: ${file.path}`));
}
}
catch (error) {
emptyComponents++;
console.warn(chalk.red(`✗ ${component.name} - Error reading ${file.path}:`), error);
}
}
}
}
// Write updated registry
writeFileSync(registryPath, JSON.stringify(registry, null, 2));
console.log("\n🎉 Registry content population complete!");
console.log(chalk.green(`✅ Updated ${updatedComponents} components with content`));
if (emptyComponents > 0) {
console.log(chalk.yellow(`⚠ ${emptyComponents} components could not be populated`));
}
console.log("📊 Registry now ready for component installation\n");
}
catch (error) {
console.error(chalk.red("✗ Failed to populate registry content:"), error);
}
}
async function syncMissingToRegistry() {
console.log("🔄 Syncing missing components to registry...\n");
try {
// First, find missing components
const registryPath = join(__dirname, "..", "registry", "registry.json");
const registry = JSON.parse(readFileSync(registryPath, "utf-8"));
const registryComponents = new Set(registry.items.map((c) => c.name.toLowerCase()));
const componentFiles = execSync('find src/components -name "*.tsx" -not -path "*/stories/*" -not -path "*/test/*" -not -name "*.stories.tsx" -not -name "*.test.tsx" -not -name "index.tsx"', { encoding: "utf-8" })
.trim()
.split("\n")
.filter(Boolean);
const missing = [];
for (const filePath of componentFiles) {
const fileName = filePath.split("/").pop();
if (!fileName)
continue;
const componentName = fileName.replace(".tsx", "").toLowerCase();
if (!registryComponents.has(componentName)) {
const pathParts = filePath.split("/");
let category = "component:other";
if (pathParts.includes("authentication"))
category = "component:auth";
else if (pathParts.includes("backup-recovery"))
category = "component:backup";
else if (pathParts.includes("bap-identity"))
category = "component:identity";
else if (pathParts.includes("developer-tools"))
category = "component:developer";
else if (pathParts.includes("droplit"))
category = "component:integration";
else if (pathParts.includes("examples"))
category = "component:example";
else if (pathParts.includes("inscriptions"))
category = "component:inscription";
else if (pathParts.includes("layouts"))
category = "component:layout";
else if (pathParts.includes("market"))
category = "component:market";
else if (pathParts.includes("metalens"))
category = "component:integration";
else if (pathParts.includes("profile-management"))
category = "component:profile";
else if (pathParts.includes("providers"))
category = "component:provider";
else if (pathParts.includes("social"))
category = "component:social";
else if (pathParts.includes("ui-components"))
category = "component:ui";
else if (pathParts.includes("wallet-integrations"))
category = "component:integration";
else if (pathParts.includes("wallet"))
category = "component:wallet";
missing.push({ file: filePath, componentName, category });
}
}
if (missing.length === 0) {
console.log(chalk.green("🎉 Registry is already complete!"));
return;
}
console.log(`📝 Adding ${missing.length} missing components to registry...\n`);
// Generate registry entries for missing components
const newComponents = [];
for (const item of missing) {
// Generate basic categories array based on component type
const categories = [];
const categoryName = categoryDisplayMap[item.category]?.name.toLowerCase() || "other";
categories.push(categoryName);
// Add specific categories based on component name
if (item.componentName.includes("button"))
categories.push("button");
if (item.componentName.includes("form"))
categories.push("form");
if (item.componentName.includes("modal"))
categories.push("modal");
if (item.componentName.includes("display"))
categories.push("display");
if (item.componentName.includes("editor"))
categories.push("editor");
if (item.componentName.includes("manager"))
categories.push("management");
if (item.componentName.includes("provider"))
categories.push("provider");
// Read actual component content
let content = "";
try {
const fullPath = join(__dirname, "..", item.file);
if (existsSync(fullPath)) {
content = readFileSync(fullPath, "utf-8");
}
}
catch (error) {
console.warn(`Warning: Could not read content for ${item.file}:`, error);
}
const newComponent = {
name: item.componentName,
type: item.category,
categories,
files: [
{
path: item.file,
content,
type: "registry:component",
target: "",
},
],
dependencies: [],
devDependencies: [],
registryDependencies: [],
description: `${item.componentName.charAt(0).toUpperCase() + item.componentName.slice(1)} component for Bitcoin applications`,
};
newComponents.push(newComponent);
}
// Add new components to registry
registry.items = [...registry.items, ...newComponents];
// Sort components by name for consistency
registry.items.sort((a, b) => a.name.localeCompare(b.name));
// Write updated registry
writeFileSync(registryPath, JSON.stringify(registry, null, 2));
console.log(chalk.green(`✅ Successfully added ${missing.length} components to registry!`));
console.log(`📊 Registry now contains ${registry.items.length} total components\n`);
// Show summary by category
const addedByCategory = {};
for (const item of missing) {
addedByCategory[item.category] =
(addedByCategory[item.category] || 0) + 1;
}
console.log("📈 Components added by category:");
for (const [category, count] of Object.entries(addedByCategory)) {
const categoryInfo = categoryDisplayMap[category] || {
emoji: "📦",
name: category,
};
console.log(` ${categoryInfo.emoji} ${categoryInfo.name}: +${count}`);
}
}
catch (error) {
console.error(chalk.red("✗ Failed to sync components to registry:"), error);
}
}
async function showMissingComponents() {
console.log("🔍 Finding components not in registry...\n");
try {
const registry = await loadRegistry();
const registryComponents = new Set(registry.items.map((c) => c.name.toLowerCase()));
const componentFiles = execSync('find src/components -name "*.tsx" -not -path "*/stories/*" -not -path "*/test/*" -not -name "*.stories.tsx" -not -name "*.test.tsx" -not -name "index.tsx"', { encoding: "utf-8" })
.trim()
.split("\n")
.filter(Boolean);
console.log(`📁 Found ${componentFiles.length} component files`);
console.log(`📋 Registry has ${registry.items.length} components\n`);
const missing = [];
for (const filePath of componentFiles) {
const fileName = filePath.split("/").pop();
if (!fileName)
continue;
const componentName = fileName.replace(".tsx", "").toLowerCase();
if (!registryComponents.has(componentName)) {
const pathParts = filePath.split("/");
let category = "component:other";
if (pathParts.includes("authentication"))
category = "component:auth";
else if (pathParts.includes("backup-recovery"))
category = "component:backup";
else if (pathParts.includes("bap-identity"))
category = "component:identity";
else if (pathParts.includes("developer-tools"))
category = "component:developer";
else if (pathParts.includes("droplit"))
category = "component:integration";
else if (pathParts.includes("examples"))
category = "component:example";
else if (pathParts.includes("inscriptions"))
category = "component:inscription";
else if (pathParts.includes("layouts"))
category = "component:layout";
else if (pathParts.includes("market"))
category = "component:market";
else if (pathParts.includes("metalens"))
category = "component:integration";
else if (pathParts.includes("profile-management"))
category = "component:profile";
else if (pathParts.includes("providers"))
category = "component:provider";
else if (pathParts.includes("social"))
category = "component:social";
else if (pathParts.includes("ui-components"))
category = "component:ui";
else if (pathParts.includes("wallet-integrations"))
category = "component:integration";
else if (pathParts.includes("wallet"))
category = "component:wallet";
missing.push({ file: filePath, componentName, category });
}
}
if (missing.length === 0) {
console.log(chalk.green("🎉 All components are in the registry!"));
return;
}
console.log(chalk.red(`❌ Found ${missing.length} missing components:\n`));
const missingByCategory = {};
for (const item of missing) {
if (!missingByCategory[item.category]) {
missingByCategory[item.category] = [];
}
missingByCategory[item.category]?.push(item);
}
for (const [category, items] of Object.entries(missingByCategory)) {
const categoryInfo = categoryDisplayMap[category] || {
emoji: "📦",
name: category,
};
console.log(`${categoryInfo.emoji} ${categoryInfo.name} (${items.length} missing)`);
console.log("-".repeat(50));
for (const item of items) {
console.log(` 📄 ${item.componentName} - ${item.file}`);
}
console.log("");
}
console.log(chalk.yellow("💡 Use `bigblocks registry --sync` to add these to the registry"));
}
catch (error) {
console.error(chalk.red("✗ Failed to find missing components:"), error);
}
}
program
.name("bigblocks")
.description("Production-ready Bitcoin UI components for React applications. Built with TypeScript and Radix Themes.")
.version("0.0.13");
program
.command("init")
.description("Initialize BigBlocks UI in your project")
.option("-d, --dir <path>", "Components directory", "./components/bigblocks")
.option("--typescript", "Use TypeScript", true)
.option("--no-typescript", "Use JavaScript")
.action(async (options) => {
console.log("");
console.log(chalk.bold.cyan(" Welcome to BigBlocks"));
console.log(chalk.gray(" Production-ready Bitcoin UI components\n"));
// Check if we're in a Next.js/React project
if (!existsSync("package.json")) {
console.log(chalk.red("✕ No package.json found"));
console.log(chalk.gray(" Please run this command in your project root directory\n"));
process.exit(1);
}
const packageJson = JSON.parse(readFileSync("package.json", "utf-8"));
const hasReact = packageJson.dependencies?.react || packageJson.devDependencies?.react;
if (!hasReact) {
console.log(chalk.yellow("⚠ React not detected in your project"));
console.log(chalk.gray(" BigBlocks requires React 18+ to function properly\n"));
const { proceed } = await prompts({
type: "confirm",
name: "proceed",
message: "Continue setup anyway?",
initial: false,
});
if (!proceed) {
console.log(chalk.gray("\n Setup cancelled. Install React first, then run this command again.\n"));
process.exit(0);
}
}
// Check if shadcn/ui is already installed
const componentsJsonPath = join(process.cwd(), "components.json");
const hasShadcnConfig = existsSync(componentsJsonPath);
if (hasShadcnConfig) {
console.log(chalk.green("✓ Detected existing shadcn/ui installation"));
}
// Ask for configuration
const response = await prompts([
{
type: "text",
name: "componentsDir",
message: "Where would you like to install components?",
initial: options.dir,
},
{
type: "confirm",
name: "typescript",
message: "Using TypeScript?",
initial: options.typescript,
},
{
type: hasShadcnConfig ? null : "confirm",
name: "installShadcn",
message: "Would you like to initialize shadcn/ui? (required for BigBlocks)",
initial: true,
},
{
type: "select",
name: "theme",
message: hasShadcnConfig
? "Would you like to add a BigBlocks theme? (optional)"
: "Choose a theme for your BigBlocks components",
initial: 0,
choices: [
{
title: hasShadcnConfig
? "Skip - Keep my existing theme"
: "Default (inherit from your project)",
value: "default",
},
{ title: "Core Colors", value: "heading", disabled: true },
{ title: " Purple", value: "purple" },
{ title: " Blue", value: "blue" },
{ title: " Green", value: "green" },
{ title: " Orange", value: "orange" },
{ title: "Sophisticated", value: "heading", disabled: true },
{ title: " Neo (Futuristic)", value: "neo" },
{ title: " Cyberpunk", value: "cyberpunk" },
{ title: " Noir", value: "noir" },
{ title: "Nature", value: "heading", disabled: true },
{ title: " Forest", value: "forest" },
{ title: " Ocean", value: "ocean" },
{ title: " Sunset", value: "sunset" },
],
},
]);
// Initialize shadcn/ui if needed
if (!hasShadcnConfig && response.installShadcn) {
console.log(`\n${chalk.cyan("Initializing shadcn/ui...")}`);
try {
execSync("npx shadcn@latest init -y", { stdio: "inherit" });
console.log(chalk.green("✓ shadcn/ui initialized successfully"));
}
catch (error) {
console.error(chalk.red("✗ Failed to initialize shadcn/ui"));
console.log(chalk.gray(" Please run: npx shadcn@latest init"));
process.exit(1);
}
}
else if (!hasShadcnConfig && !response.installShadcn) {
console.log(chalk.yellow("\n⚠ BigBlocks requires shadcn/ui to be installed"));
console.log(chalk.gray(" Please run: npx shadcn@latest init"));
process.exit(1);
}
// Create components directory
const componentsDir = join(process.cwd(), response.componentsDir);
if (!existsSync(componentsDir)) {
mkdirSync(componentsDir, { recursive: true });
}
// Create hooks directory
const hooksDir = join(componentsDir, "hooks");
if (!existsSync(hooksDir)) {
mkdirSync(hooksDir, { recursive: true });
}
// Create utils directory
const utilsDir = join(componentsDir, "utils");
if (!existsSync(utilsDir)) {
mkdirSync(utilsDir, { recursive: true });
}
// Create config file
const config = {
componentsDir: response.componentsDir,
typescript: response.typescript,
shadcnInstalled: hasShadcnConfig || response.installShadcn,
theme: response.theme,
initialized: true,
};
writeFileSync(join(process.cwd(), "bigblocks.config.json"), JSON.stringify(config, null, 2));
console.log("");
console.log(`${chalk.green("✓")} Configuration created`);
console.log(`${chalk.green("✓")} Components directory ready`);
console.log(chalk.gray(` ${componentsDir}`));
// Add theme import to CSS if a theme was selected
if (response.theme && response.theme !== "default") {
console.log(`\n${chalk.cyan("Adding theme to your CSS...")}`);
const cssFiles = [
"app/globals.css",
"src/app/globals.css",
"styles/globals.css",
"src/styles/globals.css",
"src/index.css",
"index.css",
];
let cssFileFound = false;
for (const cssFile of cssFiles) {
const cssPath = join(process.cwd(), cssFile);
if (existsSync(cssPath)) {
const cssContent = readFileSync(cssPath, "utf-8");
const themeImport = `@import "bigblocks/themes/${response.theme}.css";`;
// Check if there are existing CSS variables
const hasExistingTheme = cssContent.includes("--primary:") ||
cssContent.includes("--background:") ||
cssContent.includes("@layer base");
if (!cssContent.includes(themeImport)) {
let updatedCss;
if (hasExistingTheme) {
// Add after the last import or at the end
const importRegex = /@import[^;]+;/g;
const imports = cssContent.match(importRegex) || [];
if (imports.length > 0) {
// Find the position after the last import
const lastImport = imports[imports.length - 1];
const lastImportIndex = cssContent.lastIndexOf(lastImport) + lastImport.length;
updatedCss = `${cssContent.slice(0, lastImportIndex)}\n\n/* BigBlocks theme - Comment out to use your original theme */\n${themeImport}${cssContent.slice(lastImportIndex)}`;
}
else {
// Add at the end of file
updatedCss = `${cssContent}\n\n/* BigBlocks theme - Comment out to use your original theme */\n${themeImport}\n`;
}
console.log(chalk.yellow(`⚠ Detected existing theme in ${cssFile}`));
console.log(chalk.gray(" BigBlocks theme added but commented for safety"));
console.log(chalk.gray(" To use it, uncomment the import or remove existing theme variables"));
}
else {
// No existing theme, add at the beginning
updatedCss = `${themeImport}\n${cssContent}`;
console.log(chalk.green(`✓ Added ${response.theme} theme to ${cssFile}`));
}
writeFileSync(cssPath, updatedCss);
}
else {
console.log(chalk.gray(` Theme already imported in ${cssFile}`));
}
cssFileFound = true;
break;
}
}
if (!cssFileFound) {
console.log(chalk.yellow("⚠ Could not find CSS file to add theme import"));
console.log(chalk.gray(` Add this to your CSS file: ${chalk.cyan(`@import "bigblocks/themes/${response.theme}.css";`)}`));
}
}
// Show next steps
console.log("");
console.log(chalk.bold("Next steps:"));
console.log("");
console.log("1. Install dependencies");
const deps = ["@bsv/sdk", "bitcoin-auth", "bitcoin-backup", "bsv-bap"];
console.log(chalk.cyan(` bun add ${deps.join(" ")}`));
console.log("");
console.log("2. Add components (copy-paste pattern)");
console.log(chalk.cyan(" bigblocks add auth-button"));
console.log(chalk.gray(" Components will be copied to your project"));
console.log("");
console.log("3. Explore all components");
console.log(chalk.cyan(" bigblocks list"));
if (response.theme && response.theme !== "default") {
console.log("");
console.log("4. Change themes anytime");
console.log(chalk.gray(` Import a different theme: @import "bigblocks/themes/neo.css"`));
}
console.log("");
console.log(chalk.gray("Need help? Visit https://github.com/b-open-io/bigblocks"));
console.log("");
});
// Add list command
program
.command("list")
.description("List all available components")
.action(async () => {
const registry = await loadRegistry();
console.log("");
console.log(chalk.bold("Available Components"));
console.log("");
// Group components by type
const grouped = registry.items.reduce((acc, comp) => {
const type = comp.type.split(":")[1] || "other";
if (!acc[type])
acc[type] = [];
acc[type].push(comp);
return acc;
}, {});
// Sort components within each group
for (const key of Object.keys(grouped)) {
grouped[key]?.sort((a, b) => a.name.localeCompare(b.name));
}
// Display by category with better formatting
const categoryEmojis = {
auth: "🔐",
social: "💬",
wallet: "💰",
market: "🛒",
profile: "👤",
backup: "💾",
ui: "🎨",
provider: "🔗",
layout: "🏗️",
bap: "🔑",
security: "🛡️",
developer: "🛠️",
hooks: "⚡",
};
// Count total components
const totalComponents = registry.items.length;
console.log(chalk.gray(`Found ${totalComponents} components in registry\n`));
for (const [category, components] of Object.entries(grouped)) {
const categoryName = category.charAt(0).toUpperCase() + category.slice(1);
console.log(chalk.bold(`${categoryEmojis[category] || "📦"} ${categoryName}`));
for (const comp of components) {
console.log(` ${chalk.cyan(comp.name.padEnd(22))} ${chalk.gray(comp.description || "No description")}`);
}
console.log("");
}
console.log(chalk.bold("Commands:"));
console.log(chalk.gray(` bigblocks add ${chalk.white("<component>")} Install component(s)`));
console.log(chalk.gray(` bigblocks info ${chalk.white("<component>")} Show component details`));
console.log(chalk.gray(` bigblocks search ${chalk.white("<query>")} Search components`));
console.log(chalk.gray(" bigblocks init Initialize BigBlocks in project"));
console.log(chalk.gray(" bigblocks update Update installed components"));
console.log("");
console.log(chalk.gray("Examples:"));
console.log(chalk.gray(" bigblocks add auth-flow"));
console.log(chalk.gray(" bigblocks add social-feed wallet-overview"));
console.log(chalk.gray(" bigblocks search wallet"));
console.log(chalk.gray(" bigblocks info social-feed"));
console.log("");
await showLatestChanges();
console.log("");
console.log(chalk.gray(`💡 Tip: Use ${chalk.cyan("bigblocks search")} to find components by keyword`));
console.log("");
});
program
.command("add [components...]")
.description("Add components to your project")
.option("-y, --yes", "Skip confirmation prompt")
.option("-o, --overwrite", "Overwrite existing files")
.option("-p, --path <path>", "Custom installation path")
.action(async (componentNames, options) => {
// Load config
const configPath = join(process.cwd(), "bigblocks.config.json");
if (!existsSync(configPath)) {
console.error(chalk.red("Error: bigblocks.config.json not found."));
console.log(`Run ${chalk.cyan("bigblocks init")} first.`);
process.exit(1);
}
const config = JSON.parse(readFileSync(configPath, "utf-8"));
const registry = await loadRegistry();
let selectedComponents = componentNames;
if (!selectedComponents || selectedComponents.length === 0) {
// Show interactive selection
const response = await prompts({
type: "multiselect",
name: "components",
message: "Which components would you like to add?",
choices: registry.items.map((comp) => ({
title: `${comp.name} ${chalk.gray(`(${comp.type.split(":")[1]})`)}`,
description: comp.description || "No description",
value: comp.name,
})),
});
selectedComponents = response.components;
}
if (!selectedComponents || selectedComponents.length === 0) {
console.log("No components selected.");
return;
}
console.log("");
console.log("Installing components...");
console.log("");
const installedSet = new Set();
const errors = [];
// Install each component
for (const name of selectedComponents) {
try {
await installComponent(name, registry, config, options, installedSet);
}
catch (error) {
const err = error;
let errorMessage = err.message;
// Provide more specific error messages
if (err.message.includes("not found")) {
errorMessage = `Component "${name}" not found in registry. Run 'bigblocks list' to see available components.`;
}
else if (err.message.includes("Permission denied")) {
errorMessage =
"Permission denied. Try running with sudo or check file permissions.";
}
else if (err.message.includes("ENOTFOUND")) {
errorMessage =
"Network error. Check your internet connection and try again.";
}
errors.push(errorMessage);
}
}
if (errors.length > 0) {
console.log("");
console.log(chalk.red("✗ Some components failed to install:"));
for (const err of errors) {
console.log(chalk.red(` ${err}`));
}
console.log("");
if (installedSet.size > 0) {
console.log(chalk.yellow(`⚠ ${installedSet.size} components copied successfully, ${errors.length} failed`));
}
else {
console.log(chalk.red("✗ No components were copied"));
}
}
else if (installedSet.size > 0) {
console.log("");
console.log(chalk.green("✓ Components copied successfully to your project"));
console.log(chalk.gray(" You can now modify them to fit your needs"));
}
else {
console.log("");
console.log(chalk.yellow("⚠ No components were copied"));
}
// Show required dependencies
const requiredDeps = new Set();
for (const compName of installedSet) {
const comp = registry.items.find((c) => c.name === compName);
if (comp?.dependencies) {
for (const dep of comp.dependencies) {
requiredDeps.add(dep);
}
}
}
if (requiredDeps.size > 0) {
console.log("");
console.log("Make sure you have the required dependencies:");
console.log(chalk.cyan(` bun add ${Array.from(requiredDeps).join(" ")}`));
}
console.log("");
});
async function installComponent(name, registry, config, options, installedSet = new Set()) {
if (installedSet.has(name))
return;
const component = registry.items.find((c) => c.name === name);
if (!component) {
throw new Error(`Component "${name}" not found`);
}
// Install registry dependencies first
if (component.registryDependencies) {
for (const dep of component.registryDependencies) {
await installComponent(dep, registry, config, options, installedSet);
}
}
const shortName = component.name.replace(/-/g, " ");
console.log(`→ ${shortName}`);
// Install component files
for (const file of component.files) {
try {
// Handle both formats: string array (old) and object array (shadcn)
const filePath = typeof file === "string" ? file : file.path;
const fileContent = typeof file === "string"
? await fetchComponentFile(filePath)
: file.content || (await fetchComponentFile(filePath));
const targetOverride = typeof file === "object" ? file.target : undefined;
// Determine target path
const targetPath = targetOverride
? join(process.cwd(), targetOverride)
: join(process.cwd(), config.componentsDir, filePath.replace(/^(src\/)?components\//, ""));
// Create directory if needed
const targetDir = dirname(targetPath);
if (!existsSync(targetDir)) {
mkdirSync(targetDir, { recursive: true });
}
// Check if file exists
if (existsSync(targetPath) && !options.overwrite) {
const { overwrite } = await prompts({
type: "confirm",
name: "overwrite",
message: `File exists. Overwrite ${targetPath.split("/").pop()}?`,
initial: false,
});
if (!overwrite) {
console.log(chalk.yellow(" ⚠ Skipped"));
continue;
}
}
// Write file directly - imports are already correct in registry
try {
const filePathStr = typeof file === "string" ? file : file.path;
writeFileSync(targetPath, fileContent);
console.log