UNPKG

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.

329 lines (317 loc) 15.6 kB
#!/usr/bin/env node import { BaseAiflowApp } from './aiflow-app.js'; import { StringUtil } from './utils/string-util.js'; import { ConanService } from './services/conan-service.js'; import { FileUpdaterService } from './services/file-updater-service.js'; import { UpdateChecker } from './utils/update-checker.js'; import { ColorUtil } from './utils/color-util.js'; import { parseCliArgs, getConfigValue, getCliHelp, initConfig } from './config.js'; import path from 'path'; import { fileURLToPath } from 'url'; import clipboardy from 'clipboardy'; import { readFileSync } from 'fs'; import { logger } from './logger.js'; import { processExit } from './utils/process-exit.js'; /** * Conan package update application with automated MR creation */ export class ConanPkgUpdateApp extends BaseAiflowApp { /** * Initialize services with configuration (override to add Conan-specific services) */ async initializeServices(cliConfig = {}) { // Call parent initialization await super.initializeServices(cliConfig); // Initialize Conan-specific services this.conan = new ConanService(getConfigValue(this.config, 'conan.remoteBaseUrl', '') || '', this.http); this.fileUpdater = new FileUpdaterService(this.conan, this.git); } /** * Update package and create MR * @param packageName Package name to update (e.g., "zterm") * @param remote */ async updatePackage(packageName, remote = "repo") { logger.info(`🚀 AIFlow Conan Tool - Package Update`); logger.info(`📦 Package: ${packageName}`); logger.info(`🌐 Remote: ${remote}`); logger.info(`📁 Working directory: ${process.cwd()}`); logger.info(`⏰ Started at: ${new Date().toISOString()}`); logger.info('─'.repeat(50)); try { // Step 1: Update package files and check for changes logger.info(`📦 Updating package ${packageName} from remote ${remote}...`); const completeInfo = await this.fileUpdater.updatePackage(remote, packageName); if (!completeInfo) { logger.info(`✅ Package ${packageName} is already up to date. No MR needed.`); return; } // Step 2: Show git status and stage updated files this.git.showGitInfo(); this.fileUpdater.stageFiles(); const diff = this.git.getDiff(); if (!diff) { logger.info(`⚠️ No changes detected in files after update. Skipping MR creation.`); return; } const changedFiles = this.git.getChangedFiles(); // Step 3: Determine target branch and current branch const currentBranch = this.git.getCurrentBranch(); const targetBranch = this.git.getTargetBranch(); logger.info(`🌿 Current branch: ${currentBranch}`); logger.info(`🎯 Target branch: ${targetBranch}`); if (!currentBranch) { logger.error("❌ Could not detect current branch or in detached HEAD. Please stage some changes."); return; } // Step 4: Generate commit message and branch name using AI logger.info(`🤖 Generating commit message and branch name...`); const { commit, branch, description, title } = await this.openai.generateCommitAndBranch(diff, getConfigValue(this.config, 'git.generation_lang', 'en')); logger.info(`✅ Generated commit message length: ${commit && commit.length}`); logger.info(`✅ Generated branch suggestion: ${branch}`); logger.info(`✅ Generated MR description length: ${description && description.length}`); logger.info(`✅ Generated MR title: ${title}`); // Step 5: Create new branch const gitUser = this.git.getUserName(); const aiBranch = StringUtil.sanitizeBranch(branch); const dateSuffix = new Date().toISOString().slice(0, 19).replace(/-|T|:/g, ""); const branchName = `${gitUser}/conan-update-${packageName}-${aiBranch}-${dateSuffix}`; logger.info(`✅ Generated branch name: ${branchName}`); // Step 6: Commit changes const enhancedCommit = commit; logger.info(`✅ Generated commit message: ${enhancedCommit}`); // Dynamic countdown before committing and pushing await ColorUtil.countdown(3, `Creating branch(${branchName}) and pushing`, 'Committing now...'); this.git.createBranch(branchName); this.git.commit(enhancedCommit); // Step 7: Push branch to remote logger.info(`📤 Pushing branch to remote...`); this.git.push(branchName); // Step 8: Create Merge Request logger.info(`📋 Creating Merge Request...`); const squashCommits = getConfigValue(this.config, 'git.squashCommits', true); const removeSourceBranch = getConfigValue(this.config, 'git.removeSourceBranch', true); // Get merge request configuration const assigneeId = getConfigValue(this.config, 'merge_request.assignee_id'); const assigneeIds = getConfigValue(this.config, 'merge_request.assignee_ids'); const reviewerIds = getConfigValue(this.config, 'merge_request.reviewer_ids'); const mergeRequestOptions = { squash: squashCommits, removeSourceBranch: removeSourceBranch, description: description }; // Add assignee configuration if specified if (typeof assigneeId === 'number' && assigneeId > 0) { mergeRequestOptions.assignee_id = assigneeId; } if (assigneeIds && Array.isArray(assigneeIds) && assigneeIds.length > 0) { mergeRequestOptions.assignee_ids = assigneeIds; } if (reviewerIds && Array.isArray(reviewerIds) && reviewerIds.length > 0) { mergeRequestOptions.reviewer_ids = reviewerIds; } const mrTitle = title; // Dynamic countdown before creating MR await ColorUtil.countdown(3, 'Creating merge request in', 'Creating merge request now...'); const mrUrl = await this.gitPlatform.createMergeRequest(branchName, targetBranch, mrTitle, mergeRequestOptions); logger.info(`🎉 ${this.gitPlatform.getPlatformName() === 'github' ? 'Pull Request' : 'Merge Request'} created:`, mrUrl); // Step 9: Switch back to original branch if different if (currentBranch && currentBranch !== branchName) { logger.info(`✅ Auto checkout to ${currentBranch}`); this.git.checkout(currentBranch); } // Step 10: Send notification if (getConfigValue(this.config, 'wecom.enable', false) && getConfigValue(this.config, 'wecom.webhook', '')) { logger.info(`📢 Sending notification...`); await this.wecom.sendMergeRequestNotice(branchName, targetBranch, mrUrl, mrTitle, enhancedCommit, changedFiles); logger.info("📢 Notification sent via WeCom webhook."); } logger.info(`✅ AIFlow Conan workflow completed successfully!`); // Step 11: Print the MR info and copy to clipboard // Format MR information for sharing const isGitHub = this.gitPlatform.getPlatformName() === 'github'; const requestType = isGitHub ? 'Pull Request' : 'Merge Request'; const requestAbbr = isGitHub ? 'PR' : 'MR'; const outputMrInfo = `🎉 Conan - ${packageName} 包更新${requestType}创建成功! 📋 ${requestAbbr} 链接: ${mrUrl} 🌿 分支信息: ${branchName} -> ${targetBranch} 📝 提交信息: ${enhancedCommit} 📁 变更文件 (${changedFiles.length} 个)${changedFiles.length > 10 ? `前10个: ` : ': '} ${changedFiles.slice(0, 10).map(file => `• ${file}`).join('\n')}${changedFiles.length > 10 ? `\n...${changedFiles.length - 10}个文件` : ''}`; const consoleMrInfo = ` ${'-'.repeat(50)} ${outputMrInfo} ${'-'.repeat(50)} `; logger.info(consoleMrInfo); await clipboardy.write(outputMrInfo); logger.info("📋 MR info copied to clipboard."); } catch (error) { logger.error(`❌ Error during package update:`, error); await processExit(1, error); } } /** * Validate configuration (override to add Conan-specific validation) */ validateConfiguration(validateGitAccessToken = true) { // Call parent validation const isValid = super.validateConfiguration(validateGitAccessToken); if (!isValid) { return false; } // Additional Conan-specific validation const conanBaseUrl = getConfigValue(this.config, 'conan.remoteBaseUrl', ''); if (!conanBaseUrl) { logger.warn(`⚠️ Conan remote base URL not configured. Some features may not work.`); } logger.info(`✅ Conan configuration validation passed`); return true; } /** * Display version information */ static showVersion() { const packageJson = JSON.parse(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '../package.json'), 'utf8')); const version = packageJson.version; const name = packageJson.name; const description = packageJson.description; logger.info(` 🚀 ${name} Conan Tool v${version} ${description} 📦 Package: ${name} 🔢 Version: ${version} 📅 Built: ${new Date().toISOString().split('T')[0]} 🌐 Repository: https://github.com/HeiSir2014/git-aiflow 📋 License: MIT 💡 For more information, visit: https://github.com/HeiSir2014/git-aiflow `); } /** * Display usage information */ static showUsage() { logger.info(` 🔧 AIFlow Conan Tool Usage: aiflow-conan [init] [options] <package-name> [remote] Commands: init 交互式配置初始化 init --global, -g 初始化全局配置 Arguments: package-name Name of the Conan package to update (e.g., "zterm") remote Conan remote repository name (default: from config or "repo") Options: --version, -v 显示版本信息 --config-help 显示 CLI 配置选项帮助 --help, -h 显示此帮助信息 Configuration Options (可以通过 CLI 参数覆盖配置文件): -ok, --openai-key <key> OpenAI API 密钥 -obu, --openai-base-url <url> OpenAI API 地址 -om, --openai-model <model> OpenAI 模型 -gat, --git-access-token <host=token> Git 访问令牌 (格式: 主机名=令牌) -crbu, --conan-remote-base-url <url> Conan 仓库 API 地址 -crr, --conan-remote-repo <repo> Conan 仓库名称 -ww, --wecom-webhook <url> 企业微信 Webhook 地址 -we, --wecom-enable <bool> 启用企业微信通知 -sc, --squash-commits <bool> 压缩提交 -rsb, --remove-source-branch <bool> 删除源分支 Examples: aiflow-conan init # 交互式初始化本地配置 aiflow-conan init --global # 交互式初始化全局配置 aiflow-conan zterm # 使用配置文件运行 aiflow-conan zterm repo # 指定远程仓库 aiflow-conan -ok sk-123 -gat gitlab.example.com=glpat-456 zterm # 使用 CLI 参数覆盖配置 配置文件位置 (按优先级排序): 1. 命令行参数 (最高优先级) 2. .aiflow/config.yaml (本地配置) 3. ~/.config/aiflow/config.yaml (全局配置) 4. 环境变量 (最低优先级) Auto-Detection Features: ✅ Git 托管平台项目 ID 从 git remote URL 自动检测 (支持 HTTP/SSH) ✅ Git 托管平台 base URL 从 git remote URL 自动检测 ✅ 目标分支自动检测 (main/master/develop) ✅ Git 访问令牌基于当前仓库主机名自动选择 Files Required: conandata.yml Conan data file in current directory conan.win.lock Conan lock file in current directory `); } /** * Main entry point for command line execution */ static async main() { const args = process.argv.slice(2); // Handle init command if (args.includes('init')) { const isGlobal = args.includes('--global') || args.includes('-g'); await initConfig(isGlobal); return; } // Show version information if (args.includes('--version') || args.includes('-v')) { ConanPkgUpdateApp.showVersion(); await processExit(0); } // Show CLI help if (args.includes('--config-help')) { logger.info(getCliHelp()); await processExit(0); } // Show usage if no arguments or help requested if (args.length === 0 || args.includes('--help') || args.includes('-h')) { ConanPkgUpdateApp.showUsage(); await processExit(0); } // Check for updates at startup (for global installations only) try { const updateChecker = new UpdateChecker(); await updateChecker.checkAndUpdate(); process.env.AIFLOW_VERSION = updateChecker.getVersionInfo().version; } catch (error) { // Don't let update check failures block the main application logger.warn('⚠️ Update check failed:', error instanceof Error ? error.message : 'Unknown error'); } // Parse CLI configuration arguments (filter out package name and remote) const configArgs = args.filter(arg => arg.startsWith('-')); const cliConfig = parseCliArgs(configArgs); // Get non-config arguments (package name and remote) const nonConfigArgs = args.filter(arg => !arg.startsWith('-')); const packageName = nonConfigArgs[0]; const remote = nonConfigArgs[1]; if (!packageName) { logger.error('❌ Package name is required'); ConanPkgUpdateApp.showUsage(); await processExit(1); } logger.info(`🚀 AIFlow Conan Tool`); logger.info(`📦 Package: ${packageName}`); logger.info(`⏰ Started at: ${new Date().toISOString()}`); logger.info('─'.repeat(50)); const app = new ConanPkgUpdateApp(); // Initialize services with configuration await app.initializeServices(cliConfig); // Validate configuration before starting if (!app.validateConfiguration()) { await processExit(1); return; } // Get remote from config or CLI or default const finalRemote = remote || getConfigValue(app.config, 'conan.remoteRepo', 'repo') || 'repo'; logger.info(`🌐 Remote: ${finalRemote}`); // Run the update workflow await app.updatePackage(packageName, finalRemote); } } // Only run if this file is executed directly const run_file = path.basename(process.argv[1]).toLowerCase(); const import_file = path.basename(fileURLToPath(import.meta.url)).toLowerCase(); const isMain = run_file && (['aiflow-conan', 'git-aiflow-conan', import_file].includes(run_file)); isMain && ConanPkgUpdateApp.main().catch(async (error) => { logger.error('❌ Unhandled error:', error); await processExit(1, error); }); //# sourceMappingURL=aiflow-conan-app.js.map