UNPKG

@nvhien20vn/git-mcp-server

Version:
119 lines (118 loc) 6.93 kB
import { exec } from 'child_process'; import fs from 'fs/promises'; import path from 'path'; import { promisify } from 'util'; import { z } from 'zod'; // Import utils from barrel (logger from ../utils/internal/logger.js) import { logger } from '../../../utils/index.js'; // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js) import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global // Import utils from barrel (sanitization from ../utils/security/sanitization.js) import { sanitization } from '../../../utils/index.js'; const execAsync = promisify(exec); // Define the input schema for the git_init tool using Zod export const GitInitInputSchema = z.object({ path: z.string().min(1).optional().default('.').describe("Path where the new Git repository should be initialized. Can be relative or absolute. If relative or '.', it resolves against the directory set via `git_set_working_dir` for the session. If absolute, it's used directly. If omitted, defaults to '.' (resolved against session git working directory)."), initialBranch: z.string().optional().describe("Optional name for the initial branch (e.g., 'main'). Uses Git's default if not specified."), bare: z.boolean().default(false).describe("Create a bare repository (no working directory)."), quiet: z.boolean().default(false).describe("Only print error and warning messages; all other output will be suppressed."), }); /** * Executes the 'git init' command to initialize a new Git repository. * * @param {GitInitInput} input - The validated input object. * @param {RequestContext} context - The request context for logging and error handling. * @returns {Promise<GitInitResult>} A promise that resolves with the structured init result. * @throws {McpError} Throws an McpError if path validation fails or the git command fails unexpectedly. */ export async function gitInitLogic(input, context) { const operation = 'gitInitLogic'; logger.debug(`Executing ${operation}`, { ...context, input }); let targetPath; try { // Sanitize the provided absolute path targetPath = sanitization.sanitizePath(input.path, { allowAbsolute: true }).sanitizedPath; logger.debug('Sanitized path', { ...context, operation, sanitizedPath: targetPath }); // Ensure the target directory exists before trying to init inside it // git init creates the directory if it doesn't exist, but we might want to ensure the parent exists const parentDir = path.dirname(targetPath); try { await fs.access(parentDir, fs.constants.W_OK); // Check write access in parent } catch (accessError) { logger.error(`Parent directory check failed for ${targetPath}`, { ...context, operation, error: accessError.message }); if (accessError.code === 'ENOENT') { throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Parent directory does not exist: ${parentDir}`, { context, operation }); } throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Cannot access parent directory: ${parentDir}. Error: ${accessError.message}`, { context, operation }); } } catch (error) { logger.error('Path validation or sanitization failed', { ...context, operation, error }); if (error instanceof McpError) throw error; throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid path: ${error instanceof Error ? error.message : String(error)}`, { context, operation, originalError: error }); } try { // Construct the git init command let command = `git init`; if (input.quiet) { command += ' --quiet'; } if (input.bare) { command += ' --bare'; } // Determine the initial branch name, defaulting to 'main' if not provided const branchNameToUse = input.initialBranch || 'main'; command += ` -b "${branchNameToUse.replace(/"/g, '\\"')}"`; // Add the target directory path at the end command += ` "${targetPath}"`; logger.debug(`Executing command: ${command}`, { ...context, operation }); const { stdout, stderr } = await execAsync(command); if (stderr && !input.quiet) { // Log stderr as warning but proceed, as init might still succeed (e.g., reinitializing) logger.warning(`Git init command produced stderr`, { ...context, operation, stderr }); } if (stdout && !input.quiet) { logger.info(`Git init command produced stdout`, { ...context, operation, stdout }); } // Verify .git directory exists (or equivalent for bare repo) const gitDirPath = input.bare ? targetPath : path.join(targetPath, '.git'); let gitDirExists = false; try { await fs.access(gitDirPath); gitDirExists = true; } catch (e) { logger.warning(`Could not verify existence of ${gitDirPath} after git init`, { ...context, operation }); } const successMessage = stdout.trim() || `Initialized empty Git repository in ${targetPath}`; logger.info(`${operation} executed successfully`, { ...context, operation, path: targetPath }); return { success: true, message: successMessage, path: targetPath, gitDirExists: gitDirExists }; } catch (error) { const errorMessage = error.stderr || error.message || ''; logger.error(`Failed to execute git init command`, { ...context, operation, path: targetPath, error: errorMessage, stderr: error.stderr, stdout: error.stdout }); // Handle specific error cases if (errorMessage.toLowerCase().includes('already exists') && errorMessage.toLowerCase().includes('git repository')) { // Reinitializing is often okay, treat as success but mention it. logger.info(`Repository already exists, reinitialized: ${targetPath}`, { ...context, operation }); return { success: true, // Treat reinitialization as success message: `Reinitialized existing Git repository in ${targetPath}`, path: targetPath, gitDirExists: true // Assume it exists if reinit message appears }; } if (errorMessage.toLowerCase().includes('permission denied')) { throw new McpError(BaseErrorCode.FORBIDDEN, `Permission denied to initialize repository at: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error }); } // Generic internal error for other failures throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to initialize repository at: ${targetPath}. Error: ${errorMessage}`, { context, operation, originalError: error }); } }