claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
275 lines (274 loc) • 9.52 kB
JavaScript
/**
* Skill Git Integration
*
* Manages version tracking, content hashing, and git metadata for skills
* Provides atomic updates with rollback capabilities
*
* @module skill-git-integration
* @version 1.0.0
*/ import { exec } from 'child_process';
import { promisify } from 'util';
import { createHash } from 'crypto';
import { readFile } from 'fs/promises';
import { StandardError } from './errors.js';
const execAsync = promisify(exec);
/**
* Git integration error
*/ export class GitIntegrationError extends StandardError {
context;
constructor(message, context){
super('GIT_INTEGRATION_ERROR', message, context), this.context = context;
this.name = 'GitIntegrationError';
}
}
/**
* Calculate SHA256 hash of file content
*
* @param content - File content string
* @returns SHA256 hash in hexadecimal
*/ export function calculateContentHash(content) {
return createHash('sha256').update(content).digest('hex');
}
/**
* Calculate SHA256 hash of file
*
* @param filePath - Path to file
* @returns SHA256 hash in hexadecimal
*/ export async function calculateFileHash(filePath) {
try {
const content = await readFile(filePath, 'utf-8');
return calculateContentHash(content);
} catch (error) {
throw new GitIntegrationError(`Failed to calculate file hash for ${filePath}`, {
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* Check if a path is in a git repository
*
* @param filePath - Path to check
* @returns True if in git repository
*/ export async function isGitRepository(filePath) {
try {
await execAsync('git rev-parse --is-inside-work-tree', {
cwd: filePath
});
return true;
} catch {
return false;
}
}
/**
* Get git commit metadata for a file
*
* @param filePath - Path to file
* @param commitRef - Git commit reference (default: HEAD)
* @returns Commit metadata
*/ export async function getCommitMetadata(filePath, commitRef = 'HEAD') {
try {
const { stdout } = await execAsync(`git log -1 --format="%H%n%an%n%ae%n%ai%n%s" ${commitRef} -- "${filePath}"`);
const [hash, author, email, date, message] = stdout.trim().split('\n');
if (!hash) {
throw new GitIntegrationError('No git history found for file', {
filePath
});
}
return {
hash,
author,
email,
date,
message: message || 'No commit message'
};
} catch (error) {
throw new GitIntegrationError(`Failed to get git metadata for ${filePath}`, {
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* Get version history for a skill file
*
* @param filePath - Path to SKILL.md file
* @param limit - Maximum number of entries (default: 10)
* @returns Array of version history entries
*/ export async function getVersionHistory(filePath, limit = 10) {
try {
// Get commit history
const { stdout } = await execAsync(`git log -${limit} --format="%H" -- "${filePath}"`);
const commits = stdout.trim().split('\n').filter(Boolean);
const history = [];
for (const commitHash of commits){
try {
// Get file content at this commit
const { stdout: content } = await execAsync(`git show ${commitHash}:"${filePath}"`);
// Get commit metadata
const metadata = await getCommitMetadata(filePath, commitHash);
// Calculate content hash
const contentHash = calculateContentHash(content);
// Extract version from content (if available in frontmatter)
const versionMatch = content.match(/version:\s*["']?([0-9.]+)["']?/);
const version = versionMatch ? versionMatch[1] : 'unknown';
history.push({
version,
commit: metadata,
contentHash,
timestamp: metadata.date,
filePath
});
} catch (error) {
continue;
}
}
return history;
} catch (error) {
throw new GitIntegrationError(`Failed to get version history for ${filePath}`, {
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* Stage and commit a file with atomic operation
*
* @param filePath - Path to file to commit
* @param message - Commit message
* @returns Git operation result
*/ export async function commitFile(filePath, message) {
try {
// Stage file
await execAsync(`git add "${filePath}"`);
// Commit
const { stdout } = await execAsync(`git commit -m "${message}"`);
// Get commit hash
const { stdout: hashOutput } = await execAsync('git rev-parse HEAD');
const commitHash = hashOutput.trim();
return {
success: true,
commitHash
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Rollback file to previous commit
*
* @param filePath - Path to file to rollback
* @param commitRef - Git commit reference to rollback to (default: HEAD~1)
* @returns Git operation result
*/ export async function rollbackFile(filePath, commitRef = 'HEAD~1') {
try {
// Checkout file from previous commit
await execAsync(`git checkout ${commitRef} -- "${filePath}"`);
// Commit the rollback
const message = `Rollback ${filePath} to ${commitRef}`;
return await commitFile(filePath, message);
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Check if file has uncommitted changes
*
* @param filePath - Path to file
* @returns True if file has uncommitted changes
*/ export async function hasUncommittedChanges(filePath) {
try {
const { stdout } = await execAsync(`git status --porcelain "${filePath}"`);
return stdout.trim().length > 0;
} catch (error) {
throw new GitIntegrationError(`Failed to check git status for ${filePath}`, {
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* Get current git branch
*
* @returns Current branch name
*/ export async function getCurrentBranch() {
try {
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD');
return stdout.trim();
} catch (error) {
throw new GitIntegrationError('Failed to get current branch', {
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* Get file diff between two commits
*
* @param filePath - Path to file
* @param commitRef1 - First commit reference
* @param commitRef2 - Second commit reference (default: HEAD)
* @returns Diff output
*/ export async function getFileDiff(filePath, commitRef1, commitRef2 = 'HEAD') {
try {
const { stdout } = await execAsync(`git diff ${commitRef1} ${commitRef2} -- "${filePath}"`);
return stdout;
} catch (error) {
throw new GitIntegrationError(`Failed to get diff for ${filePath}`, {
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* Verify content integrity using git hash
*
* @param filePath - Path to file
* @param expectedHash - Expected content hash
* @returns True if content matches expected hash
*/ export async function verifyContentIntegrity(filePath, expectedHash) {
try {
const actualHash = await calculateFileHash(filePath);
return actualHash === expectedHash;
} catch (error) {
throw new GitIntegrationError(`Failed to verify content integrity for ${filePath}`, {
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* Get file creation date from git history
*
* @param filePath - Path to file
* @returns ISO date string of first commit
*/ export async function getFileCreationDate(filePath) {
try {
const { stdout } = await execAsync(`git log --diff-filter=A --follow --format="%ai" -- "${filePath}" | tail -1`);
if (!stdout.trim()) {
throw new GitIntegrationError('No creation date found in git history', {
filePath
});
}
return new Date(stdout.trim()).toISOString();
} catch (error) {
throw new GitIntegrationError(`Failed to get creation date for ${filePath}`, {
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* Get file last modification date from git history
*
* @param filePath - Path to file
* @returns ISO date string of last commit
*/ export async function getFileModificationDate(filePath) {
try {
const metadata = await getCommitMetadata(filePath);
return new Date(metadata.date).toISOString();
} catch (error) {
throw new GitIntegrationError(`Failed to get modification date for ${filePath}`, {
error: error instanceof Error ? error.message : String(error)
});
}
}
//# sourceMappingURL=skill-git-integration.js.map