repoweaver
Version:
A GitHub App that skillfully weaves multiple templates together to create and update repositories with intelligent merge strategies
387 lines • 15 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.MergeStrategyRegistry = void 0;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
class MergeStrategyRegistry {
constructor() {
this.strategies = new Map();
this.plugins = new Map();
this.customStrategies = new Map();
// Cache for compiled regex patterns
this.patternRegexCache = new Map();
this.loadBuiltinStrategies();
}
loadBuiltinStrategies() {
// Load built-in merge strategies
const builtinStrategies = [new OverwriteMergeStrategy(), new SkipExistingMergeStrategy(), new SimpleMergeStrategy(), new JsonMergeStrategy(), new PackageJsonMergeStrategy(), new MarkdownMergeStrategy(), new YamlMergeStrategy(), new ConfigMergeStrategy(), new CodeMergeStrategy()];
builtinStrategies.forEach((strategy) => {
this.strategies.set(strategy.name, strategy);
});
}
async loadPlugin(pluginName) {
try {
// Try to load from node_modules first
const pluginPath = require.resolve(`repoweaver-plugin-${pluginName}`);
const pluginModule = await Promise.resolve(`${pluginPath}`).then(s => __importStar(require(s)));
const plugin = pluginModule.default || pluginModule;
if (plugin.initialize) {
await plugin.initialize();
}
// Register plugin strategies
plugin.strategies.forEach((strategy) => {
this.strategies.set(`${pluginName}:${strategy.name}`, strategy);
});
this.plugins.set(pluginName, plugin);
console.log(`✅ Loaded plugin: ${pluginName} (${plugin.version})`);
}
catch (error) {
console.error(`❌ Failed to load plugin ${pluginName}:`, error);
throw new Error(`Plugin ${pluginName} could not be loaded`);
}
}
async loadCustomStrategy(implementation, name) {
try {
const strategyPath = path.resolve(implementation);
// Check if file exists
await fs.access(strategyPath);
// Clear require cache to allow reloading
delete require.cache[require.resolve(strategyPath)];
const strategyModule = require(strategyPath);
const strategy = strategyModule.default || strategyModule;
const strategyName = name || strategy.name || path.basename(implementation, path.extname(implementation));
this.customStrategies.set(strategyName, strategy);
this.strategies.set(strategyName, strategy);
console.log(`✅ Loaded custom strategy: ${strategyName} from ${implementation}`);
}
catch (error) {
console.error(`❌ Failed to load custom strategy ${implementation}:`, error);
throw new Error(`Custom strategy ${implementation} could not be loaded`);
}
}
getStrategy(name) {
return this.strategies.get(name);
}
listStrategies() {
return Array.from(this.strategies.keys());
}
async resolveStrategyForFile(filePath, strategies, defaultStrategy) {
// Sort strategies by priority (higher priority first)
const sortedStrategies = [...strategies].sort((a, b) => (b.priority || 0) - (a.priority || 0));
// Find the first matching strategy
for (const strategyConfig of sortedStrategies) {
if (this.matchesPatterns(filePath, strategyConfig.patterns)) {
return await this.resolveStrategy(strategyConfig.strategy);
}
}
// Fall back to default strategy
if (defaultStrategy) {
return await this.resolveStrategy(defaultStrategy);
}
// Ultimate fallback
return this.getStrategy('merge') || new SimpleMergeStrategy();
}
async resolveStrategy(config) {
switch (config.type) {
case 'overwrite':
return this.getStrategy('overwrite') || new OverwriteMergeStrategy();
case 'merge':
return this.getStrategy('merge') || new SimpleMergeStrategy();
case 'skip':
return this.getStrategy('skip') || new SkipExistingMergeStrategy();
case 'custom':
if (!config.implementation) {
throw new Error('Custom strategy requires implementation path');
}
await this.loadCustomStrategy(config.implementation);
return this.getStrategy(path.basename(config.implementation, path.extname(config.implementation)));
case 'plugin':
if (!config.implementation) {
throw new Error('Plugin strategy requires plugin name');
}
const [pluginName, strategyName] = config.implementation.split(':');
if (!this.plugins.has(pluginName)) {
await this.loadPlugin(pluginName);
}
return this.getStrategy(config.implementation);
default:
throw new Error(`Unknown strategy type: ${config.type}`);
}
}
matchesPatterns(filePath, patterns) {
return patterns.some((pattern) => {
let regex = this.patternRegexCache.get(pattern);
if (!regex) {
regex = new RegExp(pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*'));
this.patternRegexCache.set(pattern, regex);
}
return regex.test(filePath);
});
}
async cleanup() {
// Cleanup plugins
for (const plugin of this.plugins.values()) {
if (plugin.cleanup) {
await plugin.cleanup();
}
}
this.plugins.clear();
this.customStrategies.clear();
}
}
exports.MergeStrategyRegistry = MergeStrategyRegistry;
// Built-in merge strategies
class OverwriteMergeStrategy {
constructor() {
this.name = 'overwrite';
this.description = 'Overwrite existing content with new content';
}
async merge(context) {
return {
success: true,
content: context.newContent,
};
}
}
class SkipExistingMergeStrategy {
constructor() {
this.name = 'skip';
this.description = 'Skip files that already exist';
}
async merge(context) {
return {
success: true,
content: context.existingContent,
};
}
}
class SimpleMergeStrategy {
constructor() {
this.name = 'merge';
this.description = 'Simple merge by appending new content to existing content';
}
async merge(context) {
if (!context.existingContent.trim()) {
return {
success: true,
content: context.newContent,
};
}
const separator = context.options?.separator || '\n\n# --- Template Update ---\n\n';
const content = context.existingContent + separator + context.newContent;
return {
success: true,
content,
warnings: ['Content was appended with separator'],
};
}
}
class JsonMergeStrategy {
constructor() {
this.name = 'json';
this.description = 'Deep merge JSON objects';
}
async merge(context) {
try {
const existingObj = JSON.parse(context.existingContent);
const newObj = JSON.parse(context.newContent);
const merged = this.deepMerge(existingObj, newObj);
return {
success: true,
content: JSON.stringify(merged, null, 2),
metadata: { mergedKeys: Object.keys(merged) },
};
}
catch (error) {
return {
success: false,
content: context.existingContent,
warnings: [`JSON merge failed: ${error}. Using existing content.`],
};
}
}
deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = this.deepMerge(target[key] || {}, source[key]);
}
else {
result[key] = source[key];
}
}
return result;
}
}
class PackageJsonMergeStrategy {
constructor() {
this.name = 'package-json';
this.description = 'Merge package.json files with special handling for dependencies';
}
async merge(context) {
try {
const existingPkg = JSON.parse(context.existingContent);
const newPkg = JSON.parse(context.newContent);
const merged = {
...existingPkg,
...newPkg,
dependencies: { ...existingPkg.dependencies, ...newPkg.dependencies },
devDependencies: { ...existingPkg.devDependencies, ...newPkg.devDependencies },
peerDependencies: { ...existingPkg.peerDependencies, ...newPkg.peerDependencies },
scripts: { ...existingPkg.scripts, ...newPkg.scripts },
};
return {
success: true,
content: JSON.stringify(merged, null, 2),
metadata: {
dependenciesAdded: Object.keys(newPkg.dependencies || {}),
scriptsAdded: Object.keys(newPkg.scripts || {}),
},
};
}
catch (error) {
return {
success: false,
content: context.existingContent,
warnings: [`package.json merge failed: ${error}`],
};
}
}
}
class MarkdownMergeStrategy {
constructor() {
this.name = 'markdown';
this.description = 'Merge Markdown files by appending sections';
}
async merge(context) {
const separator = context.options?.separator || '\n\n---\n\n';
const content = context.existingContent + separator + context.newContent;
return {
success: true,
content,
warnings: ['Markdown content was appended with separator'],
};
}
}
class YamlMergeStrategy {
constructor() {
this.name = 'yaml';
this.description = 'Merge YAML files (requires js-yaml)';
}
async merge(context) {
try {
// Try to use js-yaml if available
const yaml = require('js-yaml');
const existingObj = yaml.load(context.existingContent);
const newObj = yaml.load(context.newContent);
const merged = this.deepMerge(existingObj, newObj);
return {
success: true,
content: yaml.dump(merged, { indent: 2 }),
};
}
catch (error) {
// Fallback to simple merge if js-yaml is not available
const separator = context.options?.separator || '\n\n# --- Template Update ---\n\n';
return {
success: true,
content: context.existingContent + separator + context.newContent,
warnings: ['YAML merge failed, used simple merge instead'],
};
}
}
deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = this.deepMerge(target[key] || {}, source[key]);
}
else {
result[key] = source[key];
}
}
return result;
}
}
class ConfigMergeStrategy {
constructor() {
this.name = 'config';
this.description = 'Merge configuration files (JSON, YAML, TOML)';
}
async merge(context) {
const ext = path.extname(context.filePath).toLowerCase();
switch (ext) {
case '.json':
return new JsonMergeStrategy().merge(context);
case '.yaml':
case '.yml':
return new YamlMergeStrategy().merge(context);
case '.toml':
return this.mergeToml(context);
default:
return new SimpleMergeStrategy().merge(context);
}
}
async mergeToml(context) {
// Simple TOML merge - could be enhanced with a proper TOML parser
const separator = context.options?.separator || '\n\n# --- Template Update ---\n\n';
return {
success: true,
content: context.existingContent + separator + context.newContent,
warnings: ['TOML merge used simple strategy'],
};
}
}
class CodeMergeStrategy {
constructor() {
this.name = 'code';
this.description = 'Merge code files with conflict markers';
}
async merge(context) {
const conflicts = [];
if (context.existingContent.trim() && context.newContent.trim()) {
conflicts.push(`Potential conflict in ${context.filePath}`);
}
const content = `${context.existingContent}\n\n<<<<<<< existing\n${context.existingContent}\n=======\n${context.newContent}\n>>>>>>> ${context.templateName}\n`;
return {
success: true,
content,
conflicts,
warnings: ['Code merge created conflict markers'],
};
}
}
//# sourceMappingURL=merge-strategy-registry.js.map