// src/utils/gitContributions.ts import { dump } from 'js-yaml'; export interface ContributionData { type: 'add' | 'edit'; tool: { name: string; icon?: string | null; type: 'software' | 'method' | 'concept'; description: string; domains: string[]; phases: string[]; platforms: string[]; skillLevel: string; accessType?: string | null; url: string; projectUrl?: string | null; license?: string | null; knowledgebase?: boolean | null; 'domain-agnostic-software'?: string[] | null; related_concepts?: string[] | null; related_software?: string[] | null; tags: string[]; statusUrl?: string | null; }; metadata: { submitter: string; reason?: string; contact?: string; }; } export interface GitOperationResult { success: boolean; message: string; issueUrl?: string; issueNumber?: number; } interface KnowledgebaseContribution { toolName?: string; title?: string; description?: string; content?: string; externalLink?: string; difficulty?: string; categories?: string[]; tags?: string[]; uploadedFiles?: { name: string; url: string }[]; submitter: string; reason?: string; contact?: string; } interface GitConfig { provider: 'gitea' | 'github' | 'gitlab'; apiEndpoint: string; apiToken: string; repoOwner: string; repoName: string; } export class GitContributionManager { private config: GitConfig; constructor() { const repoUrl = process.env.GIT_REPO_URL || ''; const { owner, name } = this.parseRepoUrl(repoUrl); this.config = { provider: (process.env.GIT_PROVIDER as any) || 'gitea', apiEndpoint: process.env.GIT_API_ENDPOINT || '', apiToken: process.env.GIT_API_TOKEN || '', repoOwner: owner, repoName: name }; if (!this.config.apiEndpoint || !this.config.apiToken) { throw new Error('Missing required git configuration'); } } private parseRepoUrl(url: string): { owner: string; name: string } { const match = url.match(/\/([^\/]+)\/([^\/]+?)(?:\.git)?$/); if (!match) { throw new Error('Invalid repository URL format'); } return { owner: match[1], name: match[2] }; } async submitContribution(data: ContributionData): Promise { try { const toolYaml = this.generateYAML(data.tool); const issueUrl = await this.createIssue(data, toolYaml); return { success: true, message: 'Tool contribution submitted as issue', issueUrl }; } catch (error) { throw new Error(`Issue creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async submitKnowledgebaseContribution(data: KnowledgebaseContribution): Promise { try { const issueUrl = await this.createKnowledgebaseIssue(data); return { success: true, message: 'Knowledge base article submitted as issue', issueUrl }; } catch (error) { throw new Error(`KB issue creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } private generateYAML(tool: any): string { const cleanTool: any = { name: tool.name, type: tool.type, description: tool.description, domains: tool.domains || [], phases: tool.phases || [], skillLevel: tool.skillLevel, url: tool.url }; if (tool.icon) cleanTool.icon = tool.icon; if (tool.platforms?.length) cleanTool.platforms = tool.platforms; if (tool.license) cleanTool.license = tool.license; if (tool.accessType) cleanTool.accessType = tool.accessType; if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl; if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase; if (tool.related_concepts?.length) cleanTool.related_concepts = tool.related_concepts; if (tool.related_software?.length) cleanTool.related_software = tool.related_software; if (tool.tags?.length) cleanTool.tags = tool.tags; if (tool['domain-agnostic-software']?.length) { cleanTool['domain-agnostic-software'] = tool['domain-agnostic-software']; } return dump(cleanTool, { lineWidth: -1, noRefs: true, quotingType: '"', forceQuotes: false, indent: 2 }).trim(); } private async createIssue(data: ContributionData, toolYaml: string): Promise { const title = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`; const body = this.generateIssueBody(data, toolYaml); let apiUrl: string; let requestBody: any; switch (this.config.provider) { case 'gitea': apiUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}/issues`; requestBody = { title, body }; break; case 'github': apiUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}/issues`; requestBody = { title, body }; break; case 'gitlab': apiUrl = `${this.config.apiEndpoint}/projects/${encodeURIComponent(this.config.repoOwner + '/' + this.config.repoName)}/issues`; requestBody = { title, description: body }; break; default: throw new Error(`Unsupported git provider: ${this.config.provider}`); } const response = await fetch(apiUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${this.config.apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const issueData = await response.json(); switch (this.config.provider) { case 'gitea': case 'github': return issueData.html_url || issueData.url; case 'gitlab': return issueData.web_url; default: throw new Error('Unknown provider response format'); } } private async createKnowledgebaseIssue(data: KnowledgebaseContribution): Promise { const title = `Knowledge Base: ${data.title || data.toolName || 'New Article'}`; const body = this.generateKnowledgebaseIssueBody(data); let apiUrl: string; let requestBody: any; switch (this.config.provider) { case 'gitea': apiUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}/issues`; requestBody = { title, body }; break; case 'github': apiUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}/issues`; requestBody = { title, body }; break; case 'gitlab': apiUrl = `${this.config.apiEndpoint}/projects/${encodeURIComponent(this.config.repoOwner + '/' + this.config.repoName)}/issues`; requestBody = { title, description: body }; break; default: throw new Error(`Unsupported git provider: ${this.config.provider}`); } const response = await fetch(apiUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${this.config.apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const issueData = await response.json(); switch (this.config.provider) { case 'gitea': case 'github': return issueData.html_url || issueData.url; case 'gitlab': return issueData.web_url; default: throw new Error('Unknown provider response format'); } } private generateIssueBody(data: ContributionData, toolYaml: string): string { return `## ${data.type === 'add' ? 'Add' : 'Update'} Tool: ${data.tool.name} **Submitted by:** ${data.metadata.submitter} **Type:** ${data.tool.type} **Action:** ${data.type} ### Tool Information - **Name:** ${data.tool.name} - **Description:** ${data.tool.description} - **URL:** ${data.tool.url} - **Skill Level:** ${data.tool.skillLevel} ${data.tool.platforms?.length ? `- **Platforms:** ${data.tool.platforms.join(', ')}` : ''} ${data.tool.license ? `- **License:** ${data.tool.license}` : ''} ${data.tool.domains?.length ? `- **Domains:** ${data.tool.domains.join(', ')}` : ''} ${data.tool.phases?.length ? `- **Phases:** ${data.tool.phases.join(', ')}` : ''} ${data.tool.related_concepts?.length ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''} ${data.tool.related_software?.length ? `- **Related Software:** ${data.tool.related_software.join(', ')}` : ''} ${data.metadata.reason ? `### Reason ${data.metadata.reason} ` : ''}${data.metadata.contact ? `### Contact ${data.metadata.contact} ` : ''}### Copy-Paste YAML \`\`\`yaml - ${toolYaml.split('\n').join('\n ')} \`\`\` ### For Maintainers 1. Copy the YAML above 2. Add to \`src/data/tools.yaml\` in the tools array 3. Maintain alphabetical order 4. Close this issue when done --- *Submitted via ForensicPathways contribution form*`; } private generateKnowledgebaseIssueBody(data: KnowledgebaseContribution): string { const sections: string[] = []; sections.push(`## Knowledge Base Article: ${data.title ?? 'Untitled'}`); sections.push(''); sections.push(`**Submitted by:** ${data.submitter}`); if (data.contact) sections.push(`**Contact:** ${data.contact}`); if (data.toolName) sections.push(`**Related Tool:** ${data.toolName}`); if (data.difficulty) sections.push(`**Difficulty:** ${data.difficulty}`); sections.push(''); if (data.description) { sections.push('### Description'); sections.push(data.description); sections.push(''); } if (data.content) { sections.push('### Article Content'); sections.push('```markdown'); sections.push(data.content); sections.push('```'); sections.push(''); } if (data.externalLink) { sections.push('### External Resource'); sections.push(`- [External Documentation](${data.externalLink})`); sections.push(''); } if (Array.isArray(data.uploadedFiles) && data.uploadedFiles.length) { sections.push('### Uploaded Files'); data.uploadedFiles.forEach((file) => { const fileType = file.name.toLowerCase(); let icon = '📎'; if (fileType.endsWith('.pdf')) icon = '📄'; else if (/(png|jpe?g|gif|webp)$/.test(fileType)) icon = '🖼️'; else if (/(mp4|webm|mov|avi)$/.test(fileType)) icon = '🎥'; else if (/(docx?)$/.test(fileType)) icon = '📝'; else if (/(zip|tar|gz)$/.test(fileType)) icon = '📦'; sections.push(`- ${icon} [${file.name}](${file.url})`); }); sections.push(''); } const hasCategories = Array.isArray(data.categories) && data.categories.length > 0; const hasTags = Array.isArray(data.tags) && data.tags.length > 0; if (hasCategories || hasTags) { sections.push('### Metadata'); if (hasCategories) sections.push(`**Categories:** ${data.categories!.join(', ')}`); if (hasTags) sections.push(`**Tags:** ${data.tags!.join(', ')}`); sections.push(''); } if (data.reason) { sections.push('### Reason for Contribution'); sections.push(data.reason); sections.push(''); } sections.push('### For Maintainers'); sections.push('1. Review the content for quality and accuracy'); sections.push('2. Create the appropriate markdown file in `src/content/knowledgebase/`'); sections.push('3. Process any uploaded files as needed'); sections.push('4. Close this issue when the article is published'); sections.push(''); sections.push('---'); sections.push('*Submitted via ForensicPathways knowledge base contribution form*'); return sections.join('\n'); } }