/**
* Git Remote Tool
*
* Comprehensive Git remote management tool providing remote repository operations.
* Supports add, remove, rename, show, set-url, and prune operations.
*
* Operations: add, remove, rename, show, set-url, prune
*/
import { GitCommandExecutor, GitCommandResult } from '../utils/git-command-executor.js';
import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js';
import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js';
import { configManager } from '../config.js';
export interface GitRemoteParams extends ToolParams {
action: 'add' | 'remove' | 'rename' | 'show' | 'set-url' | 'prune' | 'list';
// Remote parameters
name?: string; // Remote name (required for most operations)
url?: string; // Remote URL (required for add, set-url)
newName?: string; // New remote name (required for rename)
// Options
fetch?: boolean; // Fetch after add (for add action)
push?: boolean; // Set push URL (for set-url action)
all?: boolean; // Show all remotes (for show action)
verbose?: boolean; // Verbose output (for show action)
dryRun?: boolean; // Dry run (for prune action)
}
export class GitRemoteTool {
private gitExecutor: GitCommandExecutor;
constructor() {
this.gitExecutor = new GitCommandExecutor();
}
/**
* Execute git-remote operation
*/
async execute(params: GitRemoteParams): Promise<ToolResult> {
const startTime = Date.now();
try {
// Validate basic parameters
const validation = ParameterValidator.validateToolParams('git-remote', params);
if (!validation.isValid) {
return OperationErrorHandler.createToolError(
'VALIDATION_ERROR',
`Parameter validation failed: ${validation.errors.join(', ')}`,
params.action,
{ validationErrors: validation.errors },
validation.suggestions
);
}
// Validate operation-specific parameters
const operationValidation = this.validateOperationParams(params);
if (!operationValidation.isValid) {
return OperationErrorHandler.createToolError(
'VALIDATION_ERROR',
`Operation validation failed: ${operationValidation.errors.join(', ')}`,
params.action,
{ validationErrors: operationValidation.errors },
operationValidation.suggestions
);
}
// Check if it's a Git repository
const isRepo = await this.gitExecutor.isGitRepository(params.projectPath);
if (!isRepo) {
return OperationErrorHandler.createToolError(
'NOT_A_GIT_REPOSITORY',
'The specified path is not a Git repository',
params.action,
{ projectPath: params.projectPath },
['Initialize a Git repository first with: git init']
);
}
// Route to appropriate handler
switch (params.action) {
case 'add':
return await this.handleAdd(params, startTime);
case 'remove':
return await this.handleRemove(params, startTime);
case 'rename':
return await this.handleRename(params, startTime);
case 'show':
return await this.handleShow(params, startTime);
case 'set-url':
return await this.handleSetUrl(params, startTime);
case 'prune':
return await this.handlePrune(params, startTime);
case 'list':
return await this.handleList(params, startTime);
default:
return OperationErrorHandler.createToolError(
'UNSUPPORTED_OPERATION',
`Operation '${params.action}' is not supported`,
params.action,
{},
['Use one of: add, remove, rename, show, set-url, prune, list']
);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'EXECUTION_ERROR',
`Failed to execute ${params.action}: ${errorMessage}`,
params.action,
{ error: errorMessage },
['Check the error details and try again']
);
}
}
/**
* Validate operation-specific parameters
*/
private validateOperationParams(params: GitRemoteParams): { isValid: boolean; errors: string[]; suggestions: string[] } {
const errors: string[] = [];
const suggestions: string[] = [];
switch (params.action) {
case 'add':
if (!params.name) {
errors.push('Remote name is required for add operation');
suggestions.push('Provide a remote name (e.g., "origin", "upstream")');
}
if (!params.url) {
errors.push('Remote URL is required for add operation');
suggestions.push('Provide a remote URL (e.g., "https://github.com/user/repo.git")');
}
break;
case 'remove':
if (!params.name) {
errors.push('Remote name is required for remove operation');
suggestions.push('Provide the name of the remote to remove');
}
break;
case 'rename':
if (!params.name) {
errors.push('Current remote name is required for rename operation');
suggestions.push('Provide the current remote name');
}
if (!params.newName) {
errors.push('New remote name is required for rename operation');
suggestions.push('Provide the new remote name');
}
break;
case 'set-url':
if (!params.name) {
errors.push('Remote name is required for set-url operation');
suggestions.push('Provide the remote name to update');
}
if (!params.url) {
errors.push('Remote URL is required for set-url operation');
suggestions.push('Provide the new remote URL');
}
break;
case 'prune':
if (!params.name) {
errors.push('Remote name is required for prune operation');
suggestions.push('Provide the remote name to prune (e.g., "origin")');
}
break;
case 'show':
// Show operation can work without parameters (shows all remotes)
break;
}
return {
isValid: errors.length === 0,
errors,
suggestions
};
}
/**
* Handle git remote add operation
*/
private async handleAdd(params: GitRemoteParams, startTime: number): Promise<ToolResult> {
try {
const args = [params.name!, params.url!];
// Add fetch option if specified
if (params.fetch) {
args.unshift('-f');
}
const result = await this.gitExecutor.executeGitCommand('remote', ['add', ...args], params.projectPath);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'remote add', params.projectPath);
}
// If fetch was requested, the output will include fetch information
const message = params.fetch
? `Remote '${params.name}' added and fetched successfully`
: `Remote '${params.name}' added successfully`;
return {
success: true,
data: {
message,
remoteName: params.name,
remoteUrl: params.url,
fetched: params.fetch || false,
output: result.stdout
},
metadata: {
operation: 'remote add',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'REMOTE_ADD_ERROR',
`Failed to add remote: ${errorMessage}`,
'add',
{ error: errorMessage, remoteName: params.name, remoteUrl: params.url }
);
}
}
/**
* Handle git remote remove operation
*/
private async handleRemove(params: GitRemoteParams, startTime: number): Promise<ToolResult> {
try {
// Check if remote exists first
const showResult = await this.gitExecutor.executeGitCommand('remote', ['show'], params.projectPath);
if (showResult.success) {
const remotes = showResult.stdout.split('\n').filter(line => line.trim());
if (!remotes.includes(params.name!)) {
return OperationErrorHandler.createToolError(
'REMOTE_NOT_FOUND',
`Remote '${params.name}' does not exist`,
'remove',
{ remoteName: params.name, availableRemotes: remotes },
[`Available remotes: ${remotes.join(', ')}`]
);
}
}
const result = await this.gitExecutor.executeGitCommand('remote', ['remove', params.name!], params.projectPath);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'remote remove', params.projectPath);
}
return {
success: true,
data: {
message: `Remote '${params.name}' removed successfully`,
remoteName: params.name,
output: result.stdout
},
metadata: {
operation: 'remote remove',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'REMOTE_REMOVE_ERROR',
`Failed to remove remote: ${errorMessage}`,
'remove',
{ error: errorMessage, remoteName: params.name }
);
}
}
/**
* Handle git remote rename operation
*/
private async handleRename(params: GitRemoteParams, startTime: number): Promise<ToolResult> {
try {
// Check if current remote exists
const showResult = await this.gitExecutor.executeGitCommand('remote', ['show'], params.projectPath);
if (showResult.success) {
const remotes = showResult.stdout.split('\n').filter(line => line.trim());
if (!remotes.includes(params.name!)) {
return OperationErrorHandler.createToolError(
'REMOTE_NOT_FOUND',
`Remote '${params.name}' does not exist`,
'rename',
{ remoteName: params.name, availableRemotes: remotes },
[`Available remotes: ${remotes.join(', ')}`]
);
}
if (remotes.includes(params.newName!)) {
return OperationErrorHandler.createToolError(
'REMOTE_ALREADY_EXISTS',
`Remote '${params.newName}' already exists`,
'rename',
{ newRemoteName: params.newName, availableRemotes: remotes },
['Choose a different name for the remote']
);
}
}
const result = await this.gitExecutor.executeGitCommand('remote', ['rename', params.name!, params.newName!], params.projectPath);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'remote rename', params.projectPath);
}
return {
success: true,
data: {
message: `Remote '${params.name}' renamed to '${params.newName}' successfully`,
oldName: params.name,
newName: params.newName,
output: result.stdout
},
metadata: {
operation: 'remote rename',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'REMOTE_RENAME_ERROR',
`Failed to rename remote: ${errorMessage}`,
'rename',
{ error: errorMessage, oldName: params.name, newName: params.newName }
);
}
}
/**
* Handle git remote show operation
*/
private async handleShow(params: GitRemoteParams, startTime: number): Promise<ToolResult> {
try {
const args: string[] = [];
if (params.verbose) {
args.push('-v');
}
if (params.name && !params.all) {
// Show specific remote details
args.push(params.name);
}
const result = await this.gitExecutor.executeGitCommand('remote', ['show', ...args], params.projectPath);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'remote show', params.projectPath);
}
// Parse the output
const output = result.stdout.trim();
let parsedData: any = { raw: output };
if (params.name && !params.all) {
// Detailed remote information
parsedData.remoteName = params.name;
parsedData.details = this.parseRemoteDetails(output);
} else {
// List of remotes
const lines = output.split('\n').filter(line => line.trim());
if (params.verbose) {
parsedData.remotes = this.parseVerboseRemotes(lines);
} else {
parsedData.remotes = lines;
}
}
return {
success: true,
data: {
message: params.name ? `Remote '${params.name}' details` : 'Remote repositories',
...parsedData
},
metadata: {
operation: 'remote show',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'REMOTE_SHOW_ERROR',
`Failed to show remote information: ${errorMessage}`,
'show',
{ error: errorMessage, remoteName: params.name }
);
}
}
/**
* Handle git remote set-url operation
*/
private async handleSetUrl(params: GitRemoteParams, startTime: number): Promise<ToolResult> {
try {
// Check if remote exists
const showResult = await this.gitExecutor.executeGitCommand('remote', ['show'], params.projectPath);
if (showResult.success) {
const remotes = showResult.stdout.split('\n').filter(line => line.trim());
if (!remotes.includes(params.name!)) {
return OperationErrorHandler.createToolError(
'REMOTE_NOT_FOUND',
`Remote '${params.name}' does not exist`,
'set-url',
{ remoteName: params.name, availableRemotes: remotes },
[`Available remotes: ${remotes.join(', ')}`]
);
}
}
const args = [params.name!, params.url!];
// Add push option if specified
if (params.push) {
args.unshift('--push');
}
const result = await this.gitExecutor.executeGitCommand('remote', ['set-url', ...args], params.projectPath);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'remote set-url', params.projectPath);
}
const urlType = params.push ? 'push URL' : 'URL';
return {
success: true,
data: {
message: `Remote '${params.name}' ${urlType} updated successfully`,
remoteName: params.name,
newUrl: params.url,
urlType: params.push ? 'push' : 'fetch',
output: result.stdout
},
metadata: {
operation: 'remote set-url',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'REMOTE_SET_URL_ERROR',
`Failed to set remote URL: ${errorMessage}`,
'set-url',
{ error: errorMessage, remoteName: params.name, url: params.url }
);
}
}
/**
* Handle git remote prune operation
*/
private async handlePrune(params: GitRemoteParams, startTime: number): Promise<ToolResult> {
try {
// Check if remote exists
const showResult = await this.gitExecutor.executeGitCommand('remote', ['show'], params.projectPath);
if (showResult.success) {
const remotes = showResult.stdout.split('\n').filter(line => line.trim());
if (!remotes.includes(params.name!)) {
return OperationErrorHandler.createToolError(
'REMOTE_NOT_FOUND',
`Remote '${params.name}' does not exist`,
'prune',
{ remoteName: params.name, availableRemotes: remotes },
[`Available remotes: ${remotes.join(', ')}`]
);
}
}
const args = [params.name!];
// Add dry-run option if specified
if (params.dryRun) {
args.unshift('--dry-run');
}
const result = await this.gitExecutor.executeGitCommand('remote', ['prune', ...args], params.projectPath);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'remote prune', params.projectPath);
}
// Parse pruned branches from output
const prunedBranches = this.parsePrunedBranches(result.stdout);
const message = params.dryRun
? `Dry run: Would prune ${prunedBranches.length} stale branches from '${params.name}'`
: `Pruned ${prunedBranches.length} stale branches from '${params.name}'`;
return {
success: true,
data: {
message,
remoteName: params.name,
dryRun: params.dryRun || false,
prunedBranches,
output: result.stdout
},
metadata: {
operation: 'remote prune',
timestamp: new Date().toISOString(),
executionTime: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'REMOTE_PRUNE_ERROR',
`Failed to prune remote: ${errorMessage}`,
'prune',
{ error: errorMessage, remoteName: params.name }
);
}
}
/**
* Handle list remotes
*/
private async handleList(params: GitRemoteParams, startTime: number): Promise<ToolResult> {
try {
const result = await this.gitExecutor.executeGitCommand('remote', [], params.projectPath);
if (!result.success) {
return OperationErrorHandler.handleGitError(result.stderr, 'remote list', params.projectPath);
}
// Parse remotes from output
const remotes = result.stdout
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0);
return {
success: true,
data: {
operation: 'list',
message: `Found ${remotes.length} remote(s)`,
details: {
remotes,
count: remotes.length
},
duration: Date.now() - startTime
}
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return OperationErrorHandler.createToolError(
'REMOTE_LIST_ERROR',
`Failed to list remotes: ${errorMessage}`,
'list',
{ error: errorMessage }
);
}
}
/**
* Parse remote details from git remote show output
*/
private parseRemoteDetails(output: string): any {
const lines = output.split('\n');
const details: any = {};
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.includes('Fetch URL:')) {
details.fetchUrl = trimmed.split('Fetch URL:')[1]?.trim();
} else if (trimmed.includes('Push URL:')) {
details.pushUrl = trimmed.split('Push URL:')[1]?.trim();
} else if (trimmed.includes('HEAD branch:')) {
details.headBranch = trimmed.split('HEAD branch:')[1]?.trim();
}
}
return details;
}
/**
* Parse verbose remotes output
*/
private parseVerboseRemotes(lines: string[]): Array<{ name: string; fetchUrl?: string; pushUrl?: string }> {
const remotes: Array<{ name: string; fetchUrl?: string; pushUrl?: string }> = [];
const remoteMap = new Map<string, { name: string; fetchUrl?: string; pushUrl?: string }>();
for (const line of lines) {
const parts = line.split('\t');
if (parts.length >= 2) {
const name = parts[0].trim();
const urlAndType = parts[1].trim();
const urlMatch = urlAndType.match(/^(.+?)\s+\((.+)\)$/);
if (urlMatch) {
const url = urlMatch[1];
const type = urlMatch[2];
if (!remoteMap.has(name)) {
remoteMap.set(name, { name });
}
const remote = remoteMap.get(name)!;
if (type === 'fetch') {
remote.fetchUrl = url;
} else if (type === 'push') {
remote.pushUrl = url;
}
}
}
}
return Array.from(remoteMap.values());
}
/**
* Parse pruned branches from git remote prune output
*/
private parsePrunedBranches(output: string): string[] {
const lines = output.split('\n');
const prunedBranches: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('* [would prune]') || trimmed.startsWith('* [pruned]')) {
const branchMatch = trimmed.match(/\* \[(would prune|pruned)\] (.+)/);
if (branchMatch) {
prunedBranches.push(branchMatch[2]);
}
}
}
return prunedBranches;
}
/**
* Get tool schema for MCP registration
*/
static getToolSchema() {
return {
name: 'git-remote',
description: 'Git remote management tool for managing remote repositories. Supports add, remove, rename, show, set-url, and prune operations.',
inputSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['add', 'remove', 'rename', 'show', 'set-url', 'prune', 'list'],
description: 'The remote operation to perform'
},
projectPath: {
type: 'string',
description: 'Absolute path to the project directory'
},
name: {
type: 'string',
description: 'Remote name (required for most operations)'
},
url: {
type: 'string',
description: 'Remote URL (required for add and set-url operations)'
},
newName: {
type: 'string',
description: 'New remote name (required for rename operation)'
},
fetch: {
type: 'boolean',
description: 'Fetch after adding remote (for add action)'
},
push: {
type: 'boolean',
description: 'Set push URL instead of fetch URL (for set-url action)'
},
all: {
type: 'boolean',
description: 'Show all remotes (for show action)'
},
verbose: {
type: 'boolean',
description: 'Show verbose output with URLs (for show action)'
},
dryRun: {
type: 'boolean',
description: 'Show what would be pruned without actually pruning (for prune action)'
}
},
required: ['action', 'projectPath']
}
};
}
}