Skip to content

Commit 83b282c

Browse files
authored
Merge pull request #8 from BeyteFlow/code
add files
2 parents 9627705 + 847e838 commit 83b282c

13 files changed

Lines changed: 1303 additions & 4 deletions

git-ai/package-lock.json

Lines changed: 481 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

git-ai/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,16 @@
3131
"author": "",
3232
"license": "ISC",
3333
"dependencies": {
34+
"@google/generative-ai": "^0.24.1",
35+
"@octokit/rest": "^22.0.1",
3436
"chalk": "^5.3.0",
3537
"commander": "^12.1.0",
3638
"ink": "^5.0.0",
39+
"pino": "^10.3.1",
40+
"pino-pretty": "^13.1.3",
3741
"react": "^18.2.0",
38-
"simple-git": "^3.27.0"
42+
"simple-git": "^3.27.0",
43+
"zod": "^3.23.8"
3944
},
4045
"devDependencies": {
4146
"@types/node": "^20.11.0",

git-ai/src/cli/pr-command.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import { render } from 'ink';
3+
import { GitService } from '../core/GitService.js';
4+
import { ConfigService } from '../services/ConfigService.js';
5+
import { GitHubService, PullRequestMetadata } from '../services/GitHubService.js';
6+
import { PRList } from '../ui/PRList.js';
7+
import { logger } from '../utils/logger.js';
8+
9+
/**
10+
* Orchestrates the Interactive PR Selection UI
11+
*/
12+
export async function runPRCommand(): Promise<void> {
13+
try {
14+
const configService = new ConfigService();
15+
const gitService = new GitService();
16+
17+
// 1. Get the remote URL to identify the GitHub repository
18+
// Accessing the underlying git instance safely:
19+
const remotes = await (gitService as any).git.getRemotes(true);
20+
const origin = remotes.find((r: any) => r.name === 'origin');
21+
22+
if (!origin || !origin.refs.fetch) {
23+
console.error('❌ Error: No remote "origin" found. Ensure your repo is hosted on GitHub.');
24+
process.exit(1);
25+
}
26+
27+
const githubService = new GitHubService(configService, origin.refs.fetch);
28+
29+
// 2. Launch the Ink TUI
30+
const renderInstance = render(
31+
React.createElement(PRList, {
32+
githubService,
33+
onSelect: (pr: PullRequestMetadata) => {
34+
console.log('\n-----------------------------------');
35+
console.log(`🚀 Selected PR: #${pr.number}`);
36+
console.log(`🔗 URL: ${pr.url}`);
37+
console.log(`🌿 Branch: ${pr.branch} -> ${pr.base}`);
38+
console.log('-----------------------------------\n');
39+
// In a future update, we can trigger gitService.checkout(pr.branch)
40+
renderInstance.unmount();
41+
}
42+
})
43+
);
44+
45+
// 3. Await clean exit
46+
await renderInstance.waitUntilExit();
47+
} catch (error) {
48+
logger.error(error instanceof Error ? error : new Error(String(error)), 'Failed to initialize PR command');
49+
console.error('❌ Critical Error: Could not launch PR interface.');
50+
process.exit(1);
51+
}
52+
}

git-ai/src/cli/resolve-command.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ConflictResolver } from '../services/ConflictResolver.js';
2+
import { AIService } from '../services/AIService.js';
3+
import { GitService } from '../core/GitService.js';
4+
import { ConfigService } from '../services/ConfigService.js';
5+
6+
export async function runResolveCommand() {
7+
try {
8+
const config = new ConfigService();
9+
const git = new GitService();
10+
const ai = new AIService(config);
11+
const resolver = new ConflictResolver(ai, git);
12+
13+
const conflicts = await resolver.getConflicts();
14+
15+
if (conflicts.length === 0) {
16+
console.log('✅ No merge conflicts detected.');
17+
return;
18+
}
19+
20+
console.log(`🔍 Found ${conflicts.length} files with conflicts.`);
21+
22+
for (const conflict of conflicts) {
23+
try {
24+
console.log(`🤖 Analyzing ${conflict.file}...`);
25+
const suggestion = await resolver.suggestResolution(conflict);
26+
27+
console.log(`\n--- AI Suggested Resolution for ${conflict.file} ---`);
28+
console.log(suggestion);
29+
console.log('--------------------------------------------------\n');
30+
} catch (error) {
31+
const message = error instanceof Error ? error.message : String(error);
32+
console.error(`⚠️ Failed to process conflict for ${conflict.file}: ${message}`);
33+
}
34+
35+
// In the final UI/Ink phase, we would add a [Apply] / [Skip] prompt here.
36+
}
37+
} catch (error) {
38+
const message = error instanceof Error ? error.message : String(error);
39+
console.error(`❌ Failed to initialize conflict resolution: ${message}`);
40+
process.exit(1);
41+
}
42+
}

