Files
puaros/packages/ipuaro/src/infrastructure/tools/git/GitStatusTool.ts
imfozilbek f5f904a847 feat(ipuaro): add git and run tools (v0.9.0)
Git tools:
- GitStatusTool: repository status (branch, staged, modified, untracked)
- GitDiffTool: uncommitted changes with diff output
- GitCommitTool: create commits with confirmation

Run tools:
- CommandSecurity: blacklist/whitelist shell command validation
- RunCommandTool: execute shell commands with security checks
- RunTestsTool: auto-detect and run vitest/jest/mocha/npm test

All 18 planned tools now implemented.
Tests: 1086 (+233), Coverage: 98.08%
2025-12-01 02:54:29 +05:00

130 lines
3.9 KiB
TypeScript

import { simpleGit, type SimpleGit, type StatusResult } from "simple-git"
import type { ITool, ToolContext, ToolParameterSchema } from "../../../domain/services/ITool.js"
import {
createErrorResult,
createSuccessResult,
type ToolResult,
} from "../../../domain/value-objects/ToolResult.js"
/**
* File status entry in git status.
*/
export interface FileStatusEntry {
/** Relative file path */
path: string
/** Working directory status (modified, deleted, etc.) */
workingDir: string
/** Index/staging status */
index: string
}
/**
* Result data from git_status tool.
*/
export interface GitStatusResult {
/** Current branch name */
branch: string
/** Tracking branch (e.g., origin/main) */
tracking: string | null
/** Number of commits ahead of tracking */
ahead: number
/** Number of commits behind tracking */
behind: number
/** Files staged for commit */
staged: FileStatusEntry[]
/** Modified files not staged */
modified: FileStatusEntry[]
/** Untracked files */
untracked: string[]
/** Files with merge conflicts */
conflicted: string[]
/** Whether working directory is clean */
isClean: boolean
}
/**
* Tool for getting git repository status.
* Returns branch info, staged/modified/untracked files.
*/
export class GitStatusTool implements ITool {
readonly name = "git_status"
readonly description =
"Get current git repository status. " +
"Returns branch name, staged files, modified files, and untracked files."
readonly parameters: ToolParameterSchema[] = []
readonly requiresConfirmation = false
readonly category = "git" as const
private readonly gitFactory: (basePath: string) => SimpleGit
constructor(gitFactory?: (basePath: string) => SimpleGit) {
this.gitFactory = gitFactory ?? ((basePath: string) => simpleGit(basePath))
}
validateParams(_params: Record<string, unknown>): string | null {
return null
}
async execute(_params: Record<string, unknown>, ctx: ToolContext): Promise<ToolResult> {
const startTime = Date.now()
const callId = `${this.name}-${String(startTime)}`
try {
const git = this.gitFactory(ctx.projectRoot)
const isRepo = await git.checkIsRepo()
if (!isRepo) {
return createErrorResult(
callId,
"Not a git repository. Initialize with 'git init' first.",
Date.now() - startTime,
)
}
const status = await git.status()
const result = this.formatStatus(status)
return createSuccessResult(callId, result, Date.now() - startTime)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return createErrorResult(callId, message, Date.now() - startTime)
}
}
/**
* Format simple-git StatusResult into our result structure.
*/
private formatStatus(status: StatusResult): GitStatusResult {
const staged: FileStatusEntry[] = []
const modified: FileStatusEntry[] = []
for (const file of status.files) {
const entry: FileStatusEntry = {
path: file.path,
workingDir: file.working_dir,
index: file.index,
}
if (file.index !== " " && file.index !== "?") {
staged.push(entry)
}
if (file.working_dir !== " " && file.working_dir !== "?") {
modified.push(entry)
}
}
return {
branch: status.current ?? "HEAD (detached)",
tracking: status.tracking ?? null,
ahead: status.ahead,
behind: status.behind,
staged,
modified,
untracked: status.not_added,
conflicted: status.conflicted,
isClean: status.isClean(),
}
}
}