embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
276 lines (238 loc) • 7.94 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const logger = require('../utils/logger');
/**
* Write project files to disk
* @param {Array} projectFiles - Array of {path, content} objects
* @param {string} targetDirectory - Directory to write files to
* @param {Object} options - Writing options
* @returns {Promise<Object>} Result of file writing operation
*/
async function writeProjectFiles(projectFiles, targetDirectory = process.cwd(), options = {}) {
const {
overwrite = false,
backup = true,
createDirectories = true
} = options;
const results = {
success: [],
skipped: [],
errors: [],
backups: []
};
logger.step(`Writing ${projectFiles.length} files to ${targetDirectory}`);
for (const file of projectFiles) {
try {
const fullPath = path.join(targetDirectory, file.path);
const directory = path.dirname(fullPath);
// Create directory if it doesn't exist
if (createDirectories) {
await fs.ensureDir(directory);
}
// Check if file exists
const fileExists = await fs.pathExists(fullPath);
if (fileExists && !overwrite) {
if (backup) {
await createBackup(fullPath);
results.backups.push(file.path);
} else {
logger.warn(`File exists, skipping: ${file.path}`);
results.skipped.push(file.path);
continue;
}
}
// Write the file
await fs.writeFile(fullPath, file.content, 'utf8');
logger.debug(`Written: ${file.path}`);
results.success.push(file.path);
} catch (error) {
logger.error(`Failed to write ${file.path}: ${error.message}`);
results.errors.push({
path: file.path,
error: error.message
});
}
}
// Log summary
if (results.success.length > 0) {
logger.success(`Successfully wrote ${results.success.length} files`);
}
if (results.skipped.length > 0) {
logger.warn(`Skipped ${results.skipped.length} existing files`);
}
if (results.backups.length > 0) {
logger.info(`Created backups for ${results.backups.length} files`);
}
if (results.errors.length > 0) {
logger.error(`Failed to write ${results.errors.length} files`);
}
return results;
}
/**
* Create a backup of an existing file
* @param {string} filePath - Path to file to backup
* @returns {Promise<string>} Backup file path
*/
async function createBackup(filePath) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = `${filePath}.backup-${timestamp}`;
try {
await fs.copy(filePath, backupPath);
logger.debug(`Created backup: ${backupPath}`);
return backupPath;
} catch (error) {
logger.warn(`Failed to create backup for ${filePath}: ${error.message}`);
throw error;
}
}
/**
* Merge content with existing package.json
* @param {string} newPackageContent - New package.json content
* @param {string} targetPath - Path to existing package.json
* @returns {Promise<string>} Merged package.json content
*/
async function mergePackageJson(newPackageContent, targetPath) {
try {
const newPackage = JSON.parse(newPackageContent);
let existingPackage = {};
// Try to read existing package.json
try {
const existingContent = await fs.readFile(targetPath, 'utf8');
existingPackage = JSON.parse(existingContent);
} catch (error) {
// If file doesn't exist or can't be parsed, use empty object
logger.debug('No existing package.json found or could not parse');
}
// Merge dependencies
const merged = {
...existingPackage,
...newPackage,
dependencies: {
...existingPackage.dependencies,
...newPackage.dependencies
},
devDependencies: {
...existingPackage.devDependencies,
...newPackage.devDependencies
},
scripts: {
...existingPackage.scripts,
...newPackage.scripts
}
};
return JSON.stringify(merged, null, 2);
} catch (error) {
logger.warn(`Failed to merge package.json: ${error.message}`);
return newPackageContent; // Return new content as fallback
}
}
/**
* Update existing .env.local with new environment variables
* @param {string} newEnvContent - New environment variables
* @param {string} targetPath - Path to existing .env.local
* @returns {Promise<string>} Updated .env.local content
*/
async function mergeEnvFile(newEnvContent, targetPath) {
try {
let existingContent = '';
// Try to read existing .env.local
try {
existingContent = await fs.readFile(targetPath, 'utf8');
} catch (error) {
// File doesn't exist, that's okay
logger.debug('No existing .env.local found');
}
// Parse environment variables
const existingVars = parseEnvContent(existingContent);
const newVars = parseEnvContent(newEnvContent);
// Merge variables (new ones take precedence if they don't exist)
const mergedVars = { ...existingVars };
Object.entries(newVars).forEach(([key, value]) => {
if (!mergedVars[key]) {
mergedVars[key] = value;
logger.debug(`Adding new environment variable: ${key}`);
} else {
logger.debug(`Environment variable ${key} already exists, keeping existing value`);
}
});
// Convert back to .env format
return Object.entries(mergedVars)
.map(([key, value]) => `${key}=${value}`)
.join('\n');
} catch (error) {
logger.warn(`Failed to merge .env.local: ${error.message}`);
return newEnvContent; // Return new content as fallback
}
}
/**
* Parse .env file content into key-value pairs
* @param {string} content - .env file content
* @returns {Object} Key-value pairs
*/
function parseEnvContent(content) {
const vars = {};
content.split('\n').forEach(line => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const [key, ...valueParts] = trimmed.split('=');
if (key && valueParts.length > 0) {
vars[key.trim()] = valueParts.join('=').trim();
}
}
});
return vars;
}
/**
* Smart file writing that handles special files differently
* @param {Array} projectFiles - Project files to write
* @param {string} targetDirectory - Target directory
* @param {Object} projectInfo - Project information
* @returns {Promise<Object>} Write results
*/
async function smartWriteFiles(projectFiles, targetDirectory, projectInfo) {
const results = {
success: [],
merged: [],
skipped: [],
errors: []
};
for (const file of projectFiles) {
try {
const fullPath = path.join(targetDirectory, file.path);
// Handle special files
if (file.path === 'package.json' && projectInfo.hasPackageJson) {
const mergedContent = await mergePackageJson(file.content, fullPath);
await fs.writeFile(fullPath, mergedContent, 'utf8');
results.merged.push(file.path);
logger.info(`Merged package.json with existing dependencies`);
continue;
}
if (file.path === '.env.local') {
const mergedContent = await mergeEnvFile(file.content, fullPath);
await fs.writeFile(fullPath, mergedContent, 'utf8');
results.merged.push(file.path);
logger.info(`Merged .env.local with existing variables`);
continue;
}
// Regular file writing
const directory = path.dirname(fullPath);
await fs.ensureDir(directory);
await fs.writeFile(fullPath, file.content, 'utf8');
results.success.push(file.path);
} catch (error) {
logger.error(`Failed to write ${file.path}: ${error.message}`);
results.errors.push({
path: file.path,
error: error.message
});
}
}
return results;
}
module.exports = {
writeProjectFiles,
createBackup,
mergePackageJson,
mergeEnvFile,
smartWriteFiles
};