envsyncer
Version:
Sync your root .env files into monorepo subfolders with ease
1 lines ⢠11.9 kB
Source Map (JSON)
{"version":3,"sources":["../src/scanner.ts","../src/prompt.ts","../src/sync.ts","../src/index.ts"],"sourcesContent":["import fs from 'fs';\nimport path from 'path';\nimport chalk from 'chalk';\n\nfunction findPackageJsonDirs(dir: string, results: string[] = []): string[] {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n \n // Check if current directory has a package.json\n if (fs.existsSync(path.join(dir, 'package.json'))) {\n // Only add if it's not the root package.json\n if (dir !== process.cwd()) {\n results.push(path.relative(process.cwd(), dir));\n }\n }\n\n // Look in subdirectories\n for (const entry of entries) {\n if (entry.isDirectory()) {\n // Skip node_modules and hidden folders\n if (entry.name === 'node_modules' || entry.name.startsWith('.')) {\n continue;\n }\n \n const fullPath = path.join(dir, entry.name);\n findPackageJsonDirs(fullPath, results);\n }\n }\n\n return results;\n}\n\nexport async function scanTargetDirs(): Promise<string[]> {\n try {\n console.log(chalk.blue('š Scanning for projects in monorepo...'));\n \n const dirs = findPackageJsonDirs(process.cwd());\n\n if (dirs.length === 0) {\n console.log(chalk.yellow('ā ļø No projects found in the monorepo'));\n return [];\n }\n\n console.log(chalk.blue('\\nš Found the following projects:'));\n dirs.forEach((dir) => console.log(chalk.gray(` - ${dir}`)));\n\n return dirs;\n } catch (error) {\n console.error(chalk.red('Error scanning directories:'), error);\n return [];\n }\n}\n","import inquirer from 'inquirer';\nimport chalk from 'chalk';\n\nexport async function promptTargetDirs(dirs: string[]): Promise<string[]> {\n if (dirs.length === 0) return [];\n\n const { selected } = await inquirer.prompt([\n {\n type: 'checkbox',\n name: 'selected',\n message: 'Select folders to sync .env into:',\n choices: dirs.map((dir) => ({\n name: dir,\n checked: true,\n value: dir,\n })),\n validate: (answer) =>\n answer.length > 0 || 'You must choose at least one directory!',\n },\n ]);\n\n console.log(\n chalk.blue('\\nšÆ Selected directories:'),\n selected.map((dir: string) => chalk.gray(dir)).join(', ')\n );\n\n return selected;\n}\n","import fs from 'fs';\nimport path from 'path';\nimport chalk from 'chalk';\n\nconst ENV_FILES = ['.env', '.env.local', '.env.development', '.env.production'];\n\nfunction parseEnvVars(content: string): Set<string> {\n const envVars = new Set<string>();\n const lines = content.split('\\n');\n \n for (const line of lines) {\n const trimmed = line.trim();\n // Skip comments and empty lines\n if (trimmed && !trimmed.startsWith('#')) {\n const equalIndex = trimmed.indexOf('=');\n if (equalIndex > 0) {\n const varName = trimmed.substring(0, equalIndex);\n envVars.add(varName);\n }\n }\n }\n \n return envVars;\n}\n\nfunction extractLocalEnvVars(localContent: string, rootEnvVars: Set<string>): string[] {\n const lines = localContent.split('\\n');\n const localEnvLines: string[] = [];\n \n for (const line of lines) {\n const trimmed = line.trim();\n \n // Keep comments and empty lines that are not at the start\n if (!trimmed || trimmed.startsWith('#')) {\n // Only keep local comments if they're not environment variable comments\n if (localEnvLines.length > 0) {\n localEnvLines.push(line);\n }\n continue;\n }\n \n const equalIndex = trimmed.indexOf('=');\n if (equalIndex > 0) {\n const varName = trimmed.substring(0, equalIndex);\n // Only keep local env vars that are not in root\n if (!rootEnvVars.has(varName)) {\n localEnvLines.push(line);\n }\n }\n }\n \n // Remove trailing empty lines\n while (localEnvLines.length > 0 && !localEnvLines[localEnvLines.length - 1].trim()) {\n localEnvLines.pop();\n }\n \n return localEnvLines;\n}\n\nexport async function syncEnvToTargets(targetDirs: string[]) {\n const rootEnvFiles = ENV_FILES.filter((file) =>\n fs.existsSync(path.join(process.cwd(), file))\n );\n\n if (rootEnvFiles.length === 0) {\n console.log(chalk.red('ā No .env files found in root directory.'));\n console.log(\n chalk.gray('Looked for:', ENV_FILES.join(', '))\n );\n return;\n }\n\n console.log(chalk.blue('\\nš Found env files:'), rootEnvFiles.join(', '));\n\n for (const envFile of rootEnvFiles) {\n const rootEnvPath = path.join(process.cwd(), envFile);\n const rootEnvContent = fs.readFileSync(rootEnvPath, 'utf-8');\n const rootEnvVars = parseEnvVars(rootEnvContent);\n\n for (const dir of targetDirs) {\n const targetPath = path.join(process.cwd(), dir, envFile);\n \n try {\n let localEnvLines: string[] = [];\n \n // Backup local env vars if the file exists\n if (fs.existsSync(targetPath)) {\n const localEnvContent = fs.readFileSync(targetPath, 'utf-8');\n localEnvLines = extractLocalEnvVars(localEnvContent, rootEnvVars);\n }\n \n // Start with root env content (preserves all formatting and comments)\n let finalContent = rootEnvContent;\n \n // Add local env vars if any exist\n if (localEnvLines.length > 0) {\n // Ensure root content ends with a newline\n if (!finalContent.endsWith('\\n')) {\n finalContent += '\\n';\n }\n \n // Add separator comment\n finalContent += '\\n# Local environment variables\\n';\n finalContent += localEnvLines.join('\\n');\n \n // Ensure final content ends with a newline\n if (!finalContent.endsWith('\\n')) {\n finalContent += '\\n';\n }\n }\n \n fs.writeFileSync(targetPath, finalContent);\n \n const localCount = localEnvLines.filter(line => line.trim() && !line.trim().startsWith('#')).length;\n const message = localCount > 0 \n ? `ā
Synced ${envFile} to ${chalk.gray(`${dir}/${envFile}`)} ${chalk.cyan(`(preserved ${localCount} local vars)`)}`\n : `ā
Synced ${envFile} to ${chalk.gray(`${dir}/${envFile}`)}`;\n \n console.log(chalk.green(message));\n } catch (error) {\n console.error(\n chalk.red(`ā Failed to sync ${envFile} to ${dir}:`),\n error\n );\n }\n }\n }\n}\n","#!/usr/bin/env node\n\nimport { scanTargetDirs } from './scanner';\nimport { promptTargetDirs } from './prompt';\nimport { syncEnvToTargets } from './sync';\nimport chalk from 'chalk';\n\nasync function main() {\n console.log(chalk.green.bold('\\nš± EnvSync ā syncing envs into your monorepo like magic\\n'));\n\n const dirs = await scanTargetDirs();\n const selectedDirs = await promptTargetDirs(dirs);\n\n if (!selectedDirs.length) {\n console.log(chalk.yellow('No folders selected. Exiting.'));\n process.exit(0);\n }\n\n await syncEnvToTargets(selectedDirs);\n\n console.log(chalk.green('\\nā
Done syncing env files!\\n'));\n}\n\nmain().catch((err) => {\n console.error(chalk.red('ā Something went wrong:'), err);\n process.exit(1);\n});\n"],"mappings":"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","names":["fs","path","chalk","findPackageJsonDirs","dir","results","entries","entry","fullPath","scanTargetDirs","dirs","error","inquirer","chalk","promptTargetDirs","dirs","selected","dir","answer","fs","path","chalk","ENV_FILES","parseEnvVars","content","envVars","lines","line","trimmed","equalIndex","varName","extractLocalEnvVars","localContent","rootEnvVars","localEnvLines","syncEnvToTargets","targetDirs","rootEnvFiles","file","envFile","rootEnvPath","rootEnvContent","dir","targetPath","localEnvContent","finalContent","localCount","message","error","chalk","main","dirs","scanTargetDirs","selectedDirs","promptTargetDirs","syncEnvToTargets","err"]}