UNPKG

repoweaver

Version:

A GitHub App that skillfully weaves multiple templates together to create and update repositories with intelligent merge strategies

300 lines (247 loc) 8.99 kB
import { GitHubClient, GitHubRepository } from './github-client'; import { GitHubTemplateManager } from './github-template-manager'; import { BootstrapOptions, BootstrapResult, WeaverConfig } from './types'; import { ConfigLoader } from './config-loader'; export interface GitHubBootstrapOptions extends Omit<BootstrapOptions, 'targetPath' | 'initGit' | 'addRemote'> { targetOwner: string; targetRepo: string; createRepository?: boolean; repositoryDescription?: string; privateRepository?: boolean; } export class GitHubBootstrapper { private client: GitHubClient; private templateManager: GitHubTemplateManager; constructor(client: GitHubClient) { this.client = client; this.templateManager = new GitHubTemplateManager(client); } async bootstrap(options: GitHubBootstrapOptions): Promise<BootstrapResult> { const result: BootstrapResult = { success: true, repositoryPath: `${options.targetOwner}/${options.targetRepo}`, templateResults: [], totalFilesProcessed: 0, errors: [] }; try { // Create repository if requested if (options.createRepository) { await this.createRepository(options); } // Process each template for (const template of options.templates) { const templateResult = await this.templateManager.processTemplate( template, options.targetOwner, options.targetRepo, options.excludePatterns, options.mergeStrategy ); result.templateResults.push(templateResult); result.totalFilesProcessed += templateResult.filesProcessed; if (!templateResult.success) { result.success = false; result.errors.push(...templateResult.errors); } } // If all templates were processed successfully, create a summary PR if (result.success && result.templateResults.length > 1) { await this.createSummaryPullRequest(options, result); } } catch (error) { result.success = false; result.errors.push(`Bootstrap failed: ${error}`); } return result; } async updateRepository(options: GitHubBootstrapOptions): Promise<BootstrapResult> { const result: BootstrapResult = { success: true, repositoryPath: `${options.targetOwner}/${options.targetRepo}`, templateResults: [], totalFilesProcessed: 0, errors: [] }; try { // Verify repository exists await this.client.getRepository(options.targetOwner, options.targetRepo); // Process templates with merge strategy for (const template of options.templates) { const templateResult = await this.templateManager.processTemplate( template, options.targetOwner, options.targetRepo, options.excludePatterns, options.mergeStrategy || 'merge' ); result.templateResults.push(templateResult); result.totalFilesProcessed += templateResult.filesProcessed; if (!templateResult.success) { result.success = false; result.errors.push(...templateResult.errors); } } } catch (error) { result.success = false; result.errors.push(`Update failed: ${error}`); } return result; } private async createRepository(options: GitHubBootstrapOptions): Promise<GitHubRepository> { try { return await this.client.createRepository( options.targetRepo, options.repositoryDescription, options.privateRepository ); } catch (error) { throw new Error(`Failed to create repository: ${error}`); } } private async createSummaryPullRequest( options: GitHubBootstrapOptions, result: BootstrapResult ): Promise<void> { try { const branch = `boots-strapper-multi-template-${Date.now()}`; await this.client.createBranch(options.targetOwner, options.targetRepo, branch); // Create a summary file const summaryContent = this.generateSummaryContent(options, result); await this.client.createOrUpdateFile( options.targetOwner, options.targetRepo, 'BOOTSTRAP_SUMMARY.md', summaryContent, 'Add bootstrap summary', branch ); // Create pull request await this.client.createPullRequest( options.targetOwner, options.targetRepo, `Bootstrap repository with ${options.templates.length} templates`, this.generateMultiTemplatePRBody(options, result), branch, 'main' ); } catch (error) { result.errors.push(`Failed to create summary PR: ${error}`); } } private generateSummaryContent(options: GitHubBootstrapOptions, result: BootstrapResult): string { const timestamp = new Date().toISOString(); return ` # Bootstrap Summary **Repository:** ${options.targetOwner}/${options.targetRepo} **Generated:** ${timestamp} **Total Files Processed:** ${result.totalFilesProcessed} ## Templates Applied ${options.templates.map(template => ` ### ${template.name} - **URL:** ${template.url} - **Branch:** ${template.branch || 'main'} ${template.subDirectory ? `- **Subdirectory:** ${template.subDirectory}` : ''} - **Files Processed:** ${result.templateResults.find(r => r.template.name === template.name)?.filesProcessed || 0} `).join('\n')} ## Configuration - **Merge Strategy:** ${options.mergeStrategy || 'merge'} - **Exclude Patterns:** ${options.excludePatterns?.join(', ') || 'None'} --- *Generated by [RepoWeaver](https://github.com/apps/repoweaver)* `.trim(); } private generateMultiTemplatePRBody(options: GitHubBootstrapOptions, result: BootstrapResult): string { return ` ## Multi-Template Bootstrap This pull request bootstraps the repository with **${options.templates.length} templates**. **Summary:** - Total files processed: **${result.totalFilesProcessed}** - Templates applied: **${options.templates.length}** - Merge strategy: **${options.mergeStrategy || 'merge'}** **Templates:** ${options.templates.map((template, index) => { const templateResult = result.templateResults[index]; return `${index + 1}. **${template.name}** - ${templateResult.filesProcessed} files`; }).join('\n')} **Pull Requests Created:** ${result.templateResults .filter(r => r.pullRequestNumber) .map(r => `- ${r.template.name}: #${r.pullRequestNumber}`) .join('\n')} See \`BOOTSTRAP_SUMMARY.md\` for detailed information. --- *This PR was automatically generated by [RepoWeaver](https://github.com/apps/repoweaver)* `.trim(); } async getRepositoryTemplates(owner: string, repo: string): Promise<string[]> { try { // Look for RepoWeaver configuration files const configFiles = ['weaver.json', '.weaver.json', '.repoweaver.json']; for (const configFileName of configFiles) { try { const files = await this.client.getRepositoryContents(owner, repo, configFileName); const configFile = files.find(f => f.name === configFileName); if (configFile) { const config = JSON.parse(configFile.content); return config.templates || []; } } catch (error) { // File doesn't exist, try next one continue; } } return []; } catch (error) { return []; } } async getRepositoryConfig(owner: string, repo: string): Promise<WeaverConfig | null> { try { // Look for RepoWeaver configuration files const configFiles = ['weaver.json', '.weaver.json', '.repoweaver.json']; for (const configFileName of configFiles) { try { const files = await this.client.getRepositoryContents(owner, repo, configFileName); const configFile = files.find(f => f.name === configFileName); if (configFile) { const config = JSON.parse(configFile.content); return config as WeaverConfig; } } catch (error) { // File doesn't exist, try next one continue; } } return null; } catch (error) { return null; } } async saveRepositoryTemplates(owner: string, repo: string, templates: string[]): Promise<void> { const config = { templates, lastUpdated: new Date().toISOString() }; await this.client.createOrUpdateFile( owner, repo, 'weaver.json', JSON.stringify(config, null, 2), 'Update RepoWeaver configuration' ); } async saveRepositoryConfig(owner: string, repo: string, config: WeaverConfig): Promise<void> { const configWithMetadata = { ...config, lastUpdated: new Date().toISOString() }; await this.client.createOrUpdateFile( owner, repo, 'weaver.json', JSON.stringify(configWithMetadata, null, 2), 'Update RepoWeaver configuration' ); } }