UNPKG

bigblocks

Version:

Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React

1,127 lines 71.8 kB
#!/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