/**
* 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']
}
};
}
}