diff --git a/src/commands/set.ts b/src/commands/set.ts index a3dff37..87d5218 100644 --- a/src/commands/set.ts +++ b/src/commands/set.ts @@ -3,7 +3,7 @@ import chalk from 'chalk'; import { existsSync } from 'fs'; import { join } from 'path'; import { ConfigManager } from '@/utils/config'; -import { saveEnvs, writeEnvs } from '@/utils/com'; +import { getEnvs, saveEnvs, writeEnvs } from '@/utils/com'; import { EnvConfig } from '@/types/config'; import { validateEnvKey, @@ -17,6 +17,7 @@ export function setCommand(program: Command): void { .option('-c, --config ', 'Path to config file (default: ./envx.config.yaml)', './envx.config.yaml') .option('-d, --description ', 'Description for the environment variable') .option('-t, --target ', 'Target path for the environment variable') + .option('--files ', 'Target file paths for monorepo (comma-separated, e.g. apps/web/.env,apps/api/.env)') .option('--force', 'Force update without confirmation if variable exists') .action(async (key: string, value: string, options) => { try { @@ -81,6 +82,11 @@ export function setCommand(program: Command): void { envConfig.target = options.target; } + if (options.files) { + const filesList = options.files.split(',').map((f: string) => f.trim()); + envConfig.files = filesList.length === 1 ? filesList[0] : filesList; + } + // 更新配置 configManager.setEnvVar(key, envConfig); configManager.save(); @@ -89,11 +95,13 @@ export function setCommand(program: Command): void { console.log(chalk.blue('🗄️ Updating database...')); await saveEnvs(configPath, { [key]: value }, 'default'); - // 写入环境文件(writeEnvs) + // 写入环境文件(writeEnvs)— 读取完整环境后合并写入,避免覆盖其他变量 if (config.files) { console.log(chalk.blue('🔄 Updating environment file based on clone configuration...')); try { - await writeEnvs(configPath, { [key]: value }); + const currentEnvs = await getEnvs(configPath); + currentEnvs[key] = value; + await writeEnvs(configPath, currentEnvs); console.log(chalk.green('✅ Environment file updated')); } catch (error) { console.warn( @@ -121,6 +129,10 @@ export function setCommand(program: Command): void { if (options.target) { console.log(chalk.gray(` Target: ${options.target}`)); } + + if (options.files) { + console.log(chalk.gray(` Files: ${options.files}`)); + } // 显示配置相关信息 if (isEnvRequired(key, config)) { diff --git a/src/utils/com.ts b/src/utils/com.ts index 279d508..c61ffda 100644 --- a/src/utils/com.ts +++ b/src/utils/com.ts @@ -7,16 +7,15 @@ import { dirname, isAbsolute, join } from 'path'; export async function getEnvs(configPath: string, tag?: string): Promise { const configManager = new ConfigManager(configPath); const isExport = configManager.isExport(); - const files = configManager.getEnvFilesConfig(); + const globalFiles = configManager.getEnvFilesConfig(); + const configDir = dirname(configPath); // 允许键集合来自配置文件定义 const configKeys = Object.keys(configManager.getConfig().env); + const allConfigs = configManager.getAllEnvConfigs(); let envMap: EnvMap = {}; - // 数据库目录应为配置文件所在目录 - const configDir = dirname(configPath); - if (tag) { const dbManager = createDatabaseManagerFromConfigPath(configPath); try { @@ -27,23 +26,48 @@ export async function getEnvs(configPath: string, tag?: string): Promise } } - // 合并 .env 文件内容 - for (const file of files) { + // 收集所有需要读取的文件 + const allFiles = new Set(globalFiles); + for (const conf of allConfigs) { + if (conf.config.files) { + const files = Array.isArray(conf.config.files) ? conf.config.files : [conf.config.files]; + files.forEach(f => allFiles.add(f)); + } + } + + // 读取所有相关文件到缓存 + const fileContents: Record = {}; + for (const file of allFiles) { const filePath = isAbsolute(file) ? file : join(configDir, file); - const fileEnvs = await readEnvFile(filePath); - envMap = mergeEnv(envMap, fileEnvs, true); + fileContents[file] = await readEnvFile(filePath); + } + + // 按变量从其配置的目标文件中读取值 + for (const conf of allConfigs) { + const key = conf.key; + let targetFiles: string[]; + if (conf.config.files) { + targetFiles = Array.isArray(conf.config.files) ? conf.config.files : [conf.config.files]; + } else { + targetFiles = globalFiles; + } + + for (const file of targetFiles) { + const fileEnv = fileContents[file]; + if (fileEnv && key in fileEnv) { + envMap[key] = fileEnv[key]; + } + } } // 如果开启 export,则从当前进程环境中读取并合并(仅限已定义键) if (isExport) { - const processingEnv: EnvMap = {}; for (const key of configKeys) { const val = process.env[key]; if (val !== undefined) { - processingEnv[key] = String(val); + envMap[key] = String(val); } } - envMap = mergeEnv(envMap, processingEnv, true); } // 只保留配置里声明过的键 @@ -68,10 +92,37 @@ export async function saveEnvs(configPath: string, envMap: EnvMap, tag?: string) export async function writeEnvs(configPath: string, envMap: EnvMap) { const configManager = new ConfigManager(configPath); - const files = configManager.getEnvFilesConfig(); + const globalFiles = configManager.getEnvFilesConfig(); const configDir = dirname(configPath); - for (const file of files) { + const allConfigs = configManager.getAllEnvConfigs(); + + // 按目标文件分组变量 + const fileEnvMap: Record = {}; + for (const file of globalFiles) { + fileEnvMap[file] = {}; + } + + for (const [key, value] of Object.entries(envMap)) { + const envConf = allConfigs.find(c => c.key === key); + let targetFiles: string[]; + + if (envConf?.config.files) { + targetFiles = Array.isArray(envConf.config.files) + ? envConf.config.files + : [envConf.config.files]; + } else { + targetFiles = globalFiles; // fallback 到全局 files + } + + for (const file of targetFiles) { + if (!fileEnvMap[file]) fileEnvMap[file] = {}; + fileEnvMap[file][key] = value; + } + } + + // 按文件写入 + for (const [file, envs] of Object.entries(fileEnvMap)) { const filePath = isAbsolute(file) ? file : join(configDir, file); - await writeEnvFile(filePath, envMap); + await writeEnvFile(filePath, envs); } } diff --git a/src/utils/config/config-validator.ts b/src/utils/config/config-validator.ts index 8086003..165a76e 100644 --- a/src/utils/config/config-validator.ts +++ b/src/utils/config/config-validator.ts @@ -151,13 +151,20 @@ export class ConfigValidator { } } - // 检查路径冲突(仅在全局与局部都为字符串时检查完全相等) - if ( - typeof config.files === 'string' && - typeof envConfig.files === 'string' && - envConfig.files === config.files - ) { - warnings.push(`环境变量 "${key}" 的 clone 路径与全局 clone 路径相同,可能造成冲突`); + // 检查 per-variable files 是否为全局 files 的子集 + const globalFilesList = config.files + ? (Array.isArray(config.files) ? config.files : [config.files]) + : []; + const varFilesList = Array.isArray(envConfig.files) + ? envConfig.files + : [envConfig.files]; + + for (const vf of varFilesList) { + if (globalFilesList.length > 0 && !globalFilesList.includes(vf)) { + warnings.push( + `环境变量 "${key}" 的目标文件 "${vf}" 未在全局 files 中声明` + ); + } } } } diff --git a/src/utils/env/index.ts b/src/utils/env/index.ts index b094e96..cc7aea8 100644 --- a/src/utils/env/index.ts +++ b/src/utils/env/index.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { dirname } from 'path'; import { EnvxConfig } from '@/types/config'; import { EnvMap, ShellKind } from '@/types/common'; import { spawn } from 'child_process'; @@ -65,6 +66,8 @@ export async function readEnvFile(filePath: string): Promise { */ export async function writeEnvFile(filePath: string, env: EnvMap): Promise { const output = serializeEnv(env); + const dir = dirname(filePath); + await fs.mkdir(dir, { recursive: true }); await fs.writeFile(filePath, output, 'utf8'); }