mirror-magi-meta-agent
Version:
AI-powered development planning and execution system with Supabase integration
246 lines (202 loc) ⢠7.48 kB
JavaScript
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
/**
* Supabase Pre-flight Checker
* Validates Supabase environment before executing database tasks
*/
class SupabasePreflightChecker {
constructor() {
this.checks = {
cli: false,
linked: false,
connection: false,
localDb: false,
migrations: false,
environment: false
};
this.warnings = [];
this.errors = [];
}
async runChecks() {
console.log('š Supabase Pre-flight Checks');
console.log('ā'.repeat(40));
await this.checkCLI();
await this.checkProjectLinked();
await this.checkConnection();
await this.checkEnvironment();
await this.checkMigrations();
this.showResults();
return this.errors.length === 0;
}
async checkCLI() {
process.stdout.write('ā Checking Supabase CLI installation... ');
try {
const { stdout } = await execAsync('supabase --version');
this.checks.cli = true;
console.log('ā
');
console.log(` Version: ${stdout.trim()}`);
} catch (error) {
this.checks.cli = false;
this.errors.push('Supabase CLI not installed');
console.log('ā');
console.log(' Run: brew install supabase/tap/supabase');
}
}
async checkProjectLinked() {
if (!this.checks.cli) return;
process.stdout.write('ā Verifying supabase project linked... ');
try {
const { stdout } = await execAsync('supabase status');
this.checks.linked = true;
// Parse status output
const lines = stdout.split('\n');
const projectId = lines.find(l => l.includes('API URL'))?.split(' ').pop();
console.log('ā
');
if (projectId) {
console.log(` Project: ${projectId}`);
}
} catch (error) {
this.checks.linked = false;
this.errors.push('No Supabase project linked');
console.log('ā');
console.log(' Run: supabase link --project-ref <project-id>');
}
}
async checkConnection() {
if (!this.checks.linked) return;
process.stdout.write('ā Checking database connection... ');
try {
const { stdout } = await execAsync('supabase db remote commit --dry-run');
this.checks.connection = true;
console.log('ā
');
} catch (error) {
// If it fails because there are no changes, that's OK
if (error.message.includes('no changes')) {
this.checks.connection = true;
console.log('ā
');
} else {
this.checks.connection = false;
this.errors.push('Cannot connect to database');
console.log('ā');
console.log(' Check your internet connection and credentials');
}
}
}
async checkEnvironment() {
process.stdout.write('ā Validating environment variables... ');
const requiredEnvVars = [
'NEXT_PUBLIC_SUPABASE_URL',
'NEXT_PUBLIC_SUPABASE_ANON_KEY'
];
const missingVars = requiredEnvVars.filter(varName => !process.env[varName]);
if (missingVars.length === 0) {
this.checks.environment = true;
console.log('ā
');
} else {
this.checks.environment = false;
this.warnings.push(`Missing environment variables: ${missingVars.join(', ')}`);
console.log('ā ļø');
console.log(` Missing: ${missingVars.join(', ')}`);
}
// Check if using local or remote database
const dbUrl = process.env.SUPABASE_DB_URL || process.env.DATABASE_URL || '';
if (dbUrl.includes('localhost') || dbUrl.includes('127.0.0.1')) {
this.checks.localDb = true;
console.log(' ā
Using local database (safe for testing)');
} else if (dbUrl) {
this.checks.localDb = false;
this.warnings.push('Using remote database - be careful with migrations!');
console.log(' ā ļø Using remote database');
}
}
async checkMigrations() {
if (!this.checks.cli) return;
process.stdout.write('ā Checking for uncommitted migrations... ');
try {
const { stdout } = await execAsync('git status supabase/migrations --porcelain');
if (stdout.trim() === '') {
this.checks.migrations = true;
console.log('ā
');
} else {
this.checks.migrations = false;
this.warnings.push('You have uncommitted migrations');
console.log('ā ļø');
console.log(' Uncommitted files in supabase/migrations/');
console.log(' Consider committing before creating new migrations');
}
} catch (error) {
// Git not available or not a git repo
console.log('ā ļø (Git check skipped)');
}
}
showResults() {
console.log('\nš PRE-FLIGHT RESULTS:');
console.log('ā'.repeat(30));
if (this.errors.length > 0) {
console.log('\nā ERRORS (must fix):');
this.errors.forEach(error => {
console.log(` ⢠${error}`);
});
}
if (this.warnings.length > 0) {
console.log('\nā ļø WARNINGS (review):');
this.warnings.forEach(warning => {
console.log(` ⢠${warning}`);
});
}
if (this.errors.length === 0 && this.warnings.length === 0) {
console.log('\nā
All checks passed! Ready for Supabase operations.');
}
console.log('\nš”ļø SAFETY RECOMMENDATIONS:');
console.log('⢠Always test migrations locally first');
console.log('⢠Use "supabase db reset" to test from scratch');
console.log('⢠Keep migrations small and focused');
console.log('⢠Include rollback SQL in comments');
}
async generateMigrationCommand(taskDetails) {
console.log('\nš SUPABASE MIGRATION COMMAND:');
console.log('ā'.repeat(40));
const migrationName = taskDetails.specifics?.migrationName || 'new_migration';
const timestamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
console.log(`\n# Generate migration file:`);
console.log(`supabase migration new ${migrationName}\n`);
console.log(`# This creates: supabase/migrations/${timestamp}_${migrationName}.sql\n`);
if (taskDetails.specifics?.sql) {
console.log(`# Add this SQL to the migration file:`);
console.log('```sql');
console.log(taskDetails.specifics.sql);
console.log('```\n');
}
console.log(`# Test locally:`);
console.log(`supabase db reset # Recreates database and runs all migrations\n`);
console.log(`# Apply to remote (after testing):`);
console.log(`supabase db push\n`);
if (taskDetails.specifics?.rollback) {
console.log(`# Rollback SQL (keep in comments):`);
console.log('```sql');
console.log(`-- Rollback: ${migrationName}`);
console.log(taskDetails.specifics.rollback);
console.log('```');
}
}
}
// Run if called directly
if (require.main === module) {
const checker = new SupabasePreflightChecker();
// Check if task details provided
const taskJson = process.argv[2];
checker.runChecks().then(async (passed) => {
if (passed && taskJson) {
try {
const taskDetails = JSON.parse(taskJson);
await checker.generateMigrationCommand(taskDetails);
} catch (e) {
console.error('Invalid task JSON provided');
}
}
process.exit(passed ? 0 : 1);
});
}
module.exports = SupabasePreflightChecker;