-
Notifications
You must be signed in to change notification settings - Fork 0
add files #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
add files #8
Changes from 11 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
6bf3b19
add files
naheel0 7099720
Initial plan
Copilot 33ce722
fix: add missing npm dependencies to resolve TypeScript lint errors
Copilot c846751
Initial plan
Copilot 57adf18
Merge pull request #9 from BeyteFlow/copilot/sub-pr-8
naheel0 173466c
fix: add missing deps, fix security/quality issues from review
Copilot fe468f7
Merge branch 'code' into copilot/sub-pr-8-again
naheel0 bc03c6e
Merge pull request #10 from BeyteFlow/copilot/sub-pr-8-again
naheel0 a7d7477
Initial plan
Copilot 0a9918a
fix: install deps and replace short-write with writeFile in ConflictR…
Copilot a22ffaa
Merge pull request #11 from BeyteFlow/copilot/sub-pr-8
jaseel0 70a25c2
Initial plan
Copilot d177c5f
fix: preserve file mode in ConflictResolver atomic write
Copilot 847e838
Merge pull request #12 from BeyteFlow/copilot/sub-pr-8
naheel0 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import React from 'react'; | ||
| import { render } from 'ink'; | ||
| import { GitService } from '../core/GitService.js'; | ||
| import { ConfigService } from '../services/ConfigService.js'; | ||
| import { GitHubService, PullRequestMetadata } from '../services/GitHubService.js'; | ||
| import { PRList } from '../ui/PRList.js'; | ||
| import { logger } from '../utils/logger.js'; | ||
|
|
||
| /** | ||
| * Orchestrates the Interactive PR Selection UI | ||
| */ | ||
| export async function runPRCommand(): Promise<void> { | ||
| try { | ||
| const configService = new ConfigService(); | ||
| const gitService = new GitService(); | ||
|
|
||
| // 1. Get the remote URL to identify the GitHub repository | ||
| // Accessing the underlying git instance safely: | ||
| const remotes = await (gitService as any).git.getRemotes(true); | ||
| const origin = remotes.find((r: any) => r.name === 'origin'); | ||
|
|
||
| if (!origin || !origin.refs.fetch) { | ||
| console.error('❌ Error: No remote "origin" found. Ensure your repo is hosted on GitHub.'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const githubService = new GitHubService(configService, origin.refs.fetch); | ||
|
|
||
| // 2. Launch the Ink TUI | ||
| const renderInstance = render( | ||
| React.createElement(PRList, { | ||
| githubService, | ||
| onSelect: (pr: PullRequestMetadata) => { | ||
| console.log('\n-----------------------------------'); | ||
| console.log(`🚀 Selected PR: #${pr.number}`); | ||
| console.log(`🔗 URL: ${pr.url}`); | ||
| console.log(`🌿 Branch: ${pr.branch} -> ${pr.base}`); | ||
| console.log('-----------------------------------\n'); | ||
| // In a future update, we can trigger gitService.checkout(pr.branch) | ||
| renderInstance.unmount(); | ||
| } | ||
| }) | ||
| ); | ||
|
|
||
| // 3. Await clean exit | ||
| await renderInstance.waitUntilExit(); | ||
| } catch (error) { | ||
| logger.error(error instanceof Error ? error : new Error(String(error)), 'Failed to initialize PR command'); | ||
| console.error('❌ Critical Error: Could not launch PR interface.'); | ||
| process.exit(1); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { ConflictResolver } from '../services/ConflictResolver.js'; | ||
| import { AIService } from '../services/AIService.js'; | ||
| import { GitService } from '../core/GitService.js'; | ||
| import { ConfigService } from '../services/ConfigService.js'; | ||
|
|
||
| export async function runResolveCommand() { | ||
| try { | ||
| const config = new ConfigService(); | ||
| const git = new GitService(); | ||
| const ai = new AIService(config); | ||
| const resolver = new ConflictResolver(ai, git); | ||
|
|
||
| const conflicts = await resolver.getConflicts(); | ||
|
|
||
| if (conflicts.length === 0) { | ||
| console.log('✅ No merge conflicts detected.'); | ||
| return; | ||
| } | ||
|
|
||
| console.log(`🔍 Found ${conflicts.length} files with conflicts.`); | ||
|
|
||
| for (const conflict of conflicts) { | ||
| try { | ||
| console.log(`🤖 Analyzing ${conflict.file}...`); | ||
| const suggestion = await resolver.suggestResolution(conflict); | ||
|
|
||
| console.log(`\n--- AI Suggested Resolution for ${conflict.file} ---`); | ||
| console.log(suggestion); | ||
| console.log('--------------------------------------------------\n'); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| console.error(`⚠️ Failed to process conflict for ${conflict.file}: ${message}`); | ||
| } | ||
|
|
||
| // In the final UI/Ink phase, we would add a [Apply] / [Skip] prompt here. | ||
| } | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| console.error(`❌ Failed to initialize conflict resolution: ${message}`); | ||
| process.exit(1); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { simpleGit, SimpleGit, StatusResult, LogResult } from 'simple-git'; | ||
| import { logger } from './../utils/logger.js'; | ||
|
|
||
| export class GitService { | ||
| private git: SimpleGit; | ||
|
|
||
| constructor(workingDir: string = process.cwd()) { | ||
| this.git = simpleGit(workingDir); | ||
| } | ||
|
|
||
| public async getStatus(): Promise<StatusResult> { | ||
| try { | ||
| return await this.git.status(); | ||
| } catch (error) { | ||
| logger.error(`Failed to fetch git status: ${error instanceof Error ? error.message : String(error)}`); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| public async getDiff(): Promise<string> { | ||
| return await this.git.diff(['--cached']); | ||
| } | ||
|
|
||
| public async commit(message: string): Promise<void> { | ||
| const normalizedMessage = message.trim(); | ||
| if (!normalizedMessage) { | ||
| throw new Error('Commit message cannot be empty or whitespace.'); | ||
| } | ||
|
|
||
| try { | ||
| await this.git.commit(normalizedMessage); | ||
| } catch (error) { | ||
| const original = error instanceof Error ? error.message : String(error); | ||
| throw new Error(`Failed to create git commit: ${original}`); | ||
| } | ||
| } | ||
|
|
||
| public async getLog(limit: number = 10): Promise<LogResult> { | ||
| return await this.git.log({ maxCount: limit }); | ||
| } | ||
|
|
||
| public async getCurrentBranch(): Promise<string> { | ||
| const branchData = await this.git.branch(); | ||
| return branchData.current; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { Command } from 'commander'; | ||
| import { GitService } from './core/GitService.js'; | ||
| import { ConfigService } from './services/ConfigService.js'; | ||
| import { AIService } from './services/AIService.js'; | ||
| import { logger } from './utils/logger.js'; | ||
|
|
||
| const program = new Command(); | ||
| const configService = new ConfigService(); | ||
| const gitService = new GitService(); | ||
|
|
||
| program | ||
| .name('ai-git') | ||
| .version('0.1.0'); | ||
|
|
||
| program | ||
| .command('ai-commit') | ||
| .description('Generate a commit message using AI and commit staged changes') | ||
| .action(async () => { | ||
| try { | ||
| const aiService = new AIService(configService); | ||
| const diff = await gitService.getDiff(); | ||
| if (!diff) { | ||
| console.log('No staged changes found. Please stage files first.'); | ||
| return; | ||
| } | ||
|
|
||
| console.log('🤖 Generating commit message...'); | ||
| const message = await aiService.generateCommitMessage(diff); | ||
|
|
||
| console.log(`\nSuggested Message: "${message}"`); | ||
| await gitService.commit(message); | ||
| console.log('✅ Changes committed successfully.'); | ||
| } catch (err) { | ||
| logger.error(`AI Commit failed: ${err instanceof Error ? err.message : String(err)}`); | ||
| process.exit(1); | ||
| } | ||
| }); | ||
|
|
||
| program.parse(process.argv); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import { GoogleGenerativeAI, GenerativeModel } from "@google/generative-ai"; | ||
| import { ConfigService } from "./ConfigService.js"; | ||
| import { logger } from "../utils/logger.js"; | ||
|
|
||
| export interface AIProvider { | ||
| generateCommitMessage(diff: string): Promise<string>; | ||
| analyzeConflicts(conflictFileContents: Record<string, string>): Promise<string>; | ||
| generateContent(prompt: string): Promise<string>; | ||
| } | ||
|
|
||
| export class AIService implements AIProvider { | ||
| private genAI: GoogleGenerativeAI | null = null; | ||
| private model: GenerativeModel | null = null; | ||
| private configService: ConfigService; | ||
|
|
||
| constructor(configService: ConfigService) { | ||
| this.configService = configService; | ||
| this.initClient(); | ||
| } | ||
|
|
||
| private initClient(): void { | ||
| const config = this.configService.getConfig(); | ||
|
|
||
| // Ensure we only init if the provider is gemini | ||
| if (config.ai.provider === "gemini") { | ||
| if (!config.ai.apiKey) { | ||
| throw new Error("Gemini API Key is missing in .aigitrc"); | ||
| } | ||
|
|
||
| this.genAI = new GoogleGenerativeAI(config.ai.apiKey); | ||
| this.model = this.genAI.getGenerativeModel({ | ||
| model: config.ai.model || "gemini-1.5-flash", | ||
| generationConfig: { | ||
| temperature: 0.2, | ||
| topP: 0.8, | ||
| maxOutputTokens: 200, | ||
| }, | ||
| }); | ||
| } else { | ||
| throw new Error(`Unsupported AI provider: ${config.ai.provider}`); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * Generates a conventional commit message based on git diff | ||
| */ | ||
| public async generateCommitMessage(diff: string): Promise<string> { | ||
| if (!this.model) { | ||
| throw new Error("Gemini AI model not initialized. Check your config."); | ||
| } | ||
|
|
||
| const prompt = ` | ||
| You are an expert software engineer. | ||
| Generate a professional, concise conventional commit message based on this git diff: | ||
|
|
||
| "${diff}" | ||
|
|
||
| Instructions: | ||
| 1. Use the format: <type>(<scope>): <description> | ||
| 2. Common types: feat, fix, docs, style, refactor, test, chore. | ||
| 3. Description should be in present tense and lowercase. | ||
| 4. Return ONLY the commit message text. | ||
| `; | ||
|
|
||
| try { | ||
| const result = await this.model.generateContent(prompt); | ||
| const response = await result.response; | ||
| const text = response.text().trim(); | ||
|
|
||
| // Clean up potential markdown formatting if Gemini returns backticks | ||
| return text.replace(/`/g, ""); | ||
| } catch (error) { | ||
| logger.error( | ||
| `Gemini API Error: ${error instanceof Error ? error.message : String(error)}`, | ||
| ); | ||
| throw new Error("Failed to generate commit message via Gemini."); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Analyzes merge conflicts and suggests resolutions | ||
| */ | ||
| public async analyzeConflicts(conflictFileContents: Record<string, string>): Promise<string> { | ||
| if (!this.model) throw new Error("AI Service not ready"); | ||
|
|
||
| const conflictsWithContent = Object.entries(conflictFileContents) | ||
| .map(([fileName, content]) => `FILE: ${fileName}\n${content}`) | ||
| .join("\n\n"); | ||
|
|
||
| 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}`; | ||
|
|
||
| try { | ||
| const result = await this.model.generateContent(prompt); | ||
| return result.response.text(); | ||
| } catch (error) { | ||
| logger.error( | ||
| `Conflict Analysis Error: ${error instanceof Error ? error.message : String(error)}`, | ||
| ); | ||
| return "Could not analyze conflicts at this time."; | ||
| } | ||
| } | ||
|
|
||
| public async generateContent(prompt: string): Promise<string> { | ||
| if (!this.model) { | ||
| throw new Error("Gemini AI model not initialized. Check your config."); | ||
| } | ||
|
|
||
| try { | ||
| const result = await this.model.generateContent(prompt); | ||
| const response = await result.response; | ||
| return response.text(); | ||
| } catch (error) { | ||
| const errorMsg = error instanceof Error ? error.message : String(error); | ||
| logger.error( | ||
| `AI generateContent Error: ${errorMsg}`, | ||
| ); | ||
| throw new Error("Failed to generate content via AI service."); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import fs from 'fs'; | ||
| import path from 'path'; | ||
| import os from 'os'; | ||
| import { z } from 'zod'; | ||
|
|
||
| export const ConfigSchema = z.object({ | ||
| ai: z.object({ | ||
| provider: z.enum(['openai', 'gemini']), | ||
| apiKey: z.string(), | ||
| model: z.string().optional(), | ||
| }), | ||
| github: z.object({ | ||
| token: z.string().min(1), | ||
| }).optional(), | ||
| git: z.object({ | ||
| autoStage: z.boolean().default(false), | ||
| messagePrefix: z.string().optional(), | ||
| }), | ||
| ui: z.object({ | ||
| theme: z.enum(['dark', 'light', 'system']).default('dark'), | ||
| showIcons: z.boolean().default(true), | ||
| }), | ||
| }); | ||
|
|
||
| export type Config = z.infer<typeof ConfigSchema>; | ||
|
|
||
| export class ConfigService { | ||
| private static readonly CONFIG_PATH = path.join(os.homedir(), '.aigitrc'); | ||
| private config: Config | null = null; | ||
|
|
||
| constructor() { | ||
| this.loadConfig(); | ||
| } | ||
|
|
||
| private loadConfig(): void { | ||
| if (!fs.existsSync(ConfigService.CONFIG_PATH)) { | ||
| this.config = null; | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const rawConfig = JSON.parse(fs.readFileSync(ConfigService.CONFIG_PATH, 'utf-8')); | ||
| this.config = ConfigSchema.parse(rawConfig); | ||
| } catch (error) { | ||
| throw new Error(`Invalid configuration file at ${ConfigService.CONFIG_PATH}: ${error}`); | ||
| } | ||
| } | ||
|
|
||
| public getConfig(): Config { | ||
| if (!this.config) { | ||
| throw new Error("Configuration not initialized. Please run 'ai-git init'."); | ||
| } | ||
| return this.config; | ||
| } | ||
|
|
||
| public saveConfig(newConfig: Config): void { | ||
| const validated = ConfigSchema.parse(newConfig); | ||
| fs.writeFileSync(ConfigService.CONFIG_PATH, JSON.stringify(validated, null, 2), { mode: 0o600 }); | ||
| this.config = validated; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.