git-ai/src/core/GitService.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { simpleGit, SimpleGit, StatusResult, LogResult } from 'simple-git';
2+
import { logger } from './../utils/logger.js';
3+
4+
export class GitService {
5+
private git: SimpleGit;
6+
7+
constructor(workingDir: string = process.cwd()) {
8+
this.git = simpleGit(workingDir);
9+
}
10+
11+
public async getStatus(): Promise<StatusResult> {
12+
try {
13+
return await this.git.status();
14+
} catch (error) {
15+
logger.error(`Failed to fetch git status: ${error instanceof Error ? error.message : String(error)}`);
16+
throw error;
17+
}
18+
}
19+
20+
public async getDiff(): Promise<string> {
21+
return await this.git.diff(['--cached']);
22+
}
23+
24+
public async commit(message: string): Promise<void> {
25+
const normalizedMessage = message.trim();
26+
if (!normalizedMessage) {
27+
throw new Error('Commit message cannot be empty or whitespace.');
28+
}
29+
30+
try {
31+
await this.git.commit(normalizedMessage);
32+
} catch (error) {
33+
const original = error instanceof Error ? error.message : String(error);
34+
throw new Error(`Failed to create git commit: ${original}`);
35+
}
36+
}
37+
38+
public async getLog(limit: number = 10): Promise<LogResult> {
39+
return await this.git.log({ maxCount: limit });
40+
}
41+
42+
public async getCurrentBranch(): Promise<string> {
43+
const branchData = await this.git.branch();
44+
return branchData.current;
45+
}
46+
}

git-ai/src/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Command } from 'commander';
2+
import { GitService } from './core/GitService.js';
3+
import { ConfigService } from './services/ConfigService.js';
4+
import { AIService } from './services/AIService.js';
5+
import { logger } from './utils/logger.js';
6+
7+
const program = new Command();
8+
const configService = new ConfigService();
9+
const gitService = new GitService();
10+
11+
program
12+
.name('ai-git')
13+
.version('0.1.0');
14+
15+
program
16+
.command('ai-commit')
17+
.description('Generate a commit message using AI and commit staged changes')
18+
.action(async () => {
19+
try {
20+
const aiService = new AIService(configService);
21+
const diff = await gitService.getDiff();
22+
if (!diff) {
23+
console.log('No staged changes found. Please stage files first.');
24+
return;
25+
}
26+
27+
console.log('🤖 Generating commit message...');
28+
const message = await aiService.generateCommitMessage(diff);
29+
30+
console.log(`\nSuggested Message: "${message}"`);
31+
await gitService.commit(message);
32+
console.log('✅ Changes committed successfully.');
33+
} catch (err) {
34+
logger.error(`AI Commit failed: ${err instanceof Error ? err.message : String(err)}`);
35+
process.exit(1);
36+
}
37+
});
38+
39+
program.parse(process.argv);

