@sethdouglasford/claude-flow
Version:
Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology
364 lines • 14.2 kB
JavaScript
import * as fs from "fs/promises";
import { createReadStream } from "fs";
import * as path from "path";
import { createHash } from "crypto";
import { EventEmitter } from "events";
import { Logger } from "../core/logger.js";
const logger = Logger.getInstance();
export class PromptCopier extends EventEmitter {
options;
fileQueue = [];
copiedFiles = new Set();
errors = [];
backupMap = new Map();
rollbackStack = [];
constructor(options) {
super();
this.options = {
...options,
backup: options.backup ?? true,
overwrite: options.overwrite ?? false,
verify: options.verify ?? true,
preservePermissions: options.preservePermissions ?? true,
excludePatterns: options.excludePatterns ?? [],
includePatterns: options.includePatterns ?? ["*.md", "*.txt", "*.prompt", "*.prompts", "**/*.md", "**/*.txt", "**/*.prompt", "**/*.prompts"],
parallel: options.parallel ?? true,
maxWorkers: options.maxWorkers ?? 4,
dryRun: options.dryRun ?? false,
conflictResolution: options.conflictResolution ?? "backup",
progressCallback: options.progressCallback ?? (() => { }),
};
}
async copy() {
const startTime = Date.now();
try {
// Phase 1: Discovery
logger.info("Starting prompt discovery phase...");
await this.discoverFiles();
if (this.fileQueue.length === 0) {
return {
success: true,
totalFiles: 0,
copiedFiles: 0,
failedFiles: 0,
skippedFiles: 0,
errors: [],
duration: Date.now() - startTime,
};
}
// Phase 2: Pre-flight checks
if (!this.options.dryRun) {
await this.ensureDestinationDirectories();
}
// Phase 3: Copy files
logger.info(`Copying ${this.fileQueue.length} files...`);
if (this.options.parallel) {
await this.copyFilesParallel();
}
else {
await this.copyFilesSequential();
}
// Phase 4: Verification
if (this.options.verify && !this.options.dryRun) {
await this.verifyFiles();
}
const duration = Date.now() - startTime;
const result = {
success: this.errors.length === 0,
totalFiles: this.fileQueue.length,
copiedFiles: this.copiedFiles.size,
failedFiles: this.errors.length,
skippedFiles: this.fileQueue.length - this.copiedFiles.size - this.errors.length,
errors: this.errors,
duration,
};
if (this.backupMap.size > 0) {
result.backupLocation = await this.createBackupManifest();
}
logger.info(`Copy completed in ${duration}ms`, result);
return result;
}
catch (error) {
logger.error("Copy operation failed", error);
if (!this.options.dryRun) {
await this.rollback();
}
throw error;
}
}
async discoverFiles() {
const sourceStats = await fs.stat(this.options.source);
if (!sourceStats.isDirectory()) {
throw new Error(`Source path ${this.options.source} is not a directory`);
}
await this.scanDirectory(this.options.source, "");
// Sort by size for better parallel distribution
this.fileQueue.sort((a, b) => b.size - a.size);
}
async scanDirectory(dirPath, relativePath) {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
const relPath = path.join(relativePath, entry.name);
if (entry.isDirectory()) {
await this.scanDirectory(fullPath, relPath);
}
else if (entry.isFile() && this.shouldIncludeFile(relPath)) {
const stats = await fs.stat(fullPath);
this.fileQueue.push({
path: fullPath,
relativePath: relPath,
size: stats.size,
permissions: stats.mode,
});
}
}
}
shouldIncludeFile(filePath) {
// Check exclude patterns first
for (const pattern of this.options.excludePatterns) {
if (this.matchPattern(filePath, pattern)) {
return false;
}
}
// Check include patterns
if (this.options.includePatterns.length === 0) {
return true;
}
for (const pattern of this.options.includePatterns) {
if (this.matchPattern(filePath, pattern)) {
return true;
}
}
return false;
}
matchPattern(filePath, pattern) {
// Enhanced glob pattern matching that handles ** correctly
const regex = pattern
.replace(/\./g, "\\.")
.replace(/\*\*/g, "DOUBLE_STAR") // Temporarily replace ** to avoid conflict
.replace(/\*/g, "[^/]*") // * matches anything except path separator
.replace(/DOUBLE_STAR/g, ".*") // ** matches anything including path separators
.replace(/\?/g, ".");
return new RegExp(`^${regex}$`).test(filePath);
}
async ensureDestinationDirectories() {
const directories = new Set();
for (const file of this.fileQueue) {
const destDir = path.dirname(path.join(this.options.destination, file.relativePath));
directories.add(destDir);
}
// Create directories in order (parent first)
const sortedDirs = Array.from(directories).sort((a, b) => a.length - b.length);
for (const dir of sortedDirs) {
await fs.mkdir(dir, { recursive: true });
}
}
async copyFilesSequential() {
let completed = 0;
for (const file of this.fileQueue) {
try {
await this.copyFile(file);
completed++;
this.reportProgress(completed);
}
catch (error) {
this.errors.push({
file: file.path,
error: error instanceof Error ? error.message : String(error),
phase: "write",
});
}
}
}
async copyFilesParallel() {
const workerCount = Math.min(this.options.maxWorkers, this.fileQueue.length);
const chunkSize = Math.ceil(this.fileQueue.length / workerCount);
const workers = [];
for (let i = 0; i < workerCount; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, this.fileQueue.length);
const chunk = this.fileQueue.slice(start, end);
if (chunk.length > 0) {
workers.push(this.processChunk(chunk, i));
}
}
await Promise.all(workers);
}
async processChunk(files, workerId) {
for (const file of files) {
try {
await this.copyFile(file);
this.copiedFiles.add(file.path);
this.reportProgress(this.copiedFiles.size);
}
catch (error) {
this.errors.push({
file: file.path,
error: error instanceof Error ? error.message : String(error),
phase: "write",
});
}
}
}
async copyFile(file) {
const destPath = path.join(this.options.destination, file.relativePath);
if (this.options.dryRun) {
logger.info(`[DRY RUN] Would copy ${file.path} to ${destPath}`);
return;
}
// Check for conflicts
const destExists = await this.fileExists(destPath);
if (destExists) {
switch (this.options.conflictResolution) {
case "skip":
logger.info(`Skipping existing file: ${destPath}`);
return;
case "backup":
await this.backupFile(destPath);
break;
case "merge":
await this.mergeFiles(file.path, destPath);
return;
case "overwrite":
// Continue with copy
break;
}
}
// Calculate source hash if verification is enabled
if (this.options.verify) {
file.hash = await this.calculateFileHash(file.path);
}
// Copy the file
await fs.copyFile(file.path, destPath);
// Preserve permissions if requested
if (this.options.preservePermissions && file.permissions) {
await fs.chmod(destPath, file.permissions);
}
// Add to rollback stack
this.rollbackStack.push(async () => {
if (destExists && this.backupMap.has(destPath)) {
// Restore from backup
const backupPath = this.backupMap.get(destPath);
await fs.copyFile(backupPath, destPath);
}
else {
// Remove the copied file
await fs.unlink(destPath);
}
});
this.copiedFiles.add(file.path);
}
async backupFile(filePath) {
const backupDir = path.join(this.options.destination, ".prompt-backups");
await fs.mkdir(backupDir, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupName = `${path.basename(filePath)}.${timestamp}.bak`;
const backupPath = path.join(backupDir, backupName);
await fs.copyFile(filePath, backupPath);
this.backupMap.set(filePath, backupPath);
}
async mergeFiles(sourcePath, destPath) {
// Simple merge strategy: append source to destination with separator
const sourceContent = await fs.readFile(sourcePath, "utf-8");
const destContent = await fs.readFile(destPath, "utf-8");
const separator = "\n\n--- MERGED CONTENT ---\n\n";
const mergedContent = destContent + separator + sourceContent;
await this.backupFile(destPath);
await fs.writeFile(destPath, mergedContent, "utf-8");
}
async verifyFiles() {
logger.info("Verifying copied files...");
for (const file of this.fileQueue) {
if (!this.copiedFiles.has(file.path))
continue;
try {
const destPath = path.join(this.options.destination, file.relativePath);
// Verify file exists
if (!await this.fileExists(destPath)) {
throw new Error("Destination file not found");
}
// Verify size
const destStats = await fs.stat(destPath);
const sourceStats = await fs.stat(file.path);
if (destStats.size !== sourceStats.size) {
throw new Error(`Size mismatch: ${destStats.size} != ${sourceStats.size}`);
}
// Verify hash if available
if (file.hash) {
const destHash = await this.calculateFileHash(destPath);
if (destHash !== file.hash) {
throw new Error(`Hash mismatch: ${destHash} != ${file.hash}`);
}
}
}
catch (error) {
this.errors.push({
file: file.path,
error: error instanceof Error ? error.message : String(error),
phase: "verify",
});
}
}
}
async calculateFileHash(filePath) {
const hash = createHash("sha256");
const stream = createReadStream(filePath);
for await (const chunk of stream) {
hash.update(chunk);
}
return hash.digest("hex");
}
async fileExists(filePath) {
try {
await fs.access(filePath);
return true;
}
catch {
return false;
}
}
async createBackupManifest() {
const manifestPath = path.join(this.options.destination, ".prompt-backups", `manifest-${Date.now()}.json`);
await fs.writeFile(manifestPath, JSON.stringify(Object.fromEntries(this.backupMap), null, 2));
return manifestPath;
}
async rollback() {
logger.warn("Rolling back changes...");
for (let i = this.rollbackStack.length - 1; i >= 0; i--) {
await this.rollbackStack[i]();
}
this.rollbackStack = [];
logger.info("Rollback complete.");
}
reportProgress(completed) {
const progress = {
total: this.fileQueue.length,
completed,
failed: this.errors.length,
skipped: this.fileQueue.length - completed - this.errors.length,
percentage: this.fileQueue.length > 0 ? (completed / this.fileQueue.length) * 100 : 0,
};
this.emit("progress", progress);
this.options.progressCallback(progress);
}
// Utility method to restore from backup
async restoreFromBackup(manifestPath) {
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
for (const { original, backup } of manifest.backups) {
try {
await fs.copyFile(backup, original);
logger.info(`Restored ${original} from ${backup}`);
}
catch (error) {
logger.error(`Failed to restore ${original}:`, error);
}
}
}
}
// Export convenience function
export async function copyPrompts(options) {
const copier = new PromptCopier(options);
return copier.copy();
}
//# sourceMappingURL=prompt-copier.js.map