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.
681 lines (668 loc) • 33.1 kB
JavaScript
import { Shell } from './shell.js';
import { HttpClient } from './http/http-client.js';
import { StringUtil } from './utils/string-util.js';
import { GitService } from './services/git-service.js';
import { OpenAiService } from './services/openai-service.js';
import { GitPlatformServiceFactory, getGitAccessTokenForCurrentRepo } from './services/git-platform-service.js';
import { WecomNotifier } from './services/wecom-notifier.js';
import { configLoader, parseCliArgs, getConfigValue, getCliHelp, initConfig } from './config.js';
import { UpdateChecker } from './utils/update-checker.js';
import { ColorUtil } from './utils/color-util.js';
import path from 'path';
import { fileURLToPath } from 'url';
import clipboard from 'clipboardy';
import readline from 'readline';
import { logger } from './logger.js';
import { readFileSync } from 'fs';
import crypto from 'crypto';
import { processExit } from './utils/process-exit.js';
/**
* Base class for AI-powered Git automation applications
*/
export class BaseAiflowApp {
constructor() {
this.shell = Shell.instance();
this.http = new HttpClient();
this.git = GitService.instance();
}
/**
* Initialize services with configuration
*/
async initializeServices(cliConfig = {}) {
// Load configuration with priority merging
this.config = await configLoader.loadConfig(cliConfig);
// Initialize services with configuration
this.openai = new OpenAiService(getConfigValue(this.config, 'openai.key', '') || '', getConfigValue(this.config, 'openai.baseUrl', 'https://api.openai.com/v1') || 'https://api.openai.com/v1', getConfigValue(this.config, 'openai.model', 'gpt-3.5-turbo') || 'gpt-3.5-turbo', getConfigValue(this.config, 'openai.reasoning', false) || false);
// Create platform-specific service using factory (fully automatic)
const platformService = await GitPlatformServiceFactory.create();
if (!platformService) {
throw new Error('Unsupported Git platform. Currently supported: GitLab, GitHub');
}
this.gitPlatform = platformService;
this.wecom = new WecomNotifier(getConfigValue(this.config, 'wecom.webhook', '') || '');
// Display configuration warnings
const warnings = configLoader.getWarnings();
if (warnings.length > 0) {
logger.info('\n⚠️ Configuration warnings:');
warnings.forEach(warning => logger.info(` ${warning}`));
logger.info('');
}
}
/**
* Interactive file selection for staging
* @returns Promise<boolean> - true if files were staged, false if user cancelled
*/
async interactiveFileSelection() {
const fileStatuses = this.git.status();
if (fileStatuses.length === 0) {
console.log(ColorUtil.success("No changes detected in the repository."));
return false;
}
console.log(`\n${ColorUtil.UI_COLORS.emoji('📁')} ${ColorUtil.header('Detected file changes:')}`);
console.log(ColorUtil.separator());
// Group files by status for better display
const untracked = fileStatuses.filter(f => f.isUntracked);
const modified = fileStatuses.filter(f => !f.isUntracked && f.workTreeStatus === 'M');
const deleted = fileStatuses.filter(f => !f.isUntracked && f.workTreeStatus === 'D');
const added = fileStatuses.filter(f => !f.isUntracked && f.indexStatus === 'A');
let fileIndex = 1;
// Display files by category
if (modified.length > 0) {
console.log(`\n${ColorUtil.UI_COLORS.emoji('📝')} ${ColorUtil.LOG_COLORS.warning('Modified files:')}`);
modified.forEach((file) => {
console.log(` ${ColorUtil.formatFileStatusWithDescription(file.path, 'M', file.statusDescription, fileIndex - 1)}`);
fileIndex++;
});
}
if (untracked.length > 0) {
console.log(`\n${ColorUtil.UI_COLORS.emoji('❓')} ${ColorUtil.LOG_COLORS.info('Untracked files:')}`);
untracked.forEach((file) => {
console.log(` ${ColorUtil.formatFileStatusWithDescription(file.path, '?', file.statusDescription, fileIndex - 1)}`);
fileIndex++;
});
}
if (added.length > 0) {
console.log(`\n${ColorUtil.UI_COLORS.emoji('➕')} ${ColorUtil.LOG_COLORS.success('Added files:')}`);
added.forEach((file) => {
console.log(` ${ColorUtil.formatFileStatusWithDescription(file.path, 'A', file.statusDescription, fileIndex - 1)}`);
fileIndex++;
});
}
if (deleted.length > 0) {
console.log(`\n${ColorUtil.UI_COLORS.emoji('🗑️')} ${ColorUtil.LOG_COLORS.error('Deleted files:')}`);
deleted.forEach((file) => {
console.log(` ${ColorUtil.formatFileStatusWithDescription(file.path, 'D', file.statusDescription, fileIndex - 1)}`);
fileIndex++;
});
}
return await this.promptFileSelection(fileStatuses);
}
/**
* Prompt user to select files for staging
* @param fileStatuses Array of file statuses
* @returns Promise<boolean> - true if files were staged, false if cancelled
*/
async promptFileSelection(fileStatuses) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => {
return new Promise((resolve) => {
rl.question(prompt, resolve);
});
};
try {
console.log(`\n${ColorUtil.UI_COLORS.emoji('🎯')} ${ColorUtil.header('File selection options:')}`);
console.log(` ${ColorUtil.UI_COLORS.emoji('•')} Enter file numbers (e.g., 1,3,5 or 1-5)`);
console.log(` ${ColorUtil.UI_COLORS.emoji('•')} Type "all" to stage all files`);
console.log(` ${ColorUtil.UI_COLORS.emoji('•')} Type "modified" to stage only modified files`);
console.log(` ${ColorUtil.UI_COLORS.emoji('•')} Type "untracked" to stage only untracked files`);
console.log(` ${ColorUtil.UI_COLORS.emoji('•')} Press Enter or type "cancel" to cancel`);
const input = await question(`\n${ColorUtil.UI_COLORS.emoji('📋')} ${ColorUtil.prompt('Select files to stage: ')}`);
if (!input.trim() || input.toLowerCase() === 'cancel') {
console.log(ColorUtil.error('Operation cancelled.'));
return false;
}
const selectedFiles = this.parseFileSelection(input, fileStatuses);
if (selectedFiles.length === 0) {
console.log(ColorUtil.error('No valid files selected.'));
return false;
}
// Show selected files for confirmation
console.log(`\n${ColorUtil.UI_COLORS.emoji('📋')} ${ColorUtil.header('Files to be staged:')}`);
selectedFiles.forEach(file => {
console.log(` ${ColorUtil.UI_COLORS.emoji('✓')} ${ColorUtil.formatFileStatusWithDescription(file.path, file.workTreeStatus || file.indexStatus || '?', file.statusDescription)}`);
});
const confirm = await question(`\n${ColorUtil.UI_COLORS.emoji('❓')} ${ColorUtil.prompt('Stage these files? (Y/n): ')}`);
if (confirm.toLowerCase() === 'n' || confirm.toLowerCase() === 'no') {
console.log(ColorUtil.error('Staging cancelled.'));
return false;
}
// Stage selected files
console.log(`\n${ColorUtil.UI_COLORS.emoji('📦')} ${ColorUtil.LOG_COLORS.info('Staging selected files...')}`);
this.git.addFiles(selectedFiles.map(f => f.path));
console.log(ColorUtil.success(`Successfully staged ${selectedFiles.length} file(s).`));
return true;
}
finally {
rl.close();
}
}
/**
* Parse user input for file selection
* @param input User input string
* @param fileStatuses Array of file statuses
* @returns Array of selected files
*/
parseFileSelection(input, fileStatuses) {
const trimmedInput = input.trim().toLowerCase();
// Handle special keywords
if (trimmedInput === 'all') {
return fileStatuses;
}
if (trimmedInput === 'modified') {
return fileStatuses.filter(f => !f.isUntracked && f.workTreeStatus === 'M');
}
if (trimmedInput === 'untracked') {
return fileStatuses.filter(f => f.isUntracked);
}
// Parse numeric input (e.g., "1,3,5" or "1-5")
const selectedFiles = [];
const parts = input.split(',').map(p => p.trim());
for (const part of parts) {
if (part.includes('-')) {
// Handle range (e.g., "1-5")
const [start, end] = part.split('-').map(n => parseInt(n.trim()));
if (!isNaN(start) && !isNaN(end)) {
for (let i = start; i <= end; i++) {
if (i >= 1 && i <= fileStatuses.length) {
selectedFiles.push(fileStatuses[i - 1]);
}
}
}
}
else {
// Handle single number
const num = parseInt(part);
if (!isNaN(num) && num >= 1 && num <= fileStatuses.length) {
selectedFiles.push(fileStatuses[num - 1]);
}
}
}
// Remove duplicates
return [...new Set(selectedFiles)];
}
/**
* Validate required configuration for the application
*/
validateConfiguration(validateGitAccessToken = true) {
const requiredConfigs = [
{ key: 'openai.key', name: 'OpenAI API Key' },
{ key: 'openai.baseUrl', name: 'OpenAI Base URL' },
{ key: 'openai.model', name: 'OpenAI Model' },
];
const missing = [];
for (const config of requiredConfigs) {
const value = getConfigValue(this.config, config.key, '');
if (!value) {
missing.push(config.name);
}
}
// Validate Git access token for current repository
if (validateGitAccessToken) {
try {
getGitAccessTokenForCurrentRepo(this.config, this.git);
}
catch (error) {
missing.push('Git Access Token for current repository');
logger.error(`❌ ${error instanceof Error ? error.message : 'Unknown Git token error'}`);
}
}
if (missing.length > 0) {
logger.error(`❌ Missing required configuration: ${missing.join(', ')}`);
logger.error(`💡 Please run 'aiflow init' to configure or check your config files`);
return false;
}
logger.info(`✅ Configuration validation passed`);
return true;
}
/**
* Check if commit-only mode is enabled via CLI arguments
* @returns True if --commit-only or -co or -cmo is present in CLI args
*/
isCommitOnlyMode() {
const args = process.argv.slice(2);
return args.includes('--commit-only') || args.includes('-co') || args.includes('-cmo');
}
/**
* Commit only workflow - just commit staged changes without creating MR
*/
async runCommitOnly() {
logger.info(`🚀 AIFlow Tool - Commit Only Mode`);
logger.info(`📁 Working directory: ${process.cwd()}`);
logger.info(`⏰ Started at: ${new Date().toISOString()}`);
logger.info('─'.repeat(50));
try {
// Step 1: Check for staged changes
let diff = this.git.getDiff();
let changedFiles = this.git.getChangedFiles();
if (!diff) {
console.log(`${ColorUtil.UI_COLORS.emoji('📋')} ${ColorUtil.LOG_COLORS.info("No staged changes found. Let's select files to stage...")}`);
// Interactive file selection
const filesStaged = await this.interactiveFileSelection();
if (!filesStaged) {
console.log(ColorUtil.error("No files were staged. Exiting..."));
return;
}
// Re-check for staged changes after interactive selection
diff = this.git.getDiff();
changedFiles = this.git.getChangedFiles();
if (!diff) {
console.error(ColorUtil.error("❌ Still no staged changes found. Please check your selection."));
return;
}
logger.info(`✅ Successfully staged ${changedFiles.length} file(s). Continuing...`);
}
// Step 2: Generate commit message using AI
logger.info(`🤖 Generating commit message...`);
const { commit } = await this.openai.generateCommitAndBranch(diff, getConfigValue(this.config, 'git.generation_lang', 'en'));
logger.info("✅ Generated commit message:", commit);
// Step 3: Commit changes
// Dynamic countdown display
logger.info(`📝 Committing changes, starting in 3 seconds...`);
await ColorUtil.countdown(3, 'Committing in', 'Committing now...');
this.git.commit(commit);
logger.info(`✅ Successfully committed changes!`);
logger.info(`📝 Commit message: ${commit}`);
logger.info(`📁 Changed files: ${changedFiles.length}`);
}
catch (error) {
logger.error(`❌ Error during commit:`, error);
throw error;
}
}
/**
* Create MR from base branch to current branch when no staged changes
*/
async runFromBaseBranch() {
logger.info(`🔍 No staged changes found. Detecting base branch for MR creation...`);
// Step 1: Get current branch
const currentBranch = this.git.getCurrentBranch();
logger.info(`🌿 Current branch: ${currentBranch}`);
// Step 2: Detect base branch
const baseBranch = this.git.getBaseBranch();
if (!baseBranch) {
logger.error("❌ Could not detect base branch. Please specify target branch manually or stage some changes.");
return;
}
logger.info(`🎯 Detected base branch: ${baseBranch}`);
// Step 3: Get diff from base branch to current branch
const baseToCurrentDiff = this.git.getDiffBetweenBranches(baseBranch, currentBranch);
if (!baseToCurrentDiff) {
logger.info("✅ No differences found between base branch and current branch.");
logger.info("💡 Current branch is up to date with base branch.");
return;
}
logger.info(`📊 Found changes between ${baseBranch} and ${currentBranch}`);
// Step 4: Get changed files
const changedFiles = this.git.getChangedFilesBetweenBranches(baseBranch, currentBranch);
logger.info(`📁 Changed files: ${changedFiles.length}`);
// Step 5: 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(baseToCurrentDiff, 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}`);
const branchName = currentBranch;
logger.info(`✅ Using branch name: ${branchName}`);
await ColorUtil.countdown(3, `Pushing branch(${branchName})`, 'Pushing branch now...');
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;
}
// Dynamic countdown before creating MR
await ColorUtil.countdown(3, 'Creating merge request in', 'Creating merge request now...');
const mrTitle = title;
const mrUrl = await this.gitPlatform.createMergeRequest(branchName, baseBranch, mrTitle, mergeRequestOptions);
logger.info(`🎉 ${this.gitPlatform.getPlatformName() === 'github' ? 'Pull Request' : 'Merge Request'} created:`, mrUrl);
// Step 9: Send notification
if (getConfigValue(this.config, 'wecom.enable', false) && getConfigValue(this.config, 'wecom.webhook', '')) {
logger.info(`📢 Sending notification...`);
await this.wecom.sendMergeRequestNotice(branchName, baseBranch, mrUrl, mrTitle, commit, changedFiles);
logger.info("📢 Notification sent via WeCom webhook.");
}
logger.info(`✅ AIFlow workflow completed successfully!`);
// Step 10: Print the MR info and copy to clipboard
const isGitHub = this.gitPlatform.getPlatformName() === 'github';
const requestType = isGitHub ? 'Pull Request' : 'Merge Request';
const requestAbbr = isGitHub ? 'PR' : 'MR';
const outputMrInfo = `🎉 ${requestType}创建成功
📋 ${requestAbbr} 链接: ${mrUrl} ${mrTitle}
📝 提交信息:
${commit}
🌿 分支信息: ${branchName} -> ${baseBranch}
📁 变更文件 (${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 clipboard.write(outputMrInfo);
logger.info("📋 MR info copied to clipboard.");
}
/**
* Create automated merge request from staged changes
*/
async run() {
logger.info(`🚀 AIFlow Tool`);
logger.info(`📁 Working directory: ${process.cwd()}`);
logger.info(`⏰ Started at: ${new Date().toISOString()}`);
logger.info('─'.repeat(50));
try {
// Check if commit-only mode is enabled (from CLI args, not config)
const commitOnly = this.isCommitOnlyMode();
if (commitOnly) {
await this.runCommitOnly();
return;
}
// Step 1: Check for staged changes
let diff = this.git.getDiff();
let changedFiles = this.git.getChangedFiles();
if (!diff) {
logger.info("📋 No staged changes found. Let's select files to stage...");
// Interactive file selection
const filesStaged = await this.interactiveFileSelection();
if (!filesStaged) {
logger.info("❌ No files were staged. Trying to create MR from base branch...");
await this.runFromBaseBranch();
return;
}
// Re-check for staged changes after interactive selection
diff = this.git.getDiff();
changedFiles = this.git.getChangedFiles();
if (!diff) {
logger.info("❌ Still no staged changes found. Trying to create MR from base branch...");
await this.runFromBaseBranch();
return;
}
logger.info(`✅ Successfully staged ${changedFiles.length} file(s). Continuing...`);
}
// Step 2: Determine target branch
const currentBranch = this.git.getCurrentBranch();
logger.info(`🌿 Current branch: ${currentBranch}`);
const targetBranch = this.git.getTargetBranch();
logger.info(`🎯 Target branch: ${targetBranch}`);
// Step 3: 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 4: Create branch name
const gitUser = this.git.getUserName();
const aiBranch = StringUtil.sanitizeBranch(branch);
const branchName = `${gitUser}/${aiBranch}-${crypto.randomUUID().substring(0, 6)}`;
logger.info(`✅ Generated branch name: ${branchName}`);
// Step 5: Commit and push
logger.info(`📤 Creating branch and pushing changes...`);
// Dynamic countdown before committing
await ColorUtil.countdown(3, `Creating branch(${branchName}) and pushing`, 'Committing now...');
const isSuccess = this.git.commitAndPush(branchName, commit);
if (!isSuccess) {
logger.info("❌ Branch already exists, skipping creation");
return;
}
// Step 6: 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;
}
// Dynamic countdown before creating MR
await ColorUtil.countdown(3, 'Creating merge request in', 'Creating merge request now...');
const mrTitle = title;
const mrUrl = await this.gitPlatform.createMergeRequest(branchName, targetBranch, mrTitle, mergeRequestOptions);
logger.info(`🎉 ${this.gitPlatform.getPlatformName() === 'github' ? 'Pull Request' : 'Merge Request'} created:`, mrUrl);
if (currentBranch && currentBranch !== branchName) {
logger.info(`✅ Auto checkout to ${currentBranch}`);
this.git.checkout(currentBranch);
}
// Step 7: 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, commit, changedFiles);
logger.info("📢 Notification sent via WeCom webhook.");
}
logger.info(`✅ AIFlow workflow completed successfully!`);
// Step 8: 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 = `🎉 ${requestType}创建成功
📋 ${requestAbbr} 链接: ${mrUrl} ${mrTitle}
📝 提交信息:
${commit}
🌿 分支信息: ${branchName} -> ${targetBranch}
📁 变更文件 (${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 clipboard.write(outputMrInfo);
logger.info("📋 MR info copied to clipboard.");
}
catch (error) {
logger.error(`❌ Error during MR creation:`, error);
throw error;
}
}
}
/**
* Git Auto MR application for automated merge request creation
*/
export class GitAutoMrApp extends BaseAiflowApp {
/**
* 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} 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 Tool
Usage:
aiflow [init] [options]
Commands:
init 交互式配置初始化
init --global, -g 初始化全局配置
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> 删除源分支
-co, --commit-only 仅提交更改,不创建MR
Description:
使用 AI 生成的提交信息和分支名称自动创建合并请求
Prerequisites:
1. 暂存您的更改: git add .
2. 配置必要参数: aiflow init 或手动创建配置文件
配置文件位置 (按优先级排序):
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 访问令牌基于当前仓库主机名自动选择
Workflow:
1. 分析暂存的更改
2. 生成 AI 提交信息和分支名称
3. 创建并推送新分支
4. 创建合并请求
5. 发送企业微信通知
Examples:
aiflow init # 交互式初始化本地配置
aiflow init --global # 交互式初始化全局配置
aiflow # 使用配置文件运行
aiflow --commit-only # 仅提交更改,不创建MR
aiflow -co # 仅提交更改,不创建MR (短参数)
aiflow -cmo # 仅提交更改,不创建MR (短参数)
aiflow -ok sk-123 -gat github.com=ghp_456 # 使用 CLI 参数覆盖配置
aiflow -gat gitlab.example.com=glpat-456 -we true # 多平台访问令牌配置
`);
}
/**
* 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')) {
GitAutoMrApp.showVersion();
await processExit(0);
}
// Show CLI help
if (args.includes('--config-help')) {
logger.info(getCliHelp());
await processExit(0);
}
// Show usage if help requested
if (args.includes('--help') || args.includes('-h')) {
GitAutoMrApp.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');
}
// Check for commit-only mode early (before config validation)
const isCommitOnly = args.includes('--commit-only') || args.includes('-co') || args.includes('-cmo');
// Parse CLI configuration arguments
const cliConfig = parseCliArgs(args);
const app = new GitAutoMrApp();
// Initialize services with configuration and run the MR creation workflow
try {
await app.initializeServices(cliConfig);
// For commit-only mode, skip some validations that are not needed
if (!app.validateConfiguration(!isCommitOnly)) {
await processExit(1);
return;
}
await app.run();
}
catch (error) {
logger.error('❌ Error during aiflow:', error);
await processExit(1);
}
}
}
// 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', 'git-aiflow', import_file].includes(run_file));
isMain && GitAutoMrApp.main().catch(async (error) => {
logger.error('❌ Error during aiflow:', error);
await processExit(1, error);
});
// Handle termination signals
process.on('SIGTERM', async () => {
logger.info('Received SIGTERM signal, shutting down service...');
});
process.on('SIGINT', async () => {
logger.info('Received SIGINT signal, shutting down service...');
});
// Handle uncaught exceptions
process.on('uncaughtException', async (error) => {
logger.error('Uncaught exception', error);
});
process.on('unhandledRejection', async (reason, promise) => {
logger.error('Unhandled Promise rejection', { reason, promise });
});
//# sourceMappingURL=aiflow-app.js.map