UNPKG

@agenticsql/cli

Version:

Generate a production-ready SQL schema from your codebase using the power of Agentic AI.

773 lines (651 loc) • 28.3 kB
#!/usr/bin/env node import fs from 'fs/promises'; import path from 'path'; import { glob } from 'glob'; import axios from 'axios'; import ora from 'ora'; import prompts from 'prompts'; import open from 'open'; import chalk from 'chalk'; import { execSync } from 'child_process'; const API_URL = 'https://3mlh63k42evnbx4yucvas2y3pu0frbqx.lambda-url.us-east-1.on.aws/'; const DASHBOARD_URL = 'https://agenticsql.ai/dashboard'; const GITHUB_DASHBOARD_URL = 'https://agenticsql.ai/dashboard/github'; const CLI_DEPLOY_URL = process.env.CLI_DEPLOY_URL || 'https://agenticsql.ai/api/cli/deploy'; const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.agenticsql'); const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json'); // Smart detection - only scan what the project actually uses async function detectProjectType() { const detections = { isNextJS: false, isReactNative: false, isPython: false, isFlutter: false, isGo: false, isRuby: false, hasPrisma: false, hasDrizzle: false, hasSupabase: false, hasFirebase: false, // Specific feature detection hasNextAuth: false, hasBlog: false, hasWebhooks: false, hasStripeCheckout: false }; try { // Check for Next.js const nextConfig = await glob('next.config.{js,mjs,ts}', { nodir: true }); detections.isNextJS = nextConfig.length > 0; // Check for React Native const rnConfig = await glob('@(metro.config.js|app.json)', { nodir: true }); detections.isReactNative = rnConfig.length > 0; // Check for Python const pythonFiles = await glob('@(requirements.txt|setup.py|pyproject.toml)', { nodir: true }); detections.isPython = pythonFiles.length > 0; // Check for Flutter const flutterConfig = await glob('pubspec.yaml', { nodir: true }); detections.isFlutter = flutterConfig.length > 0; // Check for Go const goFiles = await glob('go.mod', { nodir: true }); detections.isGo = goFiles.length > 0; // Check for Ruby/Rails const rubyFiles = await glob('@(Gemfile|config/application.rb)', { nodir: true }); detections.isRuby = rubyFiles.length > 0; // Check for ORMs const prismaSchema = await glob('prisma/schema.prisma', { nodir: true }); detections.hasPrisma = prismaSchema.length > 0; const drizzleConfig = await glob('drizzle.config.{ts,js}', { nodir: true }); detections.hasDrizzle = drizzleConfig.length > 0; // Check for Backend services const supabaseFiles = await glob('supabase/**', { nodir: true }); detections.hasSupabase = supabaseFiles.length > 0; const firebaseFiles = await glob('firebase.json', { nodir: true }); detections.hasFirebase = firebaseFiles.length > 0; // CRITICAL: Specific feature detection for mandatory tables // NextAuth detection (sessions table required!) const nextAuthFiles = await glob('**/api/auth/**/*.{ts,js}', { nodir: true, ignore: IGNORE_PATTERNS }); const hasNextAuthRoute = nextAuthFiles.some(f => f.includes('nextauth') || f.includes('[...nextauth]')); const authConfigFiles = await glob('@(auth.ts|auth.config.ts|auth.config.js)', { nodir: true }); detections.hasNextAuth = hasNextAuthRoute || authConfigFiles.length > 0; // Blog detection (blog_posts table required!) const blogRoutes = await glob('**/*blog*/**/*.{ts,tsx,js,jsx}', { nodir: true, ignore: IGNORE_PATTERNS }); const blogApiRoutes = await glob('**/api/*blog*/**/*.{ts,js}', { nodir: true, ignore: IGNORE_PATTERNS }); detections.hasBlog = blogRoutes.length > 0 || blogApiRoutes.length > 0; // Webhook detection (webhooks table required!) const webhookRoutes = await glob('**/api/webhook*/**/*.{ts,js}', { nodir: true, ignore: IGNORE_PATTERNS }); detections.hasWebhooks = webhookRoutes.length > 0; // Stripe Checkout detection (payment_methods table required!) const stripeCheckout = await glob('**/api/stripe/checkout/**/*.{ts,js}', { nodir: true, ignore: IGNORE_PATTERNS }); detections.hasStripeCheckout = stripeCheckout.length > 0; } catch (e) { /* ignore */ } return detections; } function buildSmartPatterns(detections) { const patterns = []; // LAYER 1: Schema/Model Files (STRONGEST SIGNAL) // ORM schemas - highest priority if (detections.hasPrisma) { patterns.push('prisma/schema.prisma'); } if (detections.hasDrizzle) { patterns.push('drizzle.config.{ts,js}', 'drizzle/**/*.{ts,js}'); } // Explicit model/schema files patterns.push( '**/models.{ts,js,py,go,rb}', '**/models/**/*.{ts,js,py,go,rb}', // Files inside models folder '**/schema.{ts,js,go}', '**/types.{ts,js,go}', '**/types/**/*.{ts,js,go}', // Files inside types folder '**/*.entity.{ts,js,go}', '**/*.model.{ts,js,go,rb}', '**/schemas.py', '**/database.{py,go}', '**/db.{ts,js,py,go}' ); // LAYER 2: API Routes & Server Logic (STRONG SIGNAL) if (detections.isNextJS) { patterns.push( 'app/api/**/*.{ts,js}', 'pages/api/**/*.{ts,js}', 'src/app/api/**/*.{ts,js}', 'src/pages/api/**/*.{ts,js}' ); } if (detections.isPython) { patterns.push( 'app/**/*.py', 'api/**/*.py', 'routes/**/*.py', 'views/**/*.py' ); } if (detections.isGo) { patterns.push( 'models/**/*.go', 'api/**/*.go', 'handlers/**/*.go', 'controllers/**/*.go', 'internal/**/*.go' ); } if (detections.isRuby) { patterns.push( 'app/models/**/*.rb', 'app/controllers/**/*.rb', 'db/schema.rb' ); } if (detections.isFlutter) { patterns.push('lib/@(models|services|data)/**/*.dart'); } // Generic API/server patterns patterns.push( 'api/**/*.{ts,js,go}', 'server/**/*.{ts,js,go}', 'routes/**/*.{ts,js,go}', '**/*.controller.{ts,js,go,rb}', '**/*.service.{ts,js,go,rb}', '**/*.repository.{ts,js,go,rb}' ); // LAYER 3: Frontend Components (CONTEXTUAL SIGNAL) if (detections.isNextJS || detections.isReactNative) { patterns.push( 'components/**/*@(Form|Card|Profile|List|Table|Detail).{tsx,jsx}', 'src/components/**/*@(Form|Card|Profile|List|Table|Detail).{tsx,jsx}', 'app/**/*@(Form|Card|Profile|List|Table|Detail).{tsx,jsx}' ); } if (detections.isFlutter) { patterns.push('lib/widgets/**/*.dart', 'lib/screens/**/*.dart'); } // Backend service patterns if (detections.hasSupabase) { patterns.push('supabase/**/*.{ts,js,sql}'); } if (detections.hasFirebase) { patterns.push('firebase/**/*.{ts,js}'); } // Fallback: comprehensive scan if nothing specific detected if (patterns.length === 0) { patterns.push( 'src/**/@(models|types|schemas|api|components)/**/*.{ts,js,py,go,rb}', 'app/**/@(models|types|schemas|api|components)/**/*.{ts,js,py,go,rb}', '**/models.{ts,js,py,go,rb}', '**/schema.{ts,js,py,go,rb}' ); } return patterns; } const WIDE_SCAN_PATTERN = '**/*.{ts,tsx,js,jsx,py,dart,go,rb,mjs,mts}'; const IGNORE_PATTERNS = [ // Dependencies & Build artifacts '**/node_modules/**', '**/dist/**', '**/build/**', '**/coverage/**', '**/.git/**', '**/.next/**', '**/.vercel/**', '**/.turbo/**', '**/out/**', '**/vendor/**', // Test files '*.test.*', '*.spec.*', '**/test/**', '**/tests/**', '**/__tests__/**', // Type definitions (not actual code) '*.d.ts', // Hidden/system files (macOS ghost files) - CRITICAL! '**/.DS_Store', '**/.AppleDouble/**', '**/.LSOverride', '**/._*', // macOS resource forks '._*', // Root level ghost files // Documentation & Scripts (CRITICAL: Exclude these!) '**/*.md', '**/*.sh', // Config files '**/*.config.{js,ts,mjs}', '**/tsconfig.json', '**/package.json', '**/*.lock', '**/*.log', // Static assets '**/public/**', '**/static/**', '**/assets/**', '**/images/**', // UI-only (no data models) - BUT keep forms! '**/styles/**', '**/css/**', '**/scss/**' ]; // --- THE MISSING PISTONS - START --- const INTELLIGENCE_KEYWORDS = { 'AUTH': ['auth', 'password', 'jwt', 'token', 'login', 'user', 'session'], 'BILLING': ['stripe', 'paddle', 'lemonsqueezy', 'charge', 'subscription', 'payment', 'invoice'], 'API_KEY': ['apiKey', 'api_key', 'secret', 'client_id', 'client_secret'], 'GEOLOCATION': ['location', 'address', 'geo', 'latitude', 'longitude', 'coordinates'] }; function extractIntelligence(content) { const tags = new Set(); const lowerCaseContent = content.toLowerCase(); for (const tag in INTELLIGENCE_KEYWORDS) { for (const keyword of INTELLIGENCE_KEYWORDS[tag]) { if (lowerCaseContent.includes(keyword)) { tags.add(tag); break; } } } return Array.from(tags); } const MAX_TOTAL_CHARS = 200000; // Increased for comprehensive code analysis async function getFileContent(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); return content.substring(0, 20000); } catch { return null; } } function extractCodeIntelligence(content, filePath) { const intelligence = { entities: [], apiRoutes: [], relationships: [], features: { hasAuth: false, hasBilling: false, hasWebhooks: false, hasEmail: false, hasMultiTenant: false } }; const lower = content.toLowerCase(); // Feature detection intelligence.features.hasAuth = /auth|login|register|password|session|jwt|token/.test(lower); intelligence.features.hasBilling = /stripe|paddle|payment|subscription|invoice|checkout/.test(lower); intelligence.features.hasWebhooks = /webhook/.test(lower); intelligence.features.hasEmail = /email.*template|sendgrid|mailgun|resend|nodemailer/.test(lower); intelligence.features.hasMultiTenant = /organization|tenant|workspace/.test(lower); // Extract TypeScript interfaces/types (improved regex for multi-line) const interfaceRegex = /(?:export\s+)?(?:interface|type)\s+(\w+)\s*\{([^}]*)\}/gs; let match; while ((match = interfaceRegex.exec(content)) !== null) { const name = match[1]; const body = match[2]; const fields = extractFields(body); if (Object.keys(fields).length > 0) { intelligence.entities.push({ name, fields, source: 'interface' }); } } // Extract Python classes const pythonClassRegex = /class\s+(\w+).*?:\s*([\s\S]*?)(?=\nclass|\n\n|$)/g; while ((match = pythonClassRegex.exec(content)) !== null) { const name = match[1]; const body = match[2]; const fields = extractPythonFields(body); if (Object.keys(fields).length > 0) { intelligence.entities.push({ name, fields, source: 'class' }); } } // Extract API routes (Next.js, Express, FastAPI) const apiPatterns = [ /(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/g, /export\s+async\s+function\s+(GET|POST|PUT|DELETE|PATCH)/g, /@(?:app|router)\.(get|post|put|delete|patch)\(['"`]([^'"`]+)['"`]/g ]; for (const pattern of apiPatterns) { while ((match = pattern.exec(content)) !== null) { intelligence.apiRoutes.push({ method: match[1].toUpperCase(), path: match[2] || 'unknown' }); } } // Extract relationships (foreign keys) const fkRegex = /(\w+)_id[:\s]/g; while ((match = fkRegex.exec(content)) !== null) { if (!intelligence.relationships.includes(match[1])) { intelligence.relationships.push(match[1]); } } return intelligence; } function extractFields(interfaceBody) { const fields = {}; // Match: fieldName?: type or fieldName: type const fieldRegex = /(\w+)\??\s*:\s*(string|number|boolean|Date|any|\w+(?:\[\])?)/g; let match; while ((match = fieldRegex.exec(interfaceBody)) !== null) { fields[match[1]] = match[2]; } return fields; } function extractPythonFields(classBody) { const fields = {}; // Match: self.field = ... or field: type const fieldRegex = /(?:self\.(\w+)|(\w+)\s*:\s*(\w+))/g; let match; while ((match = fieldRegex.exec(classBody)) !== null) { const fieldName = match[1] || match[2]; const fieldType = match[3] || 'any'; if (fieldName && !fieldName.startsWith('_')) { fields[fieldName] = fieldType; } } return fields; } async function getProjectTitle(directory) { try { const htmlFiles = await glob('**/index.html', { cwd: directory, ignore: IGNORE_PATTERNS }); if (htmlFiles.length > 0) { const htmlContent = await fs.readFile(path.join(directory, htmlFiles[0]), 'utf-8'); const match = htmlContent.match(/<title>(.*?)<\/title>/i); if (match && match[1]) { return match[1].split(/[-|]/)[0].trim().replace(/\s+/g, '_'); } } } catch (e) { /* ignore */ } return path.basename(directory); } // --- THE MISSING PISTONS - END --- // Detect git repository function detectGitRepo() { try { const remoteUrl = execSync('git config --get remote.origin.url', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(); const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }).trim(); let fullName = null; if (remoteUrl.includes('github.com')) { const match = remoteUrl.match(/github\.com[:/](.+?)(?:\.git)?$/); if (match) { fullName = match[1]; } } if (!fullName) return null; return { fullName, url: remoteUrl, branch }; } catch (error) { return null; } } // Config management async function getApiKey() { try { await fs.mkdir(CONFIG_DIR, { recursive: true }); const configData = await fs.readFile(CONFIG_FILE, 'utf-8'); const config = JSON.parse(configData); return config.apiKey || null; } catch (error) { return null; } } async function saveApiKey(apiKey) { try { await fs.mkdir(CONFIG_DIR, { recursive: true }); await fs.writeFile(CONFIG_FILE, JSON.stringify({ apiKey }, null, 2)); return true; } catch (error) { console.error(chalk.red('Failed to save API key:'), error.message); return false; } } // Direct deploy with API key - ENTERPRISE FLOW async function deployWithApiKey() { console.log(chalk.bold.cyan('\nšŸš€ Direct Deploy\n')); let spinner = ora('Detecting git repository...').start(); const repoInfo = detectGitRepo(); if (!repoInfo) { spinner.fail('Not a git repository'); console.log(chalk.gray('\nšŸ’” Initialize git first:')); console.log(chalk.gray(' git init')); console.log(chalk.gray(' git remote add origin <url>\n')); console.log(chalk.yellow('šŸ’” Or use: npx @agenticsql/cli (for local scan)\n')); return; } spinner.succeed(`Repository: ${chalk.cyan(repoInfo.fullName)} (${repoInfo.branch})`); // Check for API key let apiKey = await getApiKey(); if (!apiKey) { console.log(chalk.yellow('\nāš ļø No API key found')); console.log(chalk.gray('šŸ“‹ Get your API key: https://agenticsql.ai/dashboard (API tab)\n')); const response = await prompts({ type: 'password', name: 'key', message: 'Enter your API key:', }); if (!response.key) { console.log(chalk.red('\nāŒ API key required for direct deploy\n')); return; } apiKey = response.key; await saveApiKey(apiKey); console.log(chalk.green('āœ… API key saved to ~/.agenticsql/config.json\n')); } // Note: Schema will be generated during deployment // The deployment process will save it to the database spinner = ora('Preparing deployment...').start(); spinner.succeed(chalk.green('āœ… Ready to deploy')); // Deploy spinner = ora('Deploying database...').start(); try { const response = await axios.post( CLI_DEPLOY_URL, { repoFullName: repoInfo.fullName, branch: repoInfo.branch, }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, } ); const { deploymentId, repoFullName, branch, schema } = response.data; spinner.succeed(chalk.green('āœ… Deployment started!')); // Save schema to file if available if (schema) { try { const projectName = repoInfo.fullName.split('/')[1]; const schemaFilename = `${projectName}_schema.sql`; const currentDirectory = process.cwd(); const schemaPath = path.join(currentDirectory, schemaFilename); await fs.writeFile(schemaPath, schema); console.log(''); console.log(chalk.green('āœ… Schema saved to:'), chalk.white(schemaFilename)); } catch (err) { console.log(chalk.yellow('āš ļø Could not save schema file')); } } console.log(''); console.log(chalk.gray('šŸ“¦ Repository:'), chalk.white(repoFullName)); console.log(chalk.gray('🌿 Branch:'), chalk.white(branch)); console.log(chalk.gray('šŸ†” Deployment ID:'), chalk.white(deploymentId)); console.log(''); console.log(chalk.gray('šŸ“§ Email notification sent')); console.log(chalk.gray('šŸ”— Dashboard:'), chalk.cyan('https://agenticsql.ai/dashboard')); console.log(''); console.log(chalk.green('✨ Your database will be ready in ~30 seconds!\n')); } catch (error) { spinner.fail(chalk.red('Deployment failed')); if (error.response) { const status = error.response.status; const data = error.response.data; if (status === 401) { console.log(chalk.red('\nāŒ Invalid API key')); console.log(chalk.gray('šŸ’” Get a new key: https://agenticsql.ai/dashboard (API tab)')); console.log(chalk.gray('šŸ’” Or run: npx @agenticsql/cli config --reset\n')); } else if (status === 400 && data.error?.includes('GitHub not connected')) { console.log(chalk.red('\nāŒ GitHub not connected')); console.log(chalk.gray('šŸ’” Connect GitHub first: https://agenticsql.ai/dashboard/github')); console.log(chalk.gray('šŸ’” Or run: npx @agenticsql/cli --github\n')); } else { console.log(chalk.red(`\nāŒ Error: ${data.error || 'Unknown error'}\n`)); } } else { console.log(chalk.red(`\nāŒ Error: ${error.message}\n`)); } } } // GitHub deploy flow - SEPARATE from main CLI async function deployWithGitHub() { console.log(chalk.bold.cyan('\nšŸ”— GitHub Auto-Deploy\n')); let spinner = ora('Detecting git repository...').start(); const repoInfo = detectGitRepo(); if (!repoInfo) { spinner.fail('Not a git repository'); console.log(chalk.gray('\nšŸ’” Initialize git first:')); console.log(chalk.gray(' git init')); console.log(chalk.gray(' git remote add origin <url>\n')); return; } spinner.succeed(`Repository: ${chalk.cyan(repoInfo.fullName)}`); // Open dashboard for connection management console.log(chalk.yellow('\n🌐 Opening GitHub dashboard...\n')); await open(GITHUB_DASHBOARD_URL); console.log(chalk.gray('Next steps:')); console.log(chalk.gray('1. Connect your repository (if not connected)')); console.log(chalk.gray('2. Click "Deploy Now" button')); console.log(chalk.gray('3. Your database will be deployed automatically!\n')); console.log(chalk.green('āœ… Dashboard opened!\n')); } async function main() { console.log(chalk.bold.yellow('⚔ AgenticSQL') + chalk.gray(' - Your Code is the Schema.')); console.log(''); // Check for flags const args = process.argv.slice(2); if (args.includes('--deploy')) { await deployWithApiKey(); return; } if (args.includes('--github')) { await deployWithGitHub(); return; } let spinner = ora('Detecting project type...').start(); try { // Smart detection const detections = await detectProjectType(); const smartPatterns = buildSmartPatterns(detections); // Log detected project type const projectTypes = []; if (detections.isNextJS) projectTypes.push('Next.js'); if (detections.isReactNative) projectTypes.push('React Native'); if (detections.isPython) projectTypes.push('Python'); if (detections.isFlutter) projectTypes.push('Flutter'); if (detections.isGo) projectTypes.push('Go'); if (detections.isRuby) projectTypes.push('Ruby'); if (detections.hasPrisma) projectTypes.push('Prisma'); if (detections.hasDrizzle) projectTypes.push('Drizzle'); if (projectTypes.length > 0) { spinner.succeed(`Detected: ${projectTypes.join(', ')}`); spinner = ora('Scanning relevant files...').start(); } else { spinner.text = 'Scanning relevant files...'; } let allFiles = []; for (const pattern of smartPatterns) { allFiles.push(...(await glob(pattern, { ignore: IGNORE_PATTERNS, nodir: true }))); } let uniqueFiles = [...new Set(allFiles)]; if (uniqueFiles.length === 0) { spinner.warn('āš ļø No model or schema files found. Please ensure your project has API routes, models, or schema definitions.'); return; } spinner.succeed(`Found ${uniqueFiles.length} files`); spinner = ora('Analyzing your codebase...').start(); let smartSummary = []; let totalChars = 0; for (const file of uniqueFiles) { if (totalChars >= MAX_TOTAL_CHARS) { break; } const content = await getFileContent(file); if (content) { const intelligenceTags = extractIntelligence(content); const codeIntel = extractCodeIntelligence(content, file); smartSummary.push({ filePath: file, definitions: content, tags: intelligenceTags, intelligence: codeIntel }); totalChars += content.length; } } if (smartSummary.length === 0) { spinner.warn('āš ļø No readable content with schema definitions found. Cannot generate a blueprint.'); return; } spinner.succeed(`Analyzed ${smartSummary.length} files`); const currentDirectory = process.cwd(); const projectName = await getProjectTitle(currentDirectory); // Build structured intelligence summary const structuredIntelligence = { entities: [], apiRoutes: [], relationships: [], features: { hasAuth: false, hasBilling: false, hasWebhooks: false, hasEmail: false, hasMultiTenant: false } }; // Aggregate intelligence from all files for (const item of smartSummary) { if (item.intelligence) { structuredIntelligence.entities.push(...item.intelligence.entities); structuredIntelligence.apiRoutes.push(...item.intelligence.apiRoutes); structuredIntelligence.relationships.push(...item.intelligence.relationships); // Merge features Object.keys(item.intelligence.features).forEach(key => { if (item.intelligence.features[key]) { structuredIntelligence.features[key] = true; } }); } } // Remove duplicates structuredIntelligence.entities = Array.from(new Set(structuredIntelligence.entities.map(JSON.stringify))).map(JSON.parse); structuredIntelligence.apiRoutes = Array.from(new Set(structuredIntelligence.apiRoutes.map(JSON.stringify))).map(JSON.parse); structuredIntelligence.relationships = [...new Set(structuredIntelligence.relationships)]; console.log(chalk.cyan(`\nšŸ“Š Intelligence Summary:`)); console.log(chalk.gray(` Entities: ${structuredIntelligence.entities.length}`)); console.log(chalk.gray(` API Routes: ${structuredIntelligence.apiRoutes.length}`)); console.log(chalk.gray(` Relationships: ${structuredIntelligence.relationships.length}`)); console.log(chalk.gray(` Features: ${Object.keys(structuredIntelligence.features).filter(k => structuredIntelligence.features[k]).join(', ')}`)); console.log(''); spinner = ora('Generating your production schema...').start(); const response = await axios.post(API_URL, { summary: smartSummary, projectName: projectName, detections: detections, intelligence: structuredIntelligence }, { headers: { 'Content-Type': 'application/json' } }); const { schema } = response.data; const outputFilename = `${projectName}_schema.sql`; const outputPath = path.join(currentDirectory, outputFilename); await fs.writeFile(outputPath, schema); spinner.succeed(chalk.green('āœ… Schema Generated!') + chalk.gray(` Saved to `) + chalk.white.bold(outputFilename)); console.log(''); const deployChoice = await prompts({ type: 'confirm', name: 'value', message: chalk.bold.red('Deploy your foundation? (Free Tier)'), initial: true }); if (deployChoice.value) { if (process.env.REPL_ID || process.env.REPL_SLUG) { console.log(''); console.log(chalk.bgMagenta.bold(' YOUR NEXT STEP: DEPLOY YOUR FOUNDATION ')); console.log(chalk.gray(' Replit\'s environment blocks automatic browser opening.')); console.log(chalk.white(' Please copy this link and paste it into your browser:')); console.log(chalk.cyan.underline(` ${DASHBOARD_URL}`)); console.log(''); } else { spinner.start('šŸš€ Activating Command Center...'); await open(DASHBOARD_URL); spinner.succeed(chalk.green('Command Center is online.')); } } else { console.log(chalk.yellow('šŸ‘ Blueprint is ready. Deploy your foundation when you are.')); } } catch (error) { if (spinner && spinner.isSpinning) spinner.fail(); console.error(chalk.red('āŒ An error occurred:')); if (error.response) { console.error(chalk.red(` Server Error (${error.response.status}): ${JSON.stringify(error.response.data)}`)); } else { console.error(chalk.red(` Error: ${error.message}`)); } } } main();