Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/commands/set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -17,6 +17,7 @@ export function setCommand(program: Command): void {
.option('-c, --config <path>', 'Path to config file (default: ./envx.config.yaml)', './envx.config.yaml')
.option('-d, --description <text>', 'Description for the environment variable')
.option('-t, --target <path>', 'Target path for the environment variable')
.option('--files <paths>', '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 {
Expand Down Expand Up @@ -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();
Expand All @@ -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(
Expand Down Expand Up @@ -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)) {
Expand Down
79 changes: 65 additions & 14 deletions src/utils/com.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ import { dirname, isAbsolute, join } from 'path';
export async function getEnvs(configPath: string, tag?: string): Promise<EnvMap> {
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 {
Expand All @@ -27,23 +26,48 @@ export async function getEnvs(configPath: string, tag?: string): Promise<EnvMap>
}
}

// 合并 .env 文件内容
for (const file of files) {
// 收集所有需要读取的文件
const allFiles = new Set<string>(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<string, EnvMap> = {};
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);
}

// 只保留配置里声明过的键
Expand All @@ -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<string, EnvMap> = {};
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);
}
}
21 changes: 14 additions & 7 deletions src/utils/config/config-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 中声明`
);
}
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/utils/env/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -65,6 +66,8 @@ export async function readEnvFile(filePath: string): Promise<EnvMap> {
*/
export async function writeEnvFile(filePath: string, env: EnvMap): Promise<void> {
const output = serializeEnv(env);
const dir = dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, output, 'utf8');
}

Expand Down
Loading