embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
378 lines (315 loc) • 10.7 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const logger = require('../utils/logger');
/**
* EnvironmentManager - Handles environment variables and API key setup
* Fixes the issue where users must manually create .env.local and add API keys
*/
class EnvironmentManager {
constructor() {
this.environmentResults = {
created: [],
updated: [],
warnings: []
};
}
/**
* Setup environment variables for the project
* @param {string} projectDirectory - Project directory
* @param {Object} options - Setup options
* @returns {Promise<Object>} Environment setup results
*/
async setupEnvironment(projectDirectory, options = {}) {
try {
logger.info('🔑 Setting up environment variables...');
// 1. Create or update .env.local
await this.createEnvLocal(projectDirectory, options.apiKey, options.aiProvider);
// 2. Create .env.example for reference
await this.createEnvExample(projectDirectory, options.aiProvider);
// 3. Update .gitignore to exclude .env.local
await this.updateGitignore(projectDirectory);
// 4. Validate environment setup
const validation = await this.validateEnvironment(projectDirectory);
logger.success('✅ Environment setup completed');
return {
...this.environmentResults,
validation
};
} catch (error) {
throw new Error(`Environment setup failed: ${error.message}`);
}
}
/**
* Create or update .env.local file
*/
async createEnvLocal(projectDirectory, apiKey = null, aiProvider = 'gemini') {
const envLocalPath = path.join(projectDirectory, '.env.local');
const timestamp = new Date().toISOString();
// Read existing .env.local if it exists
let existingEnv = '';
if (await fs.pathExists(envLocalPath)) {
existingEnv = await fs.readFile(envLocalPath, 'utf8');
}
// Parse existing environment variables
const existingVars = this.parseEnvContent(existingEnv);
// Define required environment variables based on AI provider
const requiredVars = {
'NEXT_PUBLIC_CHATBOT_ENABLED': 'true'
};
// Add provider-specific API key
if (aiProvider === 'gemini') {
requiredVars['GEMINI_API_KEY'] = apiKey || 'your_gemini_api_key_here';
} else if (aiProvider === 'openai') {
requiredVars['OPENAI_API_KEY'] = apiKey || 'your_openai_api_key_here';
}
// Merge with existing variables (don't overwrite existing values)
const mergedVars = { ...requiredVars, ...existingVars };
// Generate new .env.local content
const envContent = this.generateEnvContent(mergedVars, timestamp);
// Write to file
await fs.writeFile(envLocalPath, envContent, 'utf8');
if (existingEnv) {
this.environmentResults.updated.push('.env.local');
logger.success('📝 Updated .env.local with new variables');
} else {
this.environmentResults.created.push('.env.local');
logger.success('📝 Created .env.local file');
}
// Show API key instructions if no key provided
if (!apiKey || apiKey === 'your_gemini_api_key_here') {
this.environmentResults.warnings.push({
type: 'api_key_needed',
message: 'Please add your Gemini API key to .env.local',
instructions: [
'1. Visit https://makersuite.google.com/app/apikey',
'2. Create a new API key',
'3. Replace "your_gemini_api_key_here" in .env.local with your actual key'
]
});
}
}
/**
* Create .env.example file for reference
*/
async createEnvExample(projectDirectory) {
const envExamplePath = path.join(projectDirectory, '.env.example');
// Don't overwrite existing .env.example
if (await fs.pathExists(envExamplePath)) {
return;
}
const exampleContent = `# Embedia Chatbot Configuration
# Copy this file to .env.local and add your actual API keys
# Gemini API Key (required)
# Get your key from: https://makersuite.google.com/app/apikey
GEMINI_API_KEY=your_gemini_api_key_here
# Chatbot Feature Toggle
NEXT_PUBLIC_CHATBOT_ENABLED=true
# Optional: Custom API endpoint
# CHATBOT_API_ENDPOINT=/api/chat
`;
await fs.writeFile(envExamplePath, exampleContent, 'utf8');
this.environmentResults.created.push('.env.example');
logger.success('📄 Created .env.example file');
}
/**
* Update .gitignore to exclude .env.local
*/
async updateGitignore(projectDirectory) {
const gitignorePath = path.join(projectDirectory, '.gitignore');
let gitignoreContent = '';
if (await fs.pathExists(gitignorePath)) {
gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
}
// Check if .env.local is already ignored
if (gitignoreContent.includes('.env.local')) {
return;
}
// Add .env.local to .gitignore
const envSection = `
# Environment variables
.env.local
.env.*.local
`;
const updatedContent = gitignoreContent + envSection;
await fs.writeFile(gitignorePath, updatedContent, 'utf8');
if (gitignoreContent) {
this.environmentResults.updated.push('.gitignore');
} else {
this.environmentResults.created.push('.gitignore');
}
logger.success('🔒 Updated .gitignore to exclude environment files');
}
/**
* Parse environment file content
*/
parseEnvContent(content) {
const vars = {};
content.split('\n').forEach(line => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') && trimmed.includes('=')) {
const [key, ...valueParts] = trimmed.split('=');
if (key && valueParts.length > 0) {
vars[key.trim()] = valueParts.join('=').trim();
}
}
});
return vars;
}
/**
* Generate environment file content
*/
generateEnvContent(vars, timestamp) {
const lines = [
'# Environment variables for Embedia Chatbot',
`# Last updated: ${timestamp}`,
'',
'# Gemini API Key (required for chatbot functionality)',
'# Get your key from: https://makersuite.google.com/app/apikey'
];
// Add variables
Object.entries(vars).forEach(([key, value]) => {
if (key === 'GEMINI_API_KEY') {
lines.push(`${key}=${value}`);
lines.push('');
} else if (key.startsWith('NEXT_PUBLIC_')) {
if (!lines.includes('# Public environment variables')) {
lines.push('# Public environment variables');
}
lines.push(`${key}=${value}`);
} else {
lines.push(`${key}=${value}`);
}
});
return lines.join('\n') + '\n';
}
/**
* Validate environment setup
*/
async validateEnvironment(projectDirectory) {
const validation = {
hasEnvLocal: false,
hasApiKey: false,
apiKeyValid: false,
publicVarsSet: false,
issues: []
};
const envLocalPath = path.join(projectDirectory, '.env.local');
// Check if .env.local exists
if (await fs.pathExists(envLocalPath)) {
validation.hasEnvLocal = true;
const envContent = await fs.readFile(envLocalPath, 'utf8');
const vars = this.parseEnvContent(envContent);
// Check for API key
if (vars.GEMINI_API_KEY && vars.GEMINI_API_KEY !== 'your_gemini_api_key_here') {
validation.hasApiKey = true;
// Basic API key format validation
if (vars.GEMINI_API_KEY.length > 10 && !vars.GEMINI_API_KEY.includes('your_')) {
validation.apiKeyValid = true;
}
}
// Check public variables
if (vars.NEXT_PUBLIC_CHATBOT_ENABLED) {
validation.publicVarsSet = true;
}
} else {
validation.issues.push('.env.local file not found');
}
// Generate issues list
if (!validation.hasApiKey) {
validation.issues.push('Gemini API key not set or using placeholder value');
}
if (!validation.apiKeyValid && validation.hasApiKey) {
validation.issues.push('API key appears to be invalid format');
}
return validation;
}
/**
* Prompt user for API key (interactive mode)
*/
async promptForApiKey() {
// This would be implemented with a CLI prompt library
// For now, return instructions
return {
instructions: [
'🔑 API Key Setup Required:',
'',
'1. Visit https://makersuite.google.com/app/apikey',
'2. Click "Create API Key"',
'3. Copy the generated key',
'4. Add it to your .env.local file:',
' GEMINI_API_KEY=your_actual_api_key_here',
'',
'⚠️ Keep your API key secure and never commit it to version control!'
]
};
}
/**
* Test API key validity
*/
async testApiKey(apiKey) {
if (!apiKey || apiKey === 'your_gemini_api_key_here') {
return {
valid: false,
error: 'No API key provided or using placeholder'
};
}
try {
// Basic format validation
if (apiKey.length < 10) {
return {
valid: false,
error: 'API key appears too short'
};
}
// Note: We could make an actual API call here to test the key
// but that would require the @google/genai package
return {
valid: true,
message: 'API key format appears valid'
};
} catch (error) {
return {
valid: false,
error: `API key validation failed: ${error.message}`
};
}
}
/**
* Get environment setup summary
*/
getSummary() {
const { created, updated, warnings } = this.environmentResults;
return {
filesCreated: created.length,
filesUpdated: updated.length,
warningsCount: warnings.length,
details: {
created,
updated,
warnings
}
};
}
/**
* Generate next steps for user
*/
getNextSteps(validation) {
const steps = [];
if (!validation.hasEnvLocal) {
steps.push('❌ Create .env.local file with environment variables');
}
if (!validation.hasApiKey) {
steps.push('🔑 Add your Gemini API key to .env.local');
steps.push(' → Visit https://makersuite.google.com/app/apikey');
}
if (validation.hasApiKey && !validation.apiKeyValid) {
steps.push('⚠️ Verify your API key format is correct');
}
if (validation.issues.length === 0) {
steps.push('✅ Environment setup is complete!');
steps.push('🚀 You can now run: npm run dev');
}
return steps;
}
}
module.exports = EnvironmentManager;