init project
This commit is contained in:
Vendored
+249
@@ -0,0 +1,249 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
async function git(args) {
|
||||
return runCommand('git', args, {
|
||||
cwd: config.workspaceRoot,
|
||||
timeoutMs: config.defaultCommandTimeoutMs,
|
||||
});
|
||||
}
|
||||
export function registerGitStatusTool(server) {
|
||||
server.registerTool('git_status', {
|
||||
title: 'Git Status',
|
||||
description: 'Return the current branch, ahead/behind counts vs upstream (when available), and porcelain status lines. Read-only.',
|
||||
inputSchema: {},
|
||||
}, async () => {
|
||||
try {
|
||||
const branchResult = await git(['rev-parse', '--abbrev-ref', 'HEAD']);
|
||||
const porcelain = await git(['status', '--porcelain=v1']);
|
||||
const aheadBehind = await git(['rev-list', '--left-right', '--count', '@{upstream}...HEAD']);
|
||||
let ahead = null;
|
||||
let behind = null;
|
||||
if (!aheadBehind.failed && aheadBehind.output.trim()) {
|
||||
const parts = aheadBehind.output.trim().split(/\s+/);
|
||||
behind = Number.parseInt(parts[0] ?? '0', 10);
|
||||
ahead = Number.parseInt(parts[1] ?? '0', 10);
|
||||
}
|
||||
return jsonResult({
|
||||
branch: branchResult.output.trim() || null,
|
||||
ahead,
|
||||
behind,
|
||||
porcelain: porcelain.output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trimEnd())
|
||||
.filter(Boolean),
|
||||
raw: porcelain.output,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGitDiffTool(server) {
|
||||
server.registerTool('git_diff', {
|
||||
title: 'Git Diff',
|
||||
description: 'Show staged and/or unstaged diffs, optionally scoped to a workspace-relative path. Read-only; output is truncated.',
|
||||
inputSchema: {
|
||||
staged: z.boolean().optional().describe('If true, show only staged changes (`git diff --cached`).'),
|
||||
path: z.string().optional().describe('Optional workspace-relative file or directory to diff.'),
|
||||
},
|
||||
}, async ({ staged, path: filePath }) => {
|
||||
try {
|
||||
const args = ['diff'];
|
||||
if (staged)
|
||||
args.push('--cached');
|
||||
if (filePath) {
|
||||
const absolute = resolveWorkspacePath(config.workspaceRoot, filePath);
|
||||
args.push('--', absolute);
|
||||
}
|
||||
const result = await git(args);
|
||||
return jsonResult({
|
||||
staged: Boolean(staged),
|
||||
path: filePath ?? null,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
output: result.output,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGitDiffSummarizeTool(server) {
|
||||
server.registerTool('git_diff_summarize', {
|
||||
title: 'Git Diff Summarize',
|
||||
description: 'Summarize staged and/or unstaged changes: per-file insertions/deletions plus totals. Read-only; uses `git diff --numstat` (no full patch).',
|
||||
inputSchema: {
|
||||
staged: z.boolean().optional().describe('If true, summarize only staged changes (`git diff --cached --numstat`).'),
|
||||
path: z.string().optional().describe('Optional workspace-relative file or directory to summarize.'),
|
||||
},
|
||||
}, async ({ staged, path: filePath }) => {
|
||||
try {
|
||||
const args = ['diff', '--numstat'];
|
||||
if (staged)
|
||||
args.push('--cached');
|
||||
if (filePath) {
|
||||
const absolute = resolveWorkspacePath(config.workspaceRoot, filePath);
|
||||
args.push('--', absolute);
|
||||
}
|
||||
const result = await git(args);
|
||||
const files = summarizeNumstat(result.output);
|
||||
const insertions = files.reduce((sum, f) => sum + f.insertions, 0);
|
||||
const deletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
||||
return jsonResult({
|
||||
staged: Boolean(staged),
|
||||
path: filePath ?? null,
|
||||
fileCount: files.length,
|
||||
insertions,
|
||||
deletions,
|
||||
files,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
/** Parse `git diff --numstat` lines into structured file stats. Exported for tests. */
|
||||
export function summarizeNumstat(output) {
|
||||
return output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [insRaw, delRaw, ...pathParts] = line.split(/\t/);
|
||||
const filePath = pathParts.join('\t') || '';
|
||||
const binary = insRaw === '-' || delRaw === '-';
|
||||
return {
|
||||
path: filePath,
|
||||
insertions: binary ? 0 : Number.parseInt(insRaw ?? '0', 10) || 0,
|
||||
deletions: binary ? 0 : Number.parseInt(delRaw ?? '0', 10) || 0,
|
||||
binary,
|
||||
};
|
||||
})
|
||||
.filter((row) => row.path.length > 0);
|
||||
}
|
||||
export function registerGitLogTool(server) {
|
||||
server.registerTool('git_log', {
|
||||
title: 'Git Log',
|
||||
description: 'Show recent commits (hash, author, date, subject). Read-only.',
|
||||
inputSchema: {
|
||||
n: z.number().int().min(1).max(100).optional().default(10).describe('Number of commits to return.'),
|
||||
path: z.string().optional().describe('Optional workspace-relative path to limit history.'),
|
||||
},
|
||||
}, async ({ n, path: filePath }) => {
|
||||
try {
|
||||
const args = ['log', `-n`, String(n ?? 10), '--pretty=format:%H%x09%an%x09%ad%x09%s', '--date=iso'];
|
||||
if (filePath) {
|
||||
const absolute = resolveWorkspacePath(config.workspaceRoot, filePath);
|
||||
args.push('--', absolute);
|
||||
}
|
||||
const result = await git(args);
|
||||
const commits = result.output
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [hash, author, date, ...subjectParts] = line.split('\t');
|
||||
return {
|
||||
hash: hash ?? '',
|
||||
author: author ?? '',
|
||||
date: date ?? '',
|
||||
subject: subjectParts.join('\t'),
|
||||
};
|
||||
});
|
||||
return jsonResult({ n: n ?? 10, path: filePath ?? null, commits });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGitBranchTool(server) {
|
||||
server.registerTool('git_branch', {
|
||||
title: 'Git Branch',
|
||||
description: 'List local branches, or create a new branch. Creating a branch requires confirm: true. Never force-deletes or force-pushes.',
|
||||
inputSchema: {
|
||||
action: z.enum(['list', 'create']).default('list').describe('list (default) or create a branch.'),
|
||||
name: z.string().optional().describe('Branch name (required when action is create).'),
|
||||
confirm: z.boolean().optional().describe('Required as true when creating a branch.'),
|
||||
},
|
||||
}, async ({ action, name, confirm }) => {
|
||||
try {
|
||||
if (action === 'list') {
|
||||
const result = await git(['branch', '--list', '--format=%(refname:short)%09%(HEAD)']);
|
||||
const branches = result.output
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [branchName, head] = line.split('\t');
|
||||
return { name: branchName ?? '', current: head === '*' };
|
||||
});
|
||||
return jsonResult({ action: 'list', branches });
|
||||
}
|
||||
if (!name || !name.trim()) {
|
||||
return errorResult('Branch name is required when action is "create".');
|
||||
}
|
||||
if (confirm !== true) {
|
||||
return errorResult('Creating a branch requires confirm: true.');
|
||||
}
|
||||
if (!/^[A-Za-z0-9._/-]+$/.test(name) || name.includes('..')) {
|
||||
return errorResult(`Invalid branch name "${name}".`);
|
||||
}
|
||||
const result = await git(['branch', name]);
|
||||
return jsonResult({
|
||||
action: 'create',
|
||||
name,
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
output: result.output,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
export function registerGitCommitTool(server) {
|
||||
server.registerTool('git_commit', {
|
||||
title: 'Git Commit',
|
||||
description: 'Stage workspace-relative paths (or all changes with paths: ["."]) and create a commit. Requires confirm: true. Does not amend, force, or push.',
|
||||
inputSchema: {
|
||||
message: z.string().min(1).describe('Commit message.'),
|
||||
paths: z
|
||||
.array(z.string())
|
||||
.min(1)
|
||||
.describe('Workspace-relative paths to stage before committing. Use ["."] for all changes.'),
|
||||
confirm: z.boolean().optional().describe('Must be true to create a commit.'),
|
||||
},
|
||||
}, async ({ message, paths, confirm }) => {
|
||||
try {
|
||||
if (confirm !== true) {
|
||||
return errorResult('git_commit requires confirm: true.');
|
||||
}
|
||||
const absolutePaths = paths.map((p) => resolveWorkspacePath(config.workspaceRoot, p));
|
||||
const addResult = await git(['add', '--', ...absolutePaths]);
|
||||
if (addResult.failed) {
|
||||
return errorResult(`git add failed:\n${addResult.output}`);
|
||||
}
|
||||
const commitResult = await git(['commit', '-m', message]);
|
||||
return jsonResult({
|
||||
message,
|
||||
paths,
|
||||
staged: absolutePaths,
|
||||
exitCode: commitResult.exitCode,
|
||||
failed: commitResult.failed,
|
||||
output: commitResult.output,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=gitTools.js.map
|
||||
Reference in New Issue
Block a user