git-aiflow
Version:
🚀 An AI-powered workflow automation tool for effortless Git-based development, combining smart GitLab/GitHub merge & pull request creation with Conan package management.
1,172 lines (1,122 loc) • 52 kB
JavaScript
import fs from 'fs';
import path from 'path';
import os from 'os';
import yaml from 'js-yaml';
import readline from 'readline';
import { config as dotenvConfig } from 'dotenv';
import { fileURLToPath } from 'url';
import { logger } from './logger.js';
/**
* Get cross-platform user data directory for global config
*/
function getUserDataDir() {
const platform = os.platform();
const homeDir = os.homedir();
switch (platform) {
case 'win32':
return process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
case 'darwin':
return path.join(homeDir, 'Library', 'Application Support');
default:
return process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config');
}
}
/**
* ESM/CommonJS compatibility helper for getting current directory.
* @return {string} The current directory path
*/
function getDirname() {
// ESM environment
if (typeof import.meta !== 'undefined' && import.meta.url) {
return path.dirname(fileURLToPath(import.meta.url));
}
// CommonJS environment
if (typeof __dirname !== 'undefined') {
return __dirname;
}
// Fallback
return process.cwd();
}
/**
* Load environment variables with ESM/CommonJS compatibility.
* Searches for .env file in current directory, parent directory, and working directory.
* This function should be called early in the application lifecycle.
*/
export function loadEnvironmentVariables() {
const currentDir = getDirname();
let envPath = path.join(currentDir, '.env');
if (!fs.existsSync(envPath)) {
envPath = path.join(currentDir, '../.env');
}
if (!fs.existsSync(envPath)) {
envPath = path.join(process.cwd(), '.env');
}
if (fs.existsSync(envPath)) {
dotenvConfig({ path: envPath, debug: false, quiet: true });
}
else {
// Fallback to default dotenv behavior
dotenvConfig({ debug: false, quiet: true });
}
}
/**
* Configuration loader with priority-based merging
* Priority order: CLI args > Local config > Global config > Environment variables
*/
export class ConfigLoader {
constructor() {
this.warnings = [];
}
/**
* Load configuration with priority merging
*/
async loadConfig(cliArgs = {}) {
const config = { _sources: new Map() };
// Load from environment variables (lowest priority)
this.mergeEnvConfig(config);
// Load from global config file
await this.mergeGlobalConfig(config);
// Load from local config file
await this.mergeLocalConfig(config);
// Apply CLI arguments (highest priority)
this.mergeCliConfig(config, cliArgs);
// Validate and warn about missing required configs
this.validateConfig(config);
return config;
}
/**
* Get configuration warnings
*/
getWarnings() {
return [...this.warnings];
}
/**
* Clear warnings
*/
clearWarnings() {
this.warnings.length = 0;
}
/**
* Get user data directory path
*/
getUserDataDir() {
return getUserDataDir();
}
/**
* Merge environment variables into config
*/
mergeEnvConfig(config) {
// Initialize environment variables
loadEnvironmentVariables();
const envMapping = {
'OPENAI_KEY': 'openai.key',
'OPENAI_BASE_URL': 'openai.baseUrl',
'OPENAI_MODEL': 'openai.model',
'OPENAI_REASONING': 'openai.reasoning',
'CONAN_REMOTE_BASE_URL': 'conan.remoteBaseUrl',
'CONAN_REMOTE_REPO': 'conan.remoteRepo',
'WECOM_WEBHOOK': 'wecom.webhook',
'WECOM_ENABLE': 'wecom.enable',
'SQUASH_COMMITS': 'git.squashCommits',
'REMOVE_SOURCE_BRANCH': 'git.removeSourceBranch',
'GIT_GENERATION_LANG': 'git.generation_lang',
'MERGE_REQUEST_ASSIGNEE_ID': 'merge_request.assignee_id',
'MERGE_REQUEST_ASSIGNEE_IDS': 'merge_request.assignee_ids',
'MERGE_REQUEST_REVIEWER_IDS': 'merge_request.reviewer_ids',
};
// Handle git access token environment variables
for (const [envKey, envValue] of Object.entries(process.env)) {
if (envKey.startsWith('GIT_ACCESS_TOKEN_') && envValue) {
const hostname = envKey.replace('GIT_ACCESS_TOKEN_', '').toLowerCase().replace(/_/g, '.');
const configPath = `git_access_tokens.${hostname}`;
this.setNestedValue(config, configPath, envValue);
config._sources.set(configPath, { source: 'env' });
}
}
for (const [envKey, configPath] of Object.entries(envMapping)) {
const envValue = process.env[envKey];
if (envValue !== undefined) {
let parsedValue = this.parseEnvValue(envValue);
// Handle array fields for merge request configuration
if (configPath === 'merge_request.assignee_ids' || configPath === 'merge_request.reviewer_ids') {
if (typeof parsedValue === 'string') {
// Parse comma-separated string to number array
parsedValue = parsedValue.split(',').map(id => {
const num = parseInt(id.trim(), 10);
return isNaN(num) ? 0 : num;
}).filter(id => id >= 0);
}
}
else if (configPath === 'merge_request.assignee_id') {
if (typeof parsedValue === 'string') {
const num = parseInt(parsedValue, 10);
parsedValue = isNaN(num) ? 0 : num;
}
}
this.setNestedValue(config, configPath, parsedValue);
config._sources.set(configPath, { source: 'env' });
}
}
}
/**
* Parse environment variable value
*/
parseEnvValue(value) {
// Handle boolean values
if (value.toLowerCase() === 'true')
return true;
if (value.toLowerCase() === 'false')
return false;
// Return as string for other values
return value;
}
/**
* Merge global config file into config
*/
async mergeGlobalConfig(config) {
const globalConfigPath = path.join(this.getUserDataDir(), ConfigLoader.GLOBAL_CONFIG_DIR, ConfigLoader.GLOBAL_CONFIG_FILE);
await this.mergeYamlConfig(config, globalConfigPath, 'global');
}
/**
* Merge local config file into config
*/
async mergeLocalConfig(config) {
const localConfigPath = path.join(process.cwd(), ConfigLoader.LOCAL_CONFIG_PATH);
await this.mergeYamlConfig(config, localConfigPath, 'local');
}
/**
* Merge YAML config file into config
*/
async mergeYamlConfig(config, configPath, source) {
try {
if (!fs.existsSync(configPath)) {
return;
}
const yamlContent = fs.readFileSync(configPath, 'utf8');
const yamlConfig = yaml.load(yamlContent);
if (yamlConfig && typeof yamlConfig === 'object') {
this.mergeConfigRecursively(config, yamlConfig, source, configPath);
}
}
catch (error) {
this.warnings.push(`Failed to load ${source} config from ${configPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Merge CLI arguments into config
*/
mergeCliConfig(config, cliArgs) {
this.mergeConfigRecursively(config, cliArgs, 'cli');
}
/**
* Recursively merge config objects
*/
mergeConfigRecursively(target, source, sourceType, sourcePath, keyPrefix = '') {
for (const [key, value] of Object.entries(source)) {
if (key === '_sources')
continue;
const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key;
if (value !== undefined && value !== null) {
if (typeof value === 'object' && !Array.isArray(value)) {
// Ensure the nested object exists
if (!target[key]) {
target[key] = {};
}
// Recursively merge nested objects
for (const [nestedKey, nestedValue] of Object.entries(value)) {
if (nestedValue !== undefined && nestedValue !== null) {
const nestedTarget = target[key];
nestedTarget[nestedKey] = nestedValue;
target._sources.set(`${key}.${nestedKey}`, { source: sourceType, path: sourcePath });
}
}
}
else {
target[key] = value;
target._sources.set(fullKey, { source: sourceType, path: sourcePath });
}
}
}
}
/**
* Set nested value in object using dot notation
*/
setNestedValue(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current) || typeof current[key] !== 'object') {
current[key] = {};
}
current = current[key];
}
current[keys[keys.length - 1]] = value;
}
/**
* Validate configuration and generate warnings
*/
validateConfig(config) {
const requiredConfigs = [
{ path: 'openai.key', name: 'OPENAI_KEY', description: 'OpenAI API key for AI-powered features' },
{ path: 'openai.baseUrl', name: 'OPENAI_BASE_URL', description: 'OpenAI API base URL for API requests' },
{ path: 'openai.model', name: 'OPENAI_MODEL', description: 'OpenAI model name for AI operations' },
];
const optionalConfigs = [
{ path: 'conan.remoteBaseUrl', name: 'CONAN_REMOTE_BASE_URL', description: 'Conan remote base URL (required for conan operations)' },
{ path: 'conan.remoteRepo', name: 'CONAN_REMOTE_REPO', description: 'Conan remote repository name (optional)' },
{ path: 'wecom.webhook', name: 'WECOM_WEBHOOK', description: 'WeChat Work webhook URL (optional)' },
{ path: 'wecom.enable', name: 'WECOM_ENABLE', description: 'WeChat Work notifications enable flag (optional)' },
];
// Check if at least one git access token is configured
const gitTokens = this.getNestedValue(config, 'git_access_tokens');
if (!gitTokens || Object.keys(gitTokens).length === 0) {
this.warnings.push(`⚠️ No Git access tokens configured. Please configure at least one token for Git operations`);
}
// Check required configurations
for (const { path, name, description } of requiredConfigs) {
if (!this.getNestedValue(config, path)) {
this.warnings.push(`⚠️ Missing required configuration: ${name} - ${description}`);
}
}
// Report missing optional configurations
for (const { path, name, description } of optionalConfigs) {
if (!this.getNestedValue(config, path)) {
logger.info(`ℹ️ Optional configuration not set: ${name} - ${description}`);
}
}
}
/**
* Get nested value from object using dot notation
*/
getNestedValue(obj, path) {
const keys = path.split('.');
let current = obj;
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key];
}
else {
return undefined;
}
}
return current;
}
/**
* Print configuration sources for debugging
*/
printConfigSources(config) {
logger.info('\n📋 Configuration Sources:');
for (const [key, source] of config._sources.entries()) {
const sourceName = source.source.toUpperCase();
const sourcePath = source.path ? ` (${source.path})` : '';
logger.info(` ${key}: ${sourceName}${sourcePath}`);
}
}
/**
* Create example configuration files
*/
async createExampleConfigs() {
const exampleConfig = {
openai: {
key: 'your-openai-api-key',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-3.5-turbo',
reasoning: false,
},
git_access_tokens: {
'github.com': 'your-github-access-token',
'gitlab.example.com': 'your-gitlab-access-token',
'gitee.com': 'your-gitee-access-token',
},
conan: {
remoteBaseUrl: 'https://conan.example.com',
remoteRepo: 'repo',
},
wecom: {
webhook: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key',
enable: true,
},
git: {
squashCommits: true,
removeSourceBranch: true,
},
merge_request: {
assignee_id: 0,
assignee_ids: [],
reviewer_ids: [],
},
};
// Create local example config
const localConfigDir = path.join(process.cwd(), '.aiflow');
const localConfigPath = path.join(localConfigDir, 'config.example.yaml');
if (!fs.existsSync(localConfigDir)) {
fs.mkdirSync(localConfigDir, { recursive: true });
}
// Generate YAML with detailed comments
const yamlContent = `# AIFlow 配置文件
# 这是一个示例配置文件,复制到 config.yaml 并根据需要修改
# 配置优先级: 命令行参数 > 本地配置(.aiflow/config.yaml) > 全局配置(~/.config/aiflow/config.yaml) > 环境变量
# OpenAI API 配置 - 用于AI驱动的功能
openai:
# OpenAI API 密钥 (必需) - 用于生成提交信息和代码分析
key: ${exampleConfig.openai?.key}
# OpenAI API 基础URL (必需) - API请求的端点地址
baseUrl: ${exampleConfig.openai?.baseUrl}
# OpenAI 模型名称 (必需) - 指定使用的AI模型,如 gpt-3.5-turbo, gpt-4
model: ${exampleConfig.openai?.model}
# 启用推理模式 (可选) - 对于o1等推理模型,可以启用更深度的思考模式
reasoning: ${exampleConfig.openai?.reasoning}
# Git 访问令牌配置 - 支持多个Git托管平台
git_access_tokens:
# GitHub 访问令牌 - 格式: ghp_xxxxxxxxxxxxxxxxxxxx
github.com: ${exampleConfig.git_access_tokens?.['github.com']}
# GitLab 访问令牌 - 格式: glpat-xxxxxxxxxxxxxxxxxxxx
gitlab.example.com: ${exampleConfig.git_access_tokens?.['gitlab.example.com']}
# Gitee 访问令牌 - 格式: gitee_xxxxxxxxxxxxxxxxxxxx
gitee.com: ${exampleConfig.git_access_tokens?.['gitee.com']}
# 您可以添加更多Git托管平台的令牌
# 格式: 主机名: 访问令牌
# Conan 包管理器配置 - 用于C++包管理和版本更新
conan:
# Conan 远程仓库基础URL (Conan操作时必需) - Conan包仓库的API地址
remoteBaseUrl: ${exampleConfig.conan?.remoteBaseUrl}
# Conan 远程仓库名称 (可选) - 默认使用的仓库名称,默认为'repo'
remoteRepo: ${exampleConfig.conan?.remoteRepo}
# 企业微信通知配置 - 用于发送操作结果通知
wecom:
# 启用企业微信通知 (可选) - 是否开启通知功能,默认为false
enable: ${exampleConfig.wecom?.enable}
# 企业微信机器人Webhook地址 (可选) - 用于发送通知消息的机器人地址
webhook: ${exampleConfig.wecom?.webhook}
# Git 合并请求配置 - 控制MR的默认行为
git:
# 压缩提交 (可选) - 合并时是否将多个提交压缩为一个,默认为true
squashCommits: ${exampleConfig.git?.squashCommits}
# 删除源分支 (可选) - 合并后是否删除源分支,默认为true
removeSourceBranch: ${exampleConfig.git?.removeSourceBranch}
# 合并请求指派配置 - 配置指派人和审查者
merge_request:
# 单个指派人用户ID (可选) - 设置为0或留空取消指派
assignee_id: ${exampleConfig.merge_request?.assignee_id || 0}
# 指派人用户ID数组 (可选) - 多个指派人,设置为空数组取消所有指派
assignee_ids: []
# 审查者用户ID数组 (可选) - 设置为空数组不添加审查者
reviewer_ids: []
`;
fs.writeFileSync(localConfigPath, yamlContent);
logger.info(`📝 Created example config: ${localConfigPath}`);
// Create global example config
const globalConfigDir = path.join(this.getUserDataDir(), ConfigLoader.GLOBAL_CONFIG_DIR);
const globalExamplePath = path.join(globalConfigDir, 'config.example.yaml');
if (!fs.existsSync(globalConfigDir)) {
fs.mkdirSync(globalConfigDir, { recursive: true });
}
fs.writeFileSync(globalExamplePath, yamlContent);
logger.info(`📝 Created global example config: ${globalExamplePath}`);
}
}
ConfigLoader.LOCAL_CONFIG_PATH = '.aiflow/config.yaml';
ConfigLoader.GLOBAL_CONFIG_DIR = 'aiflow';
ConfigLoader.GLOBAL_CONFIG_FILE = 'config.yaml';
// Singleton instance
export const configLoader = new ConfigLoader();
/**
* Get configuration value with fallback
*/
export function getConfigValue(config, path, fallback) {
const keys = path.split('.');
let current = config;
for (const key of keys) {
if (current && typeof current === 'object' && key in current) {
current = current[key];
}
else {
return fallback;
}
}
return current !== undefined ? current : fallback;
}
/**
* Get Git access token for a specific hostname
* @param config Loaded configuration
* @param hostname Git hostname (e.g., 'github.com', 'gitlab.example.com')
* @returns Access token for the hostname or undefined if not found
*/
export function getGitAccessToken(config, hostname) {
const tokens = getConfigValue(config, 'git_access_tokens', {});
return tokens?.[hostname];
}
/**
* Get all configured Git access tokens
* @param config Loaded configuration
* @returns Object with hostname -> token mappings
*/
export function getAllGitAccessTokens(config) {
return getConfigValue(config, 'git_access_tokens', {}) || {};
}
/**
* Parse CLI arguments to config format
* Supports both long (--key) and short (-k) argument formats
*/
export function parseCliArgs(args) {
const config = {};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
let key;
let isShort = false;
if (arg.startsWith('--')) {
key = arg.slice(2);
}
else if (arg.startsWith('-') && !arg.startsWith('--')) {
key = arg.slice(1);
isShort = true;
}
else {
continue;
}
const value = args[i + 1];
// Map short arguments to their long equivalents
if (isShort) {
key = getShortArgMapping(key);
}
switch (key) {
case 'openai-key':
config.openai = { ...config.openai, key: value };
i++;
break;
case 'openai-base-url':
config.openai = { ...config.openai, baseUrl: value };
i++;
break;
case 'openai-model':
config.openai = { ...config.openai, model: value };
i++;
break;
case 'openai-reasoning':
config.openai = { ...config.openai, reasoning: value !== 'false' };
i++;
break;
case 'git-access-token':
// Parse format: hostname=token
if (value && value.includes('=')) {
const [hostname, token] = value.split('=', 2);
if (hostname && token) {
config.git_access_tokens = { ...config.git_access_tokens, [hostname]: token };
}
}
i++;
break;
case 'conan-remote-base-url':
config.conan = { ...config.conan, remoteBaseUrl: value };
i++;
break;
case 'conan-remote-repo':
config.conan = { ...config.conan, remoteRepo: value };
i++;
break;
case 'wecom-webhook':
config.wecom = { ...config.wecom, webhook: value };
i++;
break;
case 'wecom-enable':
config.wecom = { ...config.wecom, enable: value !== 'false' };
i++;
break;
case 'squash-commits':
config.git = { ...config.git, squashCommits: value !== 'false' };
i++;
break;
case 'remove-source-branch':
config.git = { ...config.git, removeSourceBranch: value !== 'false' };
i++;
break;
case 'git-generation-lang':
config.git = { ...config.git, generation_lang: value };
i++;
break;
case 'merge-request-assignee-id':
const assigneeId = parseInt(value, 10);
config.merge_request = { ...config.merge_request, assignee_id: isNaN(assigneeId) ? 0 : assigneeId };
i++;
break;
case 'merge-request-assignee-ids':
// Parse comma-separated string to number array
if (value) {
const assigneeIds = value.split(',').map(id => {
const num = parseInt(id.trim(), 10);
return isNaN(num) ? 0 : num;
}).filter(id => id >= 0);
config.merge_request = { ...config.merge_request, assignee_ids: assigneeIds };
}
i++;
break;
case 'merge-request-reviewer-ids':
// Parse comma-separated string to number array
if (value) {
const reviewerIds = value.split(',').map(id => {
const num = parseInt(id.trim(), 10);
return isNaN(num) ? 0 : num;
}).filter(id => id >= 0);
config.merge_request = { ...config.merge_request, reviewer_ids: reviewerIds };
}
i++;
break;
}
}
return config;
}
/**
* Map short argument names to their long equivalents
*/
function getShortArgMapping(shortKey) {
const shortArgMap = {
// OpenAI shortcuts (OpenAI Key, OpenAI Base Url, OpenAI Model, OpenAI Reasoning)
'ok': 'openai-key',
'obu': 'openai-base-url',
'om': 'openai-model',
'or': 'openai-reasoning',
// Git access token shortcuts (Git Access Token)
'gat': 'git-access-token',
// Conan shortcuts (Conan Remote Base Url, Conan Remote Repo)
'crbu': 'conan-remote-base-url',
'crr': 'conan-remote-repo',
// WeChat Work shortcuts (WeChat Work webhook, WeChat Work Enable)
'ww': 'wecom-webhook',
'we': 'wecom-enable',
// Git shortcuts (Squash Commits, Remove Source Branch, Generate Language)
'sc': 'squash-commits',
'rsb': 'remove-source-branch',
'ggl': 'git-generation-lang',
// Merge Request shortcuts (Merge Request Assignee ID, Assignee IDs, Reviewer IDs)
'mrai': 'merge-request-assignee-id',
'mrais': 'merge-request-assignee-ids',
'mrris': 'merge-request-reviewer-ids',
};
return shortArgMap[shortKey] || shortKey;
}
/**
* Get help text for CLI arguments
*/
export function getCliHelp() {
// Calculate actual global config path
const userDataDir = getUserDataDir();
const globalConfigPath = path.join(userDataDir, 'aiflow', 'config.yaml');
return `
AIFlow CLI 配置选项
配置优先级: 命令行参数 > 本地配置(.aiflow/config.yaml) > 全局配置(${globalConfigPath}) > 环境变量
OpenAI 配置 - AI功能支持:
-ok, --openai-key <key> OpenAI API密钥 (必需,用于AI生成提交信息)
-obu, --openai-base-url <url> OpenAI API地址 (必需,API请求端点)
-om, --openai-model <model> OpenAI模型 (必需,如gpt-3.5-turbo、gpt-4)
-or, --openai-reasoning <bool> 启用推理模式 (可选,适用于o1等推理模型)
Git 访问令牌配置 - 多平台支持:
-gat, --git-access-token <host=token> Git访问令牌 (格式: 主机名=令牌)
支持多个平台,如:
github.com=ghp_xxxxx
gitlab.example.com=glpat_xxxxx
gitee.com=gitee_xxxxx
Conan 配置 - C++包管理:
-crbu, --conan-remote-base-url <url> Conan仓库API地址 (Conan操作时必需)
-crr, --conan-remote-repo <repo> Conan仓库名称 (可选,默认为'repo')
企业微信配置 - 通知功能:
-ww, --wecom-webhook <url> 企业微信机器人Webhook地址 (可选)
-we, --wecom-enable <bool> 启用企业微信通知 (可选,true/false)
Git 配置 - 合并请求行为:
-sc, --squash-commits <bool> 压缩提交 (可选,合并时压缩多个提交)
-rsb, --remove-source-branch <bool> 删除源分支 (可选,合并后删除分支)
-ggl, --git-generation-lang <lang> 生成语言 (可选,AI生成内容的语言,如: zh-CN, en, ja)
合并请求配置 - 指派和审查者:
-mrai, --merge-request-assignee-id <id> 单个指派人用户ID (可选,设置为0取消指派)
-mrais, --merge-request-assignee-ids <ids> 指派人用户ID列表 (可选,逗号分隔,如: 1,2,3)
-mrris, --merge-request-reviewer-ids <ids> 审查者用户ID列表 (可选,逗号分隔,如: 1,2,3)
使用示例:
# 基本配置
aiflow -ok sk-abc123 -gat github.com=ghp_xyz789
# 多平台访问令牌
aiflow -ok sk-abc123 -gat gitlab.example.com=glpat-abc123 -gat github.com=ghp_def456
# 完整配置
aiflow -ok sk-abc123 -gat gitlab.company.com=glpat-xyz789 -crbu https://conan.company.com -we true
# 配置合并请求指派和审查者
aiflow -ok sk-abc123 -mrai 123 -mrris 456,789
# 使用长参数名
aiflow --openai-key sk-abc123 --git-access-token gitlab.example.com=glpat-xyz789 --merge-request-assignee-ids 1,2,3
环境变量格式:
GIT_ACCESS_TOKEN_GITHUB_COM=ghp_xxxxx
GIT_ACCESS_TOKEN_GITLAB_EXAMPLE_COM=glpat_xxxxx
GIT_ACCESS_TOKEN_GITEE_COM=gitee_xxxxx
MERGE_REQUEST_ASSIGNEE_ID=123
MERGE_REQUEST_ASSIGNEE_IDS=1,2,3
MERGE_REQUEST_REVIEWER_IDS=4,5,6
配置文件位置:
本地: .aiflow/config.yaml
全局: ${globalConfigPath}
运行 'aiflow --create-config' 可生成示例配置文件
`;
}
/**
* Interactive configuration initialization
*/
export async function initConfig(isGlobal = false) {
// Calculate actual config path
let configPath;
if (isGlobal) {
const userDataDir = getUserDataDir();
configPath = path.join(userDataDir, 'aiflow', 'config.yaml');
}
else {
configPath = path.join(process.cwd(), '.aiflow', 'config.yaml');
}
console.log(`🔧 AIFlow 配置初始化${isGlobal ? ' (全局)' : ' (本地)'}`);
console.log(`📁 配置位置: ${configPath}`);
console.log('💡 提示:直接回车使用默认值或跳过可选配置\n');
// Check if this is incremental configuration
const hasExistingConfig = fs.existsSync(configPath);
let hasGlobalConfig = false;
let globalConfigPath = '';
if (!isGlobal) {
// For local config, check if global config exists
const userDataDir = getUserDataDir();
globalConfigPath = path.join(userDataDir, 'aiflow', 'config.yaml');
hasGlobalConfig = fs.existsSync(globalConfigPath);
if (hasGlobalConfig) {
console.log('📋 检测到全局配置,您可以选择增量配置模式');
console.log('💡 增量配置模式:基于全局配置,只配置您想要在本地覆盖的模块\n');
}
}
else if (hasExistingConfig) {
console.log('📋 检测到现有全局配置,您可以选择增量配置模式');
console.log('💡 增量配置模式:只配置您想要修改的模块,其他保持不变\n');
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
};
try {
// Incremental configuration mode selection
let configModules = [];
let isIncrementalMode = false;
const canUseIncrementalMode = (hasExistingConfig && isGlobal) || (hasGlobalConfig && !isGlobal);
if (canUseIncrementalMode) {
const incrementalMode = await question('是否使用增量配置模式?(y/N): ');
if (incrementalMode.toLowerCase() === 'y' || incrementalMode.toLowerCase() === 'yes') {
isIncrementalMode = true;
console.log('\n📋 请选择要配置的模块 (可多选,用逗号分隔):');
console.log(' 1. openai - OpenAI API 配置');
console.log(' 2. git-tokens - Git 访问令牌配置');
console.log(' 3. conan - Conan 配置');
console.log(' 4. wecom - 企业微信配置');
console.log(' 5. git - Git 行为配置');
console.log(' 6. mr - 合并请求配置');
console.log(' all - 配置所有模块\n');
const selectedModules = await question('选择模块 (例如: 1,5 或 openai,git): ');
if (selectedModules.trim()) {
const modules = selectedModules.split(',').map(m => m.trim().toLowerCase());
configModules = modules.flatMap(module => {
switch (module) {
case '1':
case 'openai': return ['openai'];
case '2':
case 'git-tokens': return ['git-tokens'];
case '3':
case 'conan': return ['conan'];
case '4':
case 'wecom': return ['wecom'];
case '5':
case 'git': return ['git'];
case '6':
case 'mr': return ['mr'];
case 'all': return ['openai', 'git-tokens', 'conan', 'wecom', 'git', 'mr'];
default: return [];
}
}).filter((v, i, arr) => arr.indexOf(v) === i); // Remove duplicates
}
if (configModules.length === 0) {
console.log('❌ 未选择任何模块,退出配置');
rl.close();
return;
}
console.log(`\n✅ 将配置以下模块: ${configModules.join(', ')}\n`);
}
else {
// Full configuration mode
configModules = ['openai', 'git-tokens', 'conan', 'wecom', 'git', 'mr'];
}
}
else {
// Full configuration mode for new configs
configModules = ['openai', 'git-tokens', 'conan', 'wecom', 'git', 'mr'];
}
// Load existing configuration if available
let configData = {
openai: {},
git_access_tokens: {},
conan: {},
wecom: {},
git: {},
merge_request: {}
};
// For local config, first try to load global config as base
if (!isGlobal && hasGlobalConfig) {
try {
const globalConfigContent = fs.readFileSync(globalConfigPath, 'utf8');
const globalConfig = yaml.load(globalConfigContent);
if (globalConfig) {
configData = {
openai: globalConfig.openai || {},
git_access_tokens: globalConfig.git_access_tokens || {},
conan: globalConfig.conan || {},
wecom: globalConfig.wecom || {},
git: globalConfig.git || {},
merge_request: globalConfig.merge_request || {}
};
console.log('📋 已加载全局配置作为基础配置\n');
}
}
catch (error) {
console.log('⚠️ 读取全局配置文件失败,将使用空配置\n');
}
}
// Then try to load existing local/current config file to override
if (fs.existsSync(configPath)) {
try {
const existingConfigContent = fs.readFileSync(configPath, 'utf8');
const existingConfig = yaml.load(existingConfigContent);
if (existingConfig) {
// Merge existing config over the base config
configData = {
openai: { ...configData.openai, ...(existingConfig.openai || {}) },
git_access_tokens: { ...configData.git_access_tokens, ...(existingConfig.git_access_tokens || {}) },
conan: { ...configData.conan, ...(existingConfig.conan || {}) },
wecom: { ...configData.wecom, ...(existingConfig.wecom || {}) },
git: { ...configData.git, ...(existingConfig.git || {}) },
merge_request: { ...configData.merge_request, ...(existingConfig.merge_request || {}) }
};
console.log(`📋 发现现有${isGlobal ? '全局' : '本地'}配置文件,将作为默认值使用\n`);
}
}
catch (error) {
console.log('⚠️ 读取现有配置文件失败,将创建新配置\n');
}
}
// OpenAI configuration
if (configModules.includes('openai')) {
console.log('🤖 OpenAI 配置:');
const currentKey = configData.openai.key ? '已设置' : '';
const openaiKey = await question(` OpenAI API 密钥 (必需)${currentKey ? ` [${currentKey}]` : ''}: `);
if (openaiKey.trim())
configData.openai.key = openaiKey.trim();
const currentBaseUrl = configData.openai.baseUrl || 'https://api.openai.com/v1';
const openaiBaseUrl = await question(` OpenAI API 地址 [${currentBaseUrl}]: `);
configData.openai.baseUrl = openaiBaseUrl.trim() || currentBaseUrl;
const currentModel = configData.openai.model || 'gpt-3.5-turbo';
const openaiModel = await question(` OpenAI 模型 [${currentModel}]: `);
configData.openai.model = openaiModel.trim() || currentModel;
const currentReasoning = configData.openai.reasoning !== undefined ? configData.openai.reasoning : false;
const openaiReasoning = await question(` 启用推理模式 (推荐用于o1等推理模型) [${currentReasoning}]: `);
configData.openai.reasoning = openaiReasoning.trim() === '' ? currentReasoning : openaiReasoning.trim() !== 'false';
}
// Git access tokens configuration
if (configModules.includes('git-tokens')) {
console.log('\n🔑 Git 访问令牌配置:');
// Keep existing tokens, don't reset
if (!configData.git_access_tokens) {
configData.git_access_tokens = {};
}
// Show existing tokens
const existingHosts = Object.keys(configData.git_access_tokens);
if (existingHosts.length > 0) {
console.log(' 现有配置的Git平台:');
existingHosts.forEach(host => {
console.log(` • ${host}: 已设置`);
});
console.log('');
}
console.log(' 您可以添加新的Git平台访问令牌或修改现有配置,直接回车跳过');
// Git platform tokens with loop for multiple platforms
while (true) {
const gitHost = await question(' Git 平台主机名 (如: github.com, gitlab.example.com, gitee.com,留空结束): ');
if (!gitHost.trim())
break;
const currentToken = configData.git_access_tokens[gitHost.trim()];
const tokenPrompt = currentToken
? ` ${gitHost.trim()} 访问令牌 [已设置]: `
: ` ${gitHost.trim()} 访问令牌: `;
const gitToken = await question(tokenPrompt);
if (gitToken.trim()) {
configData.git_access_tokens[gitHost.trim()] = gitToken.trim();
console.log(` ✅ 已${currentToken ? '更新' : '添加'} ${gitHost.trim()} 的访问令牌`);
}
const continueAdding = await question(' 是否继续添加其他 Git 平台令牌?(y/N): ');
if (continueAdding.toLowerCase() !== 'y' && continueAdding.toLowerCase() !== 'yes') {
break;
}
}
}
// Conan configuration
if (configModules.includes('conan')) {
console.log('\n📦 Conan 配置:');
const currentConanUrl = configData.conan.remoteBaseUrl || '';
const conanBaseUrl = await question(` Conan 仓库 API 地址 (可选)${currentConanUrl ? ` [${currentConanUrl}]` : ''}: `);
if (conanBaseUrl.trim()) {
configData.conan.remoteBaseUrl = conanBaseUrl.trim();
}
else if (!currentConanUrl) {
delete configData.conan.remoteBaseUrl;
}
const currentConanRepo = configData.conan.remoteRepo || 'repo';
const conanRepo = await question(` Conan 仓库名称 [${currentConanRepo}]: `);
configData.conan.remoteRepo = conanRepo.trim() || currentConanRepo;
}
// WeChat Work configuration
if (configModules.includes('wecom')) {
console.log('\n💬 企业微信配置:');
const currentWebhook = configData.wecom.webhook || '';
const wecomWebhook = await question(` 企业微信 Webhook 地址 (可选)${currentWebhook ? ` [已设置]` : ''}: `);
if (wecomWebhook.trim()) {
configData.wecom.webhook = wecomWebhook.trim();
}
else if (!currentWebhook) {
delete configData.wecom.webhook;
}
const currentEnable = configData.wecom.enable !== undefined ? configData.wecom.enable : true;
const wecomEnable = await question(` 启用企业微信通知 [${currentEnable}]: `);
configData.wecom.enable = wecomEnable.trim() === '' ? currentEnable : wecomEnable.trim() !== 'false';
}
// Git configuration
if (configModules.includes('git')) {
console.log('\n🌿 Git 配置:');
const currentSquash = configData.git.squashCommits !== undefined ? configData.git.squashCommits : true;
const squashCommits = await question(` 压缩提交 [${currentSquash}]: `);
configData.git.squashCommits = squashCommits.trim() === '' ? currentSquash : squashCommits.trim() !== 'false';
const currentRemove = configData.git.removeSourceBranch !== undefined ? configData.git.removeSourceBranch : true;
const removeSourceBranch = await question(` 删除源分支 [${currentRemove}]: `);
configData.git.removeSourceBranch = removeSourceBranch.trim() === '' ? currentRemove : removeSourceBranch.trim() !== 'false';
const currentLang = configData.git.generation_lang || 'en';
const generationLang = await question(` AI生成语言 (en=英文, zh-CN=中文, ja=日文等) [${currentLang}]: `);
configData.git.generation_lang = generationLang.trim() || currentLang;
}
// Merge Request configuration
if (configModules.includes('mr')) {
console.log('\n🔀 合并请求指派配置:');
const currentAssigneeId = configData.merge_request.assignee_id || 0;
const assigneeId = await question(` 单个指派人用户ID (可选,0表示取消指派) [${currentAssigneeId}]: `);
const parsedAssigneeId = parseInt(assigneeId.trim(), 10);
configData.merge_request.assignee_id = isNaN(parsedAssigneeId) ? currentAssigneeId : parsedAssigneeId;
const currentAssigneeIds = configData.merge_request.assignee_ids || [];
const assigneeIdsStr = currentAssigneeIds.length > 0 ? currentAssigneeIds.join(',') : '';
const assigneeIds = await question(` 指派人用户ID列表 (可选,逗号分隔,如: 1,2,3)${assigneeIdsStr ? ` [${assigneeIdsStr}]` : ''}: `);
if (assigneeIds.trim()) {
configData.merge_request.assignee_ids = assigneeIds.split(',').map(id => {
const num = parseInt(id.trim(), 10);
return isNaN(num) ? 0 : num;
}).filter(id => id >= 0);
}
else if (!assigneeIdsStr) {
configData.merge_request.assignee_ids = [];
}
const currentReviewerIds = configData.merge_request.reviewer_ids || [];
const reviewerIdsStr = currentReviewerIds.length > 0 ? currentReviewerIds.join(',') : '';
const reviewerIds = await question(` 审查者用户ID列表 (可选,逗号分隔,如: 1,2,3)${reviewerIdsStr ? ` [${reviewerIdsStr}]` : ''}: `);
if (reviewerIds.trim()) {
configData.merge_request.reviewer_ids = reviewerIds.split(',').map(id => {
const num = parseInt(id.trim(), 10);
return isNaN(num) ? 0 : num;
}).filter(id => id >= 0);
}
else if (!reviewerIdsStr) {
configData.merge_request.reviewer_ids = [];
}
}
rl.close();
// Create configuration file
await createConfigFile(configData, isGlobal, configModules, isIncrementalMode);
console.log('\n✅ 配置初始化完成!');
if (canUseIncrementalMode && configModules.length < 6) {
if (isGlobal) {
console.log(`📁 已更新${configModules.join(', ')}模块的全局配置`);
console.log('💡 其他模块配置保持不变');
}
else {
console.log(`📁 已创建本地配置,覆盖${configModules.join(', ')}模块`);
console.log('💡 其他模块将继承全局配置');
}
}
else {
console.log(`📁 配置文件已创建: ${isGlobal ? '全局配置' : '本地配置'}`);
if (!isGlobal && hasGlobalConfig) {
console.log('💡 本地配置将覆盖全局配置的对应部分');
}
}
console.log('💡 您可以随时手动编辑配置文件进行修改');
}
catch (error) {
rl.close();
console.error('❌ 配置初始化失败:', error);
process.exit(1);
}
}
/**
* Create configuration file
*/
export async function createConfigFile(configData, isGlobal, configModules = ['openai', 'git-tokens', 'conan', 'wecom', 'git', 'mr'], isIncrementalMode = false) {
// Calculate actual global config path
const userDataDir = getUserDataDir();
const globalConfigPath = path.join(userDataDir, 'aiflow', 'config.yaml');
// Generate YAML content with comments
let yamlContent = '';
// Load existing global config for incremental updates
let existingConfig = {};
if (isIncrementalMode && isGlobal && fs.existsSync(globalConfigPath)) {
try {
const existingContent = fs.readFileSync(globalConfigPath, 'utf8');
existingConfig = yaml.load(existingContent) || {};
}
catch (error) {
console.warn('⚠️ 无法读取现有全局配置,将创建新配置');
}
}
if (isIncrementalMode && !isGlobal && configModules.length < 6) {
// For incremental local config, only include selected modules
yamlContent = `# AIFlow 本地配置文件 (增量模式)
# 此配置将覆盖全局配置的对应部分
# 配置优先级: 命令行参数 > 本地配置(.aiflow/config.yaml) > 全局配置(${globalConfigPath}) > 环境变量
`;
}
else {
yamlContent = `# AIFlow 配置文件
# 配置优先级: 命令行参数 > 本地配置(.aiflow/config.yaml) > 全局配置(${globalConfigPath}) > 环境变量
`;
}
// Add sections based on selected modules or existing config
const allModules = ['openai', 'git-tokens', 'conan', 'wecom', 'git', 'mr'];
const modulesToInclude = isIncrementalMode && isGlobal
? allModules // In global incremental mode, include all modules
: configModules; // In other modes, only include selected modules
// Helper function to get config for a module (new config for selected, existing for others)
const getModuleConfig = (moduleName, newConfig, existingConfig) => {
if (isIncrementalMode && isGlobal) {
if (configModules.includes(moduleName)) {
return newConfig; // Use new config for selected modules
}
else {
// For non-selected modules, try to find existing config
// Map module names to config keys
const configKeyMap = {
'openai': 'openai',
'git-tokens': 'git_access_tokens',
'conan': 'conan',
'wecom': 'wecom',
'git': 'git',
'mr': 'merge_request'
};
const configKey = configKeyMap[moduleName] || moduleName;
if (existingConfig[configKey]) {
return existingConfig[configKey]; // Use existing config for non-selected modules
}
else {
return newConfig; // Fallback to new config if no existing config
}
}
}
else {
return newConfig; // Use new config for non-incremental mode
}
};
if (modulesToInclude.includes('openai')) {
const openaiConfig = getModuleConfig('openai', configData.openai, existingConfig);
yamlContent += `# OpenAI API 配置 - 用于AI驱动的功能
openai:
# OpenAI API 密钥 (必需) - 用于生成提交信息和代码分析
key: ${openaiConfig.key || 'your-openai-api-key'}
# OpenAI API 基础URL (必需) - API请求的端点地址
baseUrl: ${openaiConfig.baseUrl || 'https://api.openai.com/v1'}
# OpenAI 模型名称 (必需) - 指定使用的AI模型,如 gpt-3.5-turbo, gpt-4
model: ${openaiConfig.model || 'gpt-3.5-turbo'}
# 启用推理模式 (可选) - 对于o1等推理模型,可以启用更深度的思考模式
reasoning: ${openaiConfig.reasoning !== undefined ? openaiConfig.reasoning : false}
`;
}
if (modulesToInclude.includes('git-tokens')) {
const gitTokensConfig = getModuleConfig('git_access_tokens', configData.git_access_tokens, existingConfig);
yamlContent += `# Git 访问令牌配置 - 支持多个Git托管平台
git_access_tokens:
${Object.keys(gitTokensConfig || {}).length > 0
? Object.entries(gitTokensConfig).map(([host, token]) => ` # ${host} 访问令牌\n ${host}: ${token}`).join('\n\n')
: ` # GitHub 访问令牌 - 格式: ghp_xxxxxxxxxxxxxxxxxxxx
# github.com: ghp_xxxxxxxxxxxxxxxxxxxxx
# GitLab 访问令牌 - 格式: glpat-xxxxxxxxxxxxxxxxxxxx
# gitlab.example.com: glpat-xxxxxxxxxxxxxxxxxxxxx
# Gitee 访问令牌 - 格式: gitee_xxxxxxxxxxxxxxxxxxxx
# gitee.com: gitee_xxxxxxxxxxxxxxxxxxxxx`}
`;
}
if (modulesToInclude.includes('conan')) {
const conanConfig = getModuleConfig('conan', configData.conan, existingConfig);
yamlContent += `# Conan 包管理器配置 - 用于C++包管理和版本更新
conan:
# Conan 远程仓库基础URL (Conan操作时必需) - Conan包仓库的API地址
${conanConfig.remoteBaseUrl ? `remoteBaseUrl: ${conanConfig.remoteBaseUrl}` : '# remoteBaseUrl: https://conan.example.com'}
# Conan 远程仓库名称 (可选) - 默认使用的仓库名称,默认为'repo'
remoteRepo: ${conanConfig.remoteRepo || 'repo'}
`;
}
if (modulesToInclude.includes('wecom')) {
const wecomConfig = getModuleConfig('wecom', configData.wecom, existingConfig);
yamlContent += `# 企业微信通知配置 - 用于发送操作结果通知
wecom:
# 启用企业微信通知 (可选) - 是否开启通知功能,默认为false
enable: ${wecomConfig.enable || false}
# 企业微信机器人Webhook地址 (可选) - 用于发送通知消息的机器人地址
${wecomConfig.webhook ? `webhook: ${wecomConfig.webhook}` : '# webhook: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=your-key'}
`;
}
if (modulesToInclude.includes('git')) {
const gitConfig = getModuleConfig('git', configData.git, existingConfig);
yamlContent += `# Git 合并请求配置 - 控制MR的默认行为
git:
# 压缩提交 (可选) - 合并时是否将多个提交压缩为一个,默认为true
squashCommits: ${gitConfig.squashCommits !== undefined ? gitConfig.squashCommits : true}
# 删除源分支 (可选) - 合并后是否删除源分支,默认为true
removeSourceBranch: ${gitConfig.removeSourceBranch !== undefined ? gitConfig.removeSourceBranch : true}
# AI生成语言 (可选) - AI生成commit message和MR描述的语言,默认为en
generation_lang: ${gitConfig.generation_lang || 'en'}
`;
}
if (modulesToInclude.includes('mr')) {
const mrConfig = getModuleConfig('mr', configData.merge_request, existingConfig);
yamlContent += `# 合并请求指派配置 - 配置指派人和审查者
merge_request:
# 单个指派人用户ID (可选) - 设置为0或留空取消指派
assignee_id: ${mrConfig?.assignee_id || 0}
# 指派人用户ID数组 (可选) - 多个指派人,设置为空数组取消所有指派
assignee_ids: ${mrConfig?.assignee_ids ? JSON.stringify(mrConfig.assignee_ids) : '[]'}
# 审查者用户ID数组 (可选) - 设置为空数组不添加审查者
reviewer_ids: ${mrConfig?.reviewer_ids ? JSON.stringify(mrConfig.reviewer_ids) : '[]'}
`;
}
// Determine config path
let configPath;
if (isGlobal) {
configPath = globalConfigPath;
const configDir = path.dirname(globalConfigPath);
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
}
else {
const configDir = path.join(process.cwd(), '.aiflow');
configPath = path.join(configDir, 'config.yaml');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
}
fs.writeFileSync(configPath, yamlContent);
console.log(`\n📝 配置文件已创建: ${configPath}`);
}
//# sourceMappingURL=config.js.map