claude-flow-multilang
Version:
Revolutionary multilingual AI orchestration framework with cultural awareness and DDD architecture
334 lines (287 loc) • 8.73 kB
text/typescript
import * as fs from 'fs/promises';
import * as path from 'path';
import { logger } from '../core/logger.js';
export interface PromptConfig {
sourceDirectories: string[];
destinationDirectory: string;
defaultOptions: {
backup: boolean;
verify: boolean;
parallel: boolean;
maxWorkers: number;
conflictResolution: 'skip' | 'overwrite' | 'backup' | 'merge';
includePatterns: string[];
excludePatterns: string[];
};
profiles: Record<string, Partial<PromptConfig['defaultOptions']>>;
}
export const DEFAULT_CONFIG: PromptConfig = {
sourceDirectories: ['.roo', '.claude/commands', 'src/templates', 'templates'],
destinationDirectory: './project-prompts',
defaultOptions: {
backup: true,
verify: true,
parallel: true,
maxWorkers: 4,
conflictResolution: 'backup',
includePatterns: ['*.md', '*.txt', '*.prompt', '*.prompts', '*.json'],
excludePatterns: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
},
profiles: {
sparc: {
includePatterns: ['*.md', 'rules.md', 'sparc-*.md'],
excludePatterns: ['**/README.md', '**/CHANGELOG.md'],
},
templates: {
includePatterns: ['*.template', '*.tmpl', '*.hbs', '*.mustache'],
conflictResolution: 'merge',
},
safe: {
backup: true,
verify: true,
conflictResolution: 'skip',
parallel: false,
},
fast: {
backup: false,
verify: false,
parallel: true,
maxWorkers: 8,
conflictResolution: 'overwrite',
},
},
};
export class PromptConfigManager {
private configPath: string;
private config: PromptConfig;
constructor(configPath?: string) {
this.configPath = configPath || path.join(process.cwd(), '.prompt-config.json');
this.config = { ...DEFAULT_CONFIG };
}
async loadConfig(): Promise<PromptConfig> {
try {
const configData = await fs.readFile(this.configPath, 'utf-8');
const userConfig = JSON.parse(configData) as Partial<PromptConfig>;
// Merge with defaults
this.config = this.mergeConfig(DEFAULT_CONFIG, userConfig);
logger.info(`Loaded config from ${this.configPath}`);
} catch (error) {
logger.info('Using default configuration');
}
return this.config;
}
async saveConfig(config?: Partial<PromptConfig>): Promise<void> {
if (config) {
this.config = this.mergeConfig(this.config, config);
}
await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2));
logger.info(`Saved config to ${this.configPath}`);
}
getConfig(): PromptConfig {
return this.config;
}
getProfile(profileName: string): PromptConfig['defaultOptions'] {
const profile = this.config.profiles[profileName];
if (!profile) {
throw new Error(`Profile '${profileName}' not found`);
}
return { ...this.config.defaultOptions, ...profile };
}
listProfiles(): string[] {
return Object.keys(this.config.profiles);
}
private mergeConfig(base: PromptConfig, override: Partial<PromptConfig>): PromptConfig {
return {
...base,
...override,
defaultOptions: {
...base.defaultOptions,
...override.defaultOptions,
},
profiles: {
...base.profiles,
...override.profiles,
},
};
}
}
export class PromptPathResolver {
private basePath: string;
constructor(basePath: string = process.cwd()) {
this.basePath = basePath;
}
resolvePaths(
sourceDirectories: string[],
destinationDirectory: string,
): {
sources: string[];
destination: string;
} {
const sources = sourceDirectories
.map((dir) => path.resolve(this.basePath, dir))
.filter((dir) => this.directoryExists(dir));
const destination = path.resolve(this.basePath, destinationDirectory);
return { sources, destination };
}
private directoryExists(dirPath: string): boolean {
try {
const stats = require('fs').statSync(dirPath);
return stats.isDirectory();
} catch {
return false;
}
}
// Discover prompt directories automatically
async discoverPromptDirectories(): Promise<string[]> {
const candidates = [
'.roo',
'.claude',
'prompts',
'templates',
'src/prompts',
'src/templates',
'docs/prompts',
'scripts/prompts',
];
const discovered: string[] = [];
for (const candidate of candidates) {
const fullPath = path.resolve(this.basePath, candidate);
if (await this.containsPromptFiles(fullPath)) {
discovered.push(fullPath);
}
}
return discovered;
}
private async containsPromptFiles(dirPath: string): Promise<boolean> {
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile()) {
const fileName = entry.name.toLowerCase();
if (
fileName.endsWith('.md') ||
fileName.endsWith('.txt') ||
fileName.endsWith('.prompt') ||
fileName.includes('prompt') ||
fileName.includes('template')
) {
return true;
}
} else if (entry.isDirectory()) {
const subPath = path.join(dirPath, entry.name);
if (await this.containsPromptFiles(subPath)) {
return true;
}
}
}
return false;
} catch {
return false;
}
}
}
export class PromptValidator {
static async validatePromptFile(filePath: string): Promise<{
valid: boolean;
issues: string[];
metadata?: any;
}> {
const issues: string[] = [];
let metadata: any = {};
try {
const content = await fs.readFile(filePath, 'utf-8');
// Check for empty files
if (content.trim().length === 0) {
issues.push('File is empty');
}
// Check for common prompt markers
const hasPromptMarkers = [
'# ',
'## ',
'### ', // Markdown headers
'You are',
'Your task',
'Please', // Common prompt starters
'```',
'`', // Code blocks
'{{',
'}}', // Template variables
].some((marker) => content.includes(marker));
if (!hasPromptMarkers) {
issues.push('File may not contain valid prompt content');
}
// Extract metadata from front matter
const frontMatterMatch = content.match(/^---\n([\s\S]*?\n)---/);
if (frontMatterMatch) {
try {
metadata = this.parseFrontMatter(frontMatterMatch[1]);
} catch (error) {
issues.push('Invalid front matter format');
}
}
// Check file size (warn if too large)
const stats = await fs.stat(filePath);
if (stats.size > 100 * 1024) {
// 100KB
issues.push('File is unusually large for a prompt');
}
return {
valid: issues.length === 0,
issues,
metadata,
};
} catch (error) {
return {
valid: false,
issues: [`Failed to read file: ${error instanceof Error ? error.message : String(error)}`],
};
}
}
private static parseFrontMatter(frontMatter: string): any {
// Simple YAML-like parser for basic key-value pairs
const metadata: any = {};
const lines = frontMatter.split('\n');
for (const line of lines) {
const match = line.match(/^(\w+):\s*(.+)$/);
if (match) {
const [, key, value] = match;
metadata[key] = value.trim();
}
}
return metadata;
}
}
export function createProgressBar(total: number): {
update: (current: number) => void;
complete: () => void;
} {
const barLength = 40;
return {
update: (current: number) => {
const percentage = Math.round((current / total) * 100);
const filledLength = Math.round((current / total) * barLength);
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
process.stdout.write(`\r[${bar}] ${percentage}% (${current}/${total})`);
},
complete: () => {
process.stdout.write('\n');
},
};
}
// Utility function to format file sizes
export function formatFileSize(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
// Utility function to format duration
export function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
}