/** * Enhanced Configuration manager for VC-SYS CLI * Handles global config, project config, .env.local management, and Claude Code integration */ import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { Logger } from './logger'; export interface Config { [key: string]: any; } export interface ProjectConfig { name: string; created: string; supabase?: { lastOrg?: string; defaultRegion?: string; includeSeeds?: boolean; lastProjectRef?: string; lastProjectUrl?: string; lastProvisionedAt?: string; projectId?: string; region?: string; }; } export class ConfigManager { private logger: Logger; private globalConfigDir: string; private globalConfigFile: string; private projectDir: string; private vcsysDir: string; constructor() { this.logger = new Logger('ConfigManager'); this.globalConfigDir = path.join(os.homedir(), '.vcsys'); this.globalConfigFile = path.join(this.globalConfigDir, 'config.json'); this.projectDir = process.cwd(); this.vcsysDir = path.join(this.projectDir, '.vcsys'); } // Global configuration methods async ensureConfigDir(): Promise { try { await fs.mkdir(this.globalConfigDir, { recursive: true }); } catch (error) { this.logger.error('Failed to create global config directory', error); throw error; } } async loadConfig(): Promise { try { await this.ensureConfigDir(); const configContent = await fs.readFile(this.globalConfigFile, 'utf-8'); return JSON.parse(configContent); } catch (error) { // If config doesn't exist, return empty config if ((error as any).code === 'ENOENT') { return {}; } this.logger.error('Failed to load global config', error); throw error; } } async saveConfig(config: Config): Promise { try { await this.ensureConfigDir(); await fs.writeFile(this.globalConfigFile, JSON.stringify(config, null, 2), 'utf-8'); this.logger.info('Global config saved successfully'); } catch (error) { this.logger.error('Failed to save global config', error); throw error; } } async get(key: string, defaultValue?: any): Promise { const config = await this.loadConfig(); return config[key] ?? defaultValue; } async set(key: string, value: any): Promise { const config = await this.loadConfig(); config[key] = value; await this.saveConfig(config); } async delete(key: string): Promise { const config = await this.loadConfig(); delete config[key]; await this.saveConfig(config); } getConfigDir(): string { return this.globalConfigDir; } // Project-specific configuration methods async ensureProjectDir(): Promise { try { await fs.mkdir(this.vcsysDir, { recursive: true }); } catch (error) { this.logger.error('Failed to create project config directory', error); throw error; } } async isInitialized(): Promise { try { await fs.access(this.vcsysDir); return true; } catch { return false; } } async getProjectConfig(): Promise { const configPath = path.join(this.vcsysDir, 'config.json'); try { if (await this.fileExists(configPath)) { const configData = await fs.readFile(configPath, 'utf-8'); return JSON.parse(configData); } } catch (error) { this.logger.warn('Failed to read project config, using defaults', error); } // Default project config return { name: path.basename(this.projectDir), created: new Date().toISOString(), supabase: { defaultRegion: 'us-east-1', includeSeeds: true } }; } async saveProjectConfig(config: ProjectConfig): Promise { try { await this.ensureProjectDir(); const configPath = path.join(this.vcsysDir, 'config.json'); await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8'); this.logger.debug('Project config saved'); } catch (error) { this.logger.error('Failed to save project config', error); throw error; } } // .env.local file management async updateEnvLocal(envContent: string): Promise { const envPath = path.join(this.projectDir, '.env.local'); try { if (await this.fileExists(envPath)) { // Merge with existing .env.local const existing = await fs.readFile(envPath, 'utf8'); const updated = this.mergeEnvContent(existing, envContent); await fs.writeFile(envPath, updated, 'utf8'); this.logger.info('Updated existing .env.local file'); } else { // Create new .env.local await fs.writeFile(envPath, envContent, 'utf8'); this.logger.info('Created new .env.local file'); } } catch (error) { this.logger.error('Failed to update .env.local', error); throw error; } } private mergeEnvContent(existing: string, newContent: string): string { const existingLines = existing.split('\n'); const newLines = newContent.split('\n'); // Remove old Supabase configuration lines const filtered = existingLines.filter(line => { const trimmed = line.trim(); return ( !trimmed.startsWith('NEXT_PUBLIC_SUPABASE_') && !trimmed.startsWith('SUPABASE_') && !trimmed.startsWith('DATABASE_URL=') && !trimmed.startsWith('DIRECT_URL=') && !trimmed.startsWith('# Supabase Configuration') && !trimmed.startsWith('# Generated by VC-SYS') && !trimmed.startsWith('NODE_ENV=development') && trimmed !== '' ); }); // Remove trailing empty lines while (filtered.length > 0 && filtered[filtered.length - 1].trim() === '') { filtered.pop(); } // Add proper spacing and new configuration return filtered.length > 0 ? [...filtered, '', ...newLines].join('\n') : newLines.join('\n'); } // Claude Code integration async updateClaudeConfig(): Promise { const claudeDir = path.join(this.projectDir, '.claude'); await fs.mkdir(claudeDir, { recursive: true }); const commandsPath = path.join(claudeDir, 'vcsys-commands.md'); if (!(await this.fileExists(commandsPath))) { const commandsContent = `# VC-SYS CLI Integration for Claude Code ## Overview The VC-SYS CLI provides command-driven Supabase project provisioning designed specifically for Claude Code AI integration. ## Authentication Commands \`\`\`bash vcsys auth login # OAuth authentication with Supabase vcsys auth status # Check auth status + API connectivity vcsys auth logout # Clear authentication tokens \`\`\` ## Provisioning Commands \`\`\`bash vcsys provision --list-orgs # Get available organizations as JSON vcsys provision --list-regions # Get available regions as JSON vcsys provision --list-templates # Get available schema templates as JSON vcsys provision --org "Organization Name" --name "project-name" [options] \`\`\` `; await fs.writeFile(commandsPath, commandsContent); this.logger.info('Created Claude Code integration documentation'); } } // Utility methods async getLastProject(): Promise<{ ref: string; url: string; provisionedAt: string } | null> { const config = await this.getProjectConfig(); if (config.supabase?.lastProjectRef) { return { ref: config.supabase.lastProjectRef, url: config.supabase.lastProjectUrl || `https://${config.supabase.lastProjectRef}.supabase.co`, provisionedAt: config.supabase.lastProvisionedAt || 'Unknown' }; } return null; } private async fileExists(filePath: string): Promise { try { await fs.access(filePath); return true; } catch { return false; } } getProjectPaths(): { projectDir: string; vcsysDir: string; envPath: string; claudeDir: string; } { return { projectDir: this.projectDir, vcsysDir: this.vcsysDir, envPath: path.join(this.projectDir, '.env.local'), claudeDir: path.join(this.projectDir, '.claude') }; } }