Skip to main content
Glama
git-archive.ts20.5 kB
/** * Git Archive Tool * * Archive operations for Git repositories. * Supports archive creation, extraction, listing, and verification. * * Operations: create, extract, list, verify */ import { GitCommandExecutor, GitCommandResult } from '../utils/git-command-executor.js'; import { TerminalController } from '../utils/terminal-controller.js'; import { ParameterValidator, ToolParams } from '../utils/parameter-validator.js'; import { OperationErrorHandler, ToolResult } from '../utils/operation-error-handler.js'; import path from 'path'; import fs from 'fs/promises'; import { existsSync } from 'fs'; export interface GitArchiveParams extends ToolParams { action: 'create' | 'extract' | 'list' | 'verify'; // Archive parameters archivePath?: string; // Path for archive file format?: 'tar' | 'zip'; // Archive format ref?: string; // Git reference (branch, tag, commit) prefix?: string; // Prefix for archive contents // Create parameters outputPath?: string; // Output path for archive includeSubmodules?: boolean; // Include submodules // Extract parameters targetPath?: string; // Target path for extraction overwrite?: boolean; // Overwrite existing files // List parameters showDetails?: boolean; // Show detailed file information // Verify parameters checkIntegrity?: boolean; // Check archive integrity } export class GitArchiveTool { private gitExecutor: GitCommandExecutor; private terminal: TerminalController; constructor() { this.gitExecutor = new GitCommandExecutor(); this.terminal = new TerminalController(); } async execute(params: GitArchiveParams): Promise<ToolResult> { const startTime = Date.now(); try { // Validate required parameters const validation = ParameterValidator.validateToolParams('git-archive', params); if (!validation.isValid) { return OperationErrorHandler.createToolError( 'VALIDATION_ERROR', `Validation failed: ${validation.errors.join(', ')}`, 'archive', { errors: validation.errors }, validation.suggestions ); } // Validate project path exists if (!existsSync(params.projectPath)) { return OperationErrorHandler.createToolError( 'PROJECT_NOT_FOUND', `Project path does not exist: ${params.projectPath}`, 'archive', { projectPath: params.projectPath }, ['Ensure the project path exists and is accessible'] ); } switch (params.action) { case 'create': return await this.handleCreate(params, startTime); case 'extract': return await this.handleExtract(params, startTime); case 'list': return await this.handleList(params, startTime); case 'verify': return await this.handleVerify(params, startTime); default: return OperationErrorHandler.createToolError( 'INVALID_ACTION', `Invalid action: ${params.action}`, params.action, {}, ['Use one of: create, extract, list, verify'] ); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'ARCHIVE_TOOL_ERROR', `Git archive tool error: ${errorMessage}`, 'archive', { error: errorMessage } ); } } /** * Handle archive creation operation */ private async handleCreate(params: GitArchiveParams, startTime: number): Promise<ToolResult> { try { // Check if it's a Git repository const isGitRepo = await this.gitExecutor.isGitRepository(params.projectPath); if (!isGitRepo) { return OperationErrorHandler.createToolError( 'NOT_GIT_REPOSITORY', 'Project is not a Git repository', 'create', { projectPath: params.projectPath }, ['Initialize Git repository with: git init'] ); } // Generate archive path if not provided if (!params.archivePath && !params.outputPath) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const repoName = path.basename(params.projectPath); const format = params.format || 'tar'; const extension = format === 'zip' ? 'zip' : 'tar.gz'; params.archivePath = path.join(params.projectPath, '..', `${repoName}-archive-${timestamp}.${extension}`); } const outputPath = params.outputPath || params.archivePath!; const format = params.format || 'tar'; const ref = params.ref || 'HEAD'; // Ensure output directory exists const outputDir = path.dirname(outputPath); await fs.mkdir(outputDir, { recursive: true }); // Build git archive command const args = ['--format', format === 'zip' ? 'zip' : 'tar.gz']; if (params.prefix) { args.push('--prefix', params.prefix); } args.push('--output', outputPath, ref); // Execute git archive command const result = await this.gitExecutor.executeGitCommand('archive', args, params.projectPath); if (!result.success) { return OperationErrorHandler.handleGitError(result.stderr, 'create', params.projectPath); } // Get archive file stats const stats = await fs.stat(outputPath); // Create archive metadata const metadata = { path: outputPath, format, ref, prefix: params.prefix, size: stats.size, created: stats.birthtime.toISOString(), repository: path.basename(params.projectPath), includeSubmodules: params.includeSubmodules || false }; // Save metadata file const metadataPath = `${outputPath}.meta.json`; await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2)); return { success: true, data: { message: 'Archive created successfully', archive: metadata, output: result.stdout }, metadata: { operation: 'create', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'ARCHIVE_CREATE_ERROR', `Failed to create archive: ${errorMessage}`, 'create', { error: errorMessage, archivePath: params.archivePath } ); } } /** * Handle archive extraction operation */ private async handleExtract(params: GitArchiveParams, startTime: number): Promise<ToolResult> { try { if (!params.archivePath) { return OperationErrorHandler.createToolError( 'MISSING_ARCHIVE_PATH', 'Archive path is required for extract operation', 'extract', {}, ['Provide archivePath parameter with the path to the archive file'] ); } // Check if archive file exists if (!existsSync(params.archivePath)) { return OperationErrorHandler.createToolError( 'ARCHIVE_NOT_FOUND', `Archive file not found: ${params.archivePath}`, 'extract', { archivePath: params.archivePath }, ['Ensure the archive file exists and path is correct'] ); } const targetPath = params.targetPath || params.projectPath; // Check if target directory exists and handle overwrite if (existsSync(targetPath) && !params.overwrite) { const files = await fs.readdir(targetPath); if (files.length > 0) { return OperationErrorHandler.createToolError( 'TARGET_NOT_EMPTY', `Target directory is not empty: ${targetPath}`, 'extract', { targetPath }, ['Use overwrite: true to overwrite existing files', 'Choose an empty target directory'] ); } } // Ensure target directory exists await fs.mkdir(targetPath, { recursive: true }); // Determine archive format from file extension const isZip = params.archivePath.endsWith('.zip'); const format = isZip ? 'zip' : 'tar'; // Extract archive let result: GitCommandResult; if (format === 'zip') { // Use unzip command for zip files const args = params.overwrite ? ['-o', params.archivePath, '-d', targetPath] : [params.archivePath, '-d', targetPath]; result = await this.terminal.executeCommand('unzip', args); } else { // Use tar command for tar.gz files const args = ['-xzf', params.archivePath, '-C', targetPath]; if (params.overwrite) { args.push('--overwrite'); } result = await this.terminal.executeCommand('tar', args); } if (!result.success) { return OperationErrorHandler.createToolError( 'EXTRACT_ERROR', `Failed to extract archive: ${result.stderr}`, 'extract', { archivePath: params.archivePath, targetPath } ); } // Load archive metadata if available const metadataPath = `${params.archivePath}.meta.json`; let metadata = null; if (existsSync(metadataPath)) { try { const metadataContent = await fs.readFile(metadataPath, 'utf-8'); metadata = JSON.parse(metadataContent); } catch (error) { // Metadata file exists but couldn't be parsed, continue without it } } return { success: true, data: { message: 'Archive extracted successfully', archivePath: params.archivePath, targetPath, format, metadata, output: result.stdout }, metadata: { operation: 'extract', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'EXTRACT_ERROR', `Failed to extract archive: ${errorMessage}`, 'extract', { error: errorMessage, archivePath: params.archivePath } ); } } /** * Handle archive listing operation */ private async handleList(params: GitArchiveParams, startTime: number): Promise<ToolResult> { try { if (!params.archivePath) { return OperationErrorHandler.createToolError( 'MISSING_ARCHIVE_PATH', 'Archive path is required for list operation', 'list', {}, ['Provide archivePath parameter with the path to the archive file'] ); } if (!existsSync(params.archivePath)) { return OperationErrorHandler.createToolError( 'ARCHIVE_NOT_FOUND', `Archive file not found: ${params.archivePath}`, 'list', { archivePath: params.archivePath }, ['Ensure the archive file exists and path is correct'] ); } // Determine archive format from file extension const isZip = params.archivePath.endsWith('.zip'); const format = isZip ? 'zip' : 'tar'; // List archive contents let result: GitCommandResult; if (format === 'zip') { // Use unzip to list zip contents const args = params.showDetails ? ['-l', params.archivePath] : ['-Z1', params.archivePath]; result = await this.terminal.executeCommand('unzip', args); } else { // Use tar to list tar.gz contents const args = params.showDetails ? ['-tvzf', params.archivePath] : ['-tzf', params.archivePath]; result = await this.terminal.executeCommand('tar', args); } if (!result.success) { return OperationErrorHandler.createToolError( 'LIST_ERROR', `Failed to list archive contents: ${result.stderr}`, 'list', { archivePath: params.archivePath } ); } // Parse the output to extract file information const files = this.parseArchiveList(result.stdout, format, params.showDetails || false); // Load archive metadata if available const metadataPath = `${params.archivePath}.meta.json`; let metadata = null; if (existsSync(metadataPath)) { try { const metadataContent = await fs.readFile(metadataPath, 'utf-8'); metadata = JSON.parse(metadataContent); } catch (error) { // Continue without metadata if parsing fails } } return { success: true, data: { message: `Archive contains ${files.length} file(s)`, archivePath: params.archivePath, format, files, metadata, rawOutput: result.stdout }, metadata: { operation: 'list', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'LIST_ERROR', `Failed to list archive: ${errorMessage}`, 'list', { error: errorMessage, archivePath: params.archivePath } ); } } /** * Handle archive verification operation */ private async handleVerify(params: GitArchiveParams, startTime: number): Promise<ToolResult> { try { if (!params.archivePath) { return OperationErrorHandler.createToolError( 'MISSING_ARCHIVE_PATH', 'Archive path is required for verify operation', 'verify', {}, ['Provide archivePath parameter with the path to the archive file'] ); } if (!existsSync(params.archivePath)) { return OperationErrorHandler.createToolError( 'ARCHIVE_NOT_FOUND', `Archive file not found: ${params.archivePath}`, 'verify', { archivePath: params.archivePath }, ['Ensure the archive file exists and path is correct'] ); } const stats = await fs.stat(params.archivePath); const isZip = params.archivePath.endsWith('.zip'); const format = isZip ? 'zip' : 'tar'; // Basic file verification const verification = { exists: true, readable: true, size: stats.size, format, created: stats.birthtime.toISOString(), modified: stats.mtime.toISOString(), integrity: 'unknown' as 'valid' | 'invalid' | 'unknown' }; // Test archive integrity if requested if (params.checkIntegrity) { let result: GitCommandResult; if (format === 'zip') { // Test zip file integrity result = await this.terminal.executeCommand('unzip', ['-t', params.archivePath]); } else { // Test tar.gz file integrity result = await this.terminal.executeCommand('tar', ['-tzf', params.archivePath]); } verification.integrity = result.success ? 'valid' : 'invalid'; } // Load and verify metadata if available const metadataPath = `${params.archivePath}.meta.json`; let metadata = null; let metadataValid = false; if (existsSync(metadataPath)) { try { const metadataContent = await fs.readFile(metadataPath, 'utf-8'); metadata = JSON.parse(metadataContent); metadataValid = true; // Verify metadata consistency if (metadata.size !== stats.size) { metadataValid = false; } } catch (error) { metadataValid = false; } } const isValid = verification.integrity !== 'invalid' && (metadata ? metadataValid : true); return { success: true, data: { message: isValid ? 'Archive verification passed' : 'Archive verification failed', valid: isValid, verification, metadata: metadata ? { ...metadata, valid: metadataValid } : null }, metadata: { operation: 'verify', timestamp: new Date().toISOString(), executionTime: Date.now() - startTime } }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return OperationErrorHandler.createToolError( 'VERIFY_ERROR', `Failed to verify archive: ${errorMessage}`, 'verify', { error: errorMessage, archivePath: params.archivePath } ); } } /** * Parse archive listing output */ private parseArchiveList(output: string, format: string, showDetails: boolean): any[] { const lines = output.trim().split('\n').filter(line => line.trim()); const files = []; for (const line of lines) { if (format === 'zip') { if (showDetails) { // Parse detailed zip listing (unzip -l format) const match = line.match(/^\s*(\d+)\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\s+(.+)$/); if (match) { files.push({ name: match[4], size: parseInt(match[1]), date: match[2], time: match[3] }); } } else { // Simple file listing if (line.trim() && !line.includes('Archive:') && !line.includes('Length')) { files.push({ name: line.trim() }); } } } else { // tar format if (showDetails) { // Parse detailed tar listing (tar -tvzf format) const match = line.match(/^([drwx-]+)\s+\S+\s+\S+\s+(\d+)\s+(.+?)\s+(.+)$/); if (match) { files.push({ name: match[4], permissions: match[1], size: parseInt(match[2]), date: match[3] }); } } else { // Simple file listing if (line.trim()) { files.push({ name: line.trim() }); } } } } return files; } /** * Get tool schema for MCP registration */ static getToolSchema() { return { name: 'git-archive', description: 'Archive operations for Git repositories. Supports archive creation, extraction, listing, and verification.', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['create', 'extract', 'list', 'verify'], description: 'The archive operation to perform' }, projectPath: { type: 'string', description: 'Path to the Git repository (required)' }, archivePath: { type: 'string', description: 'Path to the archive file' }, format: { type: 'string', enum: ['tar', 'zip'], description: 'Archive format (default: tar)' }, ref: { type: 'string', description: 'Git reference to archive (branch, tag, commit, default: HEAD)' }, prefix: { type: 'string', description: 'Prefix for archive contents' }, outputPath: { type: 'string', description: 'Output path for archive creation' }, includeSubmodules: { type: 'boolean', description: 'Include submodules in archive (default: false)' }, targetPath: { type: 'string', description: 'Target path for extraction' }, overwrite: { type: 'boolean', description: 'Overwrite existing files during extraction (default: false)' }, showDetails: { type: 'boolean', description: 'Show detailed file information when listing (default: false)' }, checkIntegrity: { type: 'boolean', description: 'Check archive integrity during verification (default: false)' } }, required: ['action', 'projectPath'] } }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Andre-Buzeli/git-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server