/* eslint-disable no-console */ import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import fs from 'fs/promises'; import path from 'path'; import { getApiKey, getProjectId, getApiUrl } from '../utils/config.js'; import axios from 'axios'; // Core Logic (Exported for re-use) export async function syncEnv(projectId: string, apiKey: string, apiUrl: string, silent = false): Promise { if (!silent) { console.log(''); console.log(chalk.bold.yellow('╔══════════════════════════════════════════╗')); console.log(chalk.bold.yellow('║') + chalk.bold.white(' 🛡️ RIGSTATE SOVEREIGN VAULT SYNC 🛡️ ') + chalk.bold.yellow('║')); console.log(chalk.bold.yellow('╚══════════════════════════════════════════╝')); console.log(''); } const spinner = ora('Fetching secrets from Vault...').start(); try { const response = await axios.post(`${apiUrl}/api/v1/vault/sync`, { project_id: projectId }, { headers: { Authorization: `Bearer ${apiKey}` } }); if (!response.data.success) { throw new Error(response.data.error || 'Failed to fetch secrets'); } const vaultContent = response.data.data.content || ''; const secretCount = response.data.data.count || 0; if (secretCount === 0) { spinner.info('No secrets found in Vault for this project.'); if (!silent) console.log(chalk.dim(' Add secrets via the Rigstate web interface.')); return true; } spinner.succeed(`Retrieved ${chalk.bold(secretCount)} secret(s)`); // Read existing .env.local for comparison const envFile = path.resolve(process.cwd(), '.env.local'); let existingContent = ''; let existingKeys: Set = new Set(); try { existingContent = await fs.readFile(envFile, 'utf-8'); // Parse existing keys existingContent.split('\n').forEach(line => { const match = line.match(/^([A-Z_][A-Z0-9_]*)=/); if (match) existingKeys.add(match[1]); }); } catch (e) { // File doesn't exist } // Parse vault keys const vaultKeys: Set = new Set(); vaultContent.split('\n').forEach((line: string) => { const match = line.match(/^([A-Z_][A-Z0-9_]*)=/); if (match) vaultKeys.add(match[1]); }); // Calculate changes let newCount = 0; let updatedCount = 0; vaultKeys.forEach(key => { if (!existingKeys.has(key)) { newCount++; } else { updatedCount++; } }); const unchangedCount = existingKeys.size - updatedCount; // Write new .env.local spinner.start('Writing .env.local...'); const header = [ '# ==========================================', '# RIGSTATE SOVEREIGN FOUNDATION', '# Authenticated Environment Configuration', `# Synced at: ${new Date().toISOString()}`, `# Project: ${projectId}`, '# ==========================================', '' ].join('\n'); await fs.writeFile(envFile, header + vaultContent + '\n'); spinner.succeed('Written to .env.local'); if (!silent) { // Summary console.log(''); console.log(chalk.bold.green('✅ Environment synchronized successfully')); console.log(''); console.log(chalk.dim(' Summary:')); console.log(chalk.green(` + ${newCount} new`)); console.log(chalk.yellow(` ~ ${updatedCount} updated`)); console.log(chalk.dim(` = ${unchangedCount} unchanged`)); console.log(''); // Security reminder console.log(chalk.bold.yellow('⚠️ Security Reminder:')); console.log(chalk.dim(' - Never commit .env.local to version control.')); console.log(chalk.dim(' - Ensure .gitignore includes .env.local')); console.log(''); } return true; } catch (e: any) { spinner.fail(chalk.red(`Failed to fetch secrets: ${e.message}`)); return false; } } export function createEnvPullCommand() { const envPull = new Command('env'); envPull .command('pull') .description('Pull environment variables from project vault') .action(async () => { // Get config let apiKey: string; let projectId: string | undefined; try { apiKey = getApiKey(); } catch (e) { console.error(chalk.red('Not authenticated. Run "rigstate login" first.')); return; } // Get project context projectId = getProjectId(); if (!projectId) { const { loadManifest } = await import('../utils/manifest.js'); const manifest = await loadManifest(); if (manifest?.project_id) projectId = manifest.project_id; } if (!projectId) { console.error(chalk.red('No project context. Run "rigstate link" first.')); return; } const apiUrl = getApiUrl(); await syncEnv(projectId, apiKey, apiUrl); }); return envPull; }