git-ai/src/services/AIService.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { GoogleGenerativeAI, GenerativeModel } from "@google/generative-ai";
2+
import { ConfigService } from "./ConfigService.js";
3+
import { logger } from "../utils/logger.js";
4+
5+
export interface AIProvider {
6+
generateCommitMessage(diff: string): Promise<string>;
7+
analyzeConflicts(conflictFileContents: Record<string, string>): Promise<string>;
8+
generateContent(prompt: string): Promise<string>;
9+
}
10+
11+
export class AIService implements AIProvider {
12+
private genAI: GoogleGenerativeAI | null = null;
13+
private model: GenerativeModel | null = null;
14+
private configService: ConfigService;
15+
16+
constructor(configService: ConfigService) {
17+
this.configService = configService;
18+
this.initClient();
19+
}
20+
21+
private initClient(): void {
22+
const config = this.configService.getConfig();
23+
24+
// Ensure we only init if the provider is gemini
25+
if (config.ai.provider === "gemini") {
26+
if (!config.ai.apiKey) {
27+
throw new Error("Gemini API Key is missing in .aigitrc");
28+
}
29+
30+
this.genAI = new GoogleGenerativeAI(config.ai.apiKey);
31+
this.model = this.genAI.getGenerativeModel({
32+
model: config.ai.model || "gemini-1.5-flash",
33+
generationConfig: {
34+
temperature: 0.2,
35+
topP: 0.8,
36+
maxOutputTokens: 200,
37+
},
38+
});
39+
} else {
40+
throw new Error(`Unsupported AI provider: ${config.ai.provider}`);
41+
}
42+
}
43+
44+
/**
45+
* Generates a conventional commit message based on git diff
46+
*/
47+
public async generateCommitMessage(diff: string): Promise<string> {
48+
if (!this.model) {
49+
throw new Error("Gemini AI model not initialized. Check your config.");
50+
}
51+
52+
const prompt = `
53+
You are an expert software engineer.
54+
Generate a professional, concise conventional commit message based on this git diff:
55+
56+
"${diff}"
57+
58+
Instructions:
59+
1. Use the format: <type>(<scope>): <description>
60+
2. Common types: feat, fix, docs, style, refactor, test, chore.
61+
3. Description should be in present tense and lowercase.
62+
4. Return ONLY the commit message text.
63+
`;
64+
65+
try {
66+
const result = await this.model.generateContent(prompt);
67+
const response = await result.response;
68+
const text = response.text().trim();
69+
70+
// Clean up potential markdown formatting if Gemini returns backticks
71+
return text.replace(/`/g, "");
72+
} catch (error) {
73+
logger.error(
74+
`Gemini API Error: ${error instanceof Error ? error.message : String(error)}`,
75+
);
76+
throw new Error("Failed to generate commit message via Gemini.");
77+
}
78+
}
79+
80+
/**
81+
* Analyzes merge conflicts and suggests resolutions
82+
*/
83+
public async analyzeConflicts(conflictFileContents: Record<string, string>): Promise<string> {
84+
if (!this.model) throw new Error("AI Service not ready");
85+
86+
const conflictsWithContent = Object.entries(conflictFileContents)
87+
.map(([fileName, content]) => `FILE: ${fileName}\n${content}`)
88+
.join("\n\n");
89+
90+
const prompt = `Analyze the following files currently in a git conflict state and provide a high-level summary of the clashing changes. Include key differences and likely intent from both sides of each conflict marker block.\n\n${conflictsWithContent}`;
91+
92+
try {
93+
const result = await this.model.generateContent(prompt);
94+
return result.response.text();
95+
} catch (error) {
96+
logger.error(
97+
`Conflict Analysis Error: ${error instanceof Error ? error.message : String(error)}`,
98+
);
99+
return "Could not analyze conflicts at this time.";
100+
}
101+
}
102+
103+
public async generateContent(prompt: string): Promise<string> {
104+
if (!this.model) {
105+
throw new Error("Gemini AI model not initialized. Check your config.");
106+
}
107+
108+
try {
109+
const result = await this.model.generateContent(prompt);
110+
const response = await result.response;
111+
return response.text();
112+
} catch (error) {
113+
const errorMsg = error instanceof Error ? error.message : String(error);
114+
logger.error(
115+
`AI generateContent Error: ${errorMsg}`,
116+
);
117+
throw new Error("Failed to generate content via AI service.");
118+
}
119+
}
120+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import os from 'os';
4+
import { z } from 'zod';
5+
6+
export const ConfigSchema = z.object({
7+
ai: z.object({
8+
provider: z.enum(['openai', 'gemini']),
9+
apiKey: z.string(),
10+
model: z.string().optional(),
11+
}),
12+
github: z.object({
13+
token: z.string().min(1),
14+
}).optional(),
15+
git: z.object({
16+
autoStage: z.boolean().default(false),
17+
messagePrefix: z.string().optional(),
18+
}),
19+
ui: z.object({
20+
theme: z.enum(['dark', 'light', 'system']).default('dark'),
21+
showIcons: z.boolean().default(true),
22+
}),
23+
});
24+
25+
export type Config = z.infer<typeof ConfigSchema>;
26+
27+
export class ConfigService {
28+
private static readonly CONFIG_PATH = path.join(os.homedir(), '.aigitrc');
29+
private config: Config | null = null;
30+
31+
constructor() {
32+
this.loadConfig();
33+
}
34+
35+
private loadConfig(): void {
36+
if (!fs.existsSync(ConfigService.CONFIG_PATH)) {
37+
this.config = null;
38+
return;
39+
}
40+
41+
try {
42+
const rawConfig = JSON.parse(fs.readFileSync(ConfigService.CONFIG_PATH, 'utf-8'));
43+
this.config = ConfigSchema.parse(rawConfig);
44+
} catch (error) {
45+
throw new Error(`Invalid configuration file at ${ConfigService.CONFIG_PATH}: ${error}`);
46+
}
47+
}
48+
49+
public getConfig(): Config {
50+
if (!this.config) {
51+
throw new Error("Configuration not initialized. Please run 'ai-git init'.");
52+
}
53+
return this.config;
54+
}
55+
56+
public saveConfig(newConfig: Config): void {
57+
const validated = ConfigSchema.parse(newConfig);
58+
fs.writeFileSync(ConfigService.CONFIG_PATH, JSON.stringify(validated, null, 2), { mode: 0o600 });
59+
this.config = validated;
60+
}
61+
}

0 commit comments

Comments
 (0)