From 3c55742dfafa120d6e90eaafc738fe6fc00bd38f Mon Sep 17 00:00:00 2001 From: overcuriousity Date: Wed, 23 Jul 2025 21:41:21 +0200 Subject: [PATCH] fix contrib system --- src/pages/api/contribute/tool.ts | 2 +- src/pages/contribute/tool.astro | 372 ++++++++++++++++++------------- src/utils/gitContributions.ts | 234 ++++++++++--------- 3 files changed, 350 insertions(+), 258 deletions(-) diff --git a/src/pages/api/contribute/tool.ts b/src/pages/api/contribute/tool.ts index 2e2c8f0..c1aad7a 100644 --- a/src/pages/api/contribute/tool.ts +++ b/src/pages/api/contribute/tool.ts @@ -37,7 +37,7 @@ const ContributionRequestSchema = z.object({ }), tool: ContributionToolSchema, metadata: z.object({ - reason: z.string().max(500, 'Reason too long').optional() + reason: z.string().transform(val => val.trim() === '' ? undefined : val).optional() }).optional().default({}) }); diff --git a/src/pages/contribute/tool.astro b/src/pages/contribute/tool.astro index 3c4ac11..de7a0e4 100644 --- a/src/pages/contribute/tool.astro +++ b/src/pages/contribute/tool.astro @@ -412,6 +412,8 @@ const title = isEdit ? `Edit ${editTool?.name}` : 'Contribute New Tool'; domainAgnosticSoftware, existingConcepts: existingTools.filter(t => t.type === 'concept') }}> +// REPLACE the JavaScript section at the bottom of tool.astro with this: + document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('contribution-form'); const typeSelect = document.getElementById('tool-type'); @@ -447,23 +449,28 @@ document.addEventListener('DOMContentLoaded', () => { const descriptionCount = document.getElementById('description-count'); const reasonCount = document.getElementById('reason-count'); - descriptionTextarea.addEventListener('input', () => updateCharacterCounter(descriptionTextarea, descriptionCount, 1000)); - reasonTextarea.addEventListener('input', () => updateCharacterCounter(reasonTextarea, reasonCount, 500)); + if (descriptionTextarea && descriptionCount) { + descriptionTextarea.addEventListener('input', () => updateCharacterCounter(descriptionTextarea, descriptionCount, 1000)); + updateCharacterCounter(descriptionTextarea, descriptionCount, 1000); + } - // Initial counter update - updateCharacterCounter(descriptionTextarea, descriptionCount, 1000); - updateCharacterCounter(reasonTextarea, reasonCount, 500); + if (reasonTextarea && reasonCount) { + reasonTextarea.addEventListener('input', () => updateCharacterCounter(reasonTextarea, reasonCount, 500)); + updateCharacterCounter(reasonTextarea, reasonCount, 500); + } // Handle type-specific field visibility function updateFieldVisibility() { const selectedType = typeSelect.value; // Hide all type-specific fields - softwareFields.style.display = 'none'; - relatedConceptsField.style.display = 'none'; + if (softwareFields) softwareFields.style.display = 'none'; + if (relatedConceptsField) relatedConceptsField.style.display = 'none'; // Show project URL for software only - projectUrlField.style.display = selectedType === 'software' ? 'block' : 'none'; + if (projectUrlField) { + projectUrlField.style.display = selectedType === 'software' ? 'block' : 'none'; + } // Handle required fields const platformsCheckboxes = document.querySelectorAll('input[name="platforms"]'); @@ -471,34 +478,38 @@ document.addEventListener('DOMContentLoaded', () => { if (selectedType === 'software') { // Show software-specific fields - softwareFields.style.display = 'block'; - relatedConceptsField.style.display = 'block'; + if (softwareFields) softwareFields.style.display = 'block'; + if (relatedConceptsField) relatedConceptsField.style.display = 'block'; // Make platforms and license required - platformsRequired.style.display = 'inline'; - licenseRequired.style.display = 'inline'; + if (platformsRequired) platformsRequired.style.display = 'inline'; + if (licenseRequired) licenseRequired.style.display = 'inline'; platformsCheckboxes.forEach(cb => cb.setAttribute('required', 'required')); - licenseSelect.setAttribute('required', 'required'); + if (licenseSelect) licenseSelect.setAttribute('required', 'required'); } else { // Hide required indicators and remove requirements - platformsRequired.style.display = 'none'; - licenseRequired.style.display = 'none'; + if (platformsRequired) platformsRequired.style.display = 'none'; + if (licenseRequired) licenseRequired.style.display = 'none'; platformsCheckboxes.forEach(cb => cb.removeAttribute('required')); - licenseSelect.removeAttribute('required'); + if (licenseSelect) licenseSelect.removeAttribute('required'); // Show related concepts for methods - if (selectedType === 'method') { + if (selectedType === 'method' && relatedConceptsField) { relatedConceptsField.style.display = 'block'; } } // Update YAML preview - updateYAMLPreview(); + if (typeof updateYAMLPreview === 'function') { + updateYAMLPreview(); + } } // Generate YAML preview function updateYAMLPreview() { + if (!yamlPreview) return; + try { const formData = new FormData(form); const toolData = { @@ -533,62 +544,64 @@ document.addEventListener('DOMContentLoaded', () => { // Handle related concepts if (toolData.type !== 'concept') { - toolData.related_concepts = formData.getAll('relatedConcepts') || null; + const relatedConcepts = formData.getAll('relatedConcepts'); + toolData.related_concepts = relatedConcepts.length > 0 ? relatedConcepts : null; } - // Convert to YAML-like format for preview - let yamlContent = `- name: "${toolData.name}"\n`; - if (toolData.icon) yamlContent += ` icon: "${toolData.icon}"\n`; - yamlContent += ` type: ${toolData.type}\n`; - yamlContent += ` description: >\n ${toolData.description}\n`; - if (toolData.domains.length > 0) { - yamlContent += ` domains:\n${toolData.domains.map(d => ` - ${d}`).join('\n')}\n`; - } - if (toolData.phases.length > 0) { - yamlContent += ` phases:\n${toolData.phases.map(p => ` - ${p}`).join('\n')}\n`; - } - if (toolData.platforms.length > 0) { - yamlContent += ` platforms:\n${toolData.platforms.map(p => ` - ${p}`).join('\n')}\n`; - } - yamlContent += ` skillLevel: ${toolData.skillLevel}\n`; - if (toolData.accessType) yamlContent += ` accessType: ${toolData.accessType}\n`; - yamlContent += ` url: ${toolData.url}\n`; - if (toolData.projectUrl) yamlContent += ` projectUrl: ${toolData.projectUrl}\n`; - if (toolData.license) yamlContent += ` license: ${toolData.license}\n`; - if (toolData.knowledgebase) yamlContent += ` knowledgebase: ${toolData.knowledgebase}\n`; - if (toolData.related_concepts && toolData.related_concepts.length > 0) { - yamlContent += ` related_concepts:\n${toolData.related_concepts.map(c => ` - ${c}`).join('\n')}\n`; - } - if (toolData.tags.length > 0) { - yamlContent += ` tags:\n${toolData.tags.map(t => ` - ${t}`).join('\n')}\n`; - } + // Generate YAML + yamlPreview.textContent = `name: "${toolData.name}" +${toolData.icon ? `icon: "${toolData.icon}"` : '# icon: "📦"'} +type: ${toolData.type} +description: "${toolData.description}" +domains: [${toolData.domains.map(d => `"${d}"`).join(', ')}] +phases: [${toolData.phases.map(p => `"${p}"`).join(', ')}] +skillLevel: ${toolData.skillLevel} +url: "${toolData.url}"${toolData.platforms.length > 0 ? ` +platforms: [${toolData.platforms.map(p => `"${p}"`).join(', ')}]` : ''}${toolData.license ? ` +license: "${toolData.license}"` : ''}${toolData.accessType ? ` +accessType: ${toolData.accessType}` : ''}${toolData.projectUrl ? ` +projectUrl: "${toolData.projectUrl}"` : ''}${toolData.knowledgebase ? ` +knowledgebase: true` : ''}${toolData.tags.length > 0 ? ` +tags: [${toolData.tags.map(t => `"${t}"`).join(', ')}]` : ''}${toolData.related_concepts ? ` +related_concepts: [${toolData.related_concepts.map(c => `"${c}"`).join(', ')}]` : ''}`; - yamlPreview.textContent = yamlContent; } catch (error) { - yamlPreview.textContent = `# Error generating preview: ${error.message}`; + yamlPreview.textContent = '# Error generating preview'; + console.error('YAML preview error:', error); } } // Form validation function validateForm() { const errors = []; + const formData = new FormData(form); + const selectedType = typeSelect.value; // Basic validation - if (!nameInput.value.trim()) errors.push('Name is required'); - if (!descriptionTextarea.value.trim() || descriptionTextarea.value.length < 10) { - errors.push('Description must be at least 10 characters'); + if (!formData.get('name')?.trim()) { + errors.push('Tool name is required'); } - const selectedType = typeSelect.value; + if (!formData.get('description')?.trim()) { + errors.push('Description is required'); + } + + if (!formData.get('url')?.trim()) { + errors.push('URL is required'); + } + + if (!formData.get('skillLevel')) { + errors.push('Skill level is required'); + } // Type-specific validation if (selectedType === 'software') { - const platforms = new FormData(form).getAll('platforms'); + const platforms = formData.getAll('platforms'); if (platforms.length === 0) { errors.push('At least one platform is required for software'); } - if (!document.getElementById('tool-license').value.trim()) { + if (!document.getElementById('tool-license')?.value?.trim()) { errors.push('License is required for software'); } } @@ -597,124 +610,171 @@ document.addEventListener('DOMContentLoaded', () => { } // Event listeners - typeSelect.addEventListener('change', updateFieldVisibility); - refreshPreviewBtn.addEventListener('click', updateYAMLPreview); + if (typeSelect) { + typeSelect.addEventListener('change', updateFieldVisibility); + } + + if (refreshPreviewBtn) { + refreshPreviewBtn.addEventListener('click', updateYAMLPreview); + } // Update preview on form changes - form.addEventListener('input', debounce(updateYAMLPreview, 500)); - form.addEventListener('change', updateYAMLPreview); + if (form) { + form.addEventListener('input', debounce(updateYAMLPreview, 500)); + form.addEventListener('change', updateYAMLPreview); + } // Form submission - form.addEventListener('submit', async (e) => { - e.preventDefault(); - - const errors = validateForm(); - if (errors.length > 0) { - alert('Please fix the following errors:\n' + errors.join('\n')); - return; - } - - // Show loading state - submitBtn.disabled = true; - submitText.textContent = isEdit ? 'Updating...' : 'Submitting...'; - submitSpinner.style.display = 'inline-block'; - - try { - const formData = new FormData(form); + if (form) { + form.addEventListener('submit', async (e) => { + e.preventDefault(); - // Prepare submission data - const submissionData = { - action: isEdit ? 'edit' : 'add', - tool: { - name: formData.get('name'), - icon: formData.get('icon') || null, - type: formData.get('type'), - description: formData.get('description'), - domains: formData.getAll('domains'), - phases: formData.getAll('phases'), - skillLevel: formData.get('skillLevel'), - url: formData.get('url'), - tags: formData.get('tags') ? formData.get('tags').split(',').map(tag => tag.trim()).filter(Boolean) : [] - }, - metadata: { - reason: formData.get('reason') || null - } - }; - - // Add type-specific fields - if (submissionData.tool.type === 'software') { - submissionData.tool.platforms = formData.getAll('platforms'); - submissionData.tool.license = formData.get('license').trim(); - submissionData.tool.accessType = formData.get('accessType'); - submissionData.tool.projectUrl = formData.get('projectUrl') || null; + const errors = validateForm(); + if (errors.length > 0) { + alert('Please fix the following errors:\n' + errors.join('\n')); + return; } - // Add optional fields - submissionData.tool.knowledgebase = formData.has('knowledgebase') || null; - - if (submissionData.tool.type !== 'concept') { - const relatedConcepts = formData.getAll('relatedConcepts'); - submissionData.tool.related_concepts = relatedConcepts.length > 0 ? relatedConcepts : null; - } + // Show loading state + if (submitBtn) submitBtn.disabled = true; + if (submitText) submitText.textContent = isEdit ? 'Updating...' : 'Submitting...'; + if (submitSpinner) submitSpinner.style.display = 'inline-block'; - const response = await fetch('/api/contribute/tool', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(submissionData) - }); - - const result = await response.json(); - - if (result.success) { - // Show success modal - document.getElementById('success-message').textContent = - `Your ${isEdit ? 'update' : 'contribution'} has been submitted successfully and will be reviewed by the maintainers.`; + try { + const formData = new FormData(form); - if (result.prUrl) { - const prLink = document.getElementById('pr-link'); - prLink.href = result.prUrl; - prLink.style.display = 'inline-flex'; + // FIXED: Prepare submission data with proper metadata.reason handling + const submissionData = { + action: isEdit ? 'edit' : 'add', + tool: { + name: formData.get('name'), + icon: formData.get('icon') || null, + type: formData.get('type'), + description: formData.get('description'), + domains: formData.getAll('domains'), + phases: formData.getAll('phases'), + skillLevel: formData.get('skillLevel'), + url: formData.get('url'), + tags: formData.get('tags') ? formData.get('tags').split(',').map(tag => tag.trim()).filter(Boolean) : [] + }, + metadata: { + // FIXED: Always provide a string, never null + reason: formData.get('reason')?.trim() || '' + } + }; + + // Add type-specific fields + if (submissionData.tool.type === 'software') { + submissionData.tool.platforms = formData.getAll('platforms'); + submissionData.tool.license = formData.get('license')?.trim() || null; + submissionData.tool.accessType = formData.get('accessType') || null; + submissionData.tool.projectUrl = formData.get('projectUrl')?.trim() || null; + } + + // Add optional fields + submissionData.tool.knowledgebase = formData.has('knowledgebase') || null; + + if (submissionData.tool.type !== 'concept') { + const relatedConcepts = formData.getAll('relatedConcepts'); + submissionData.tool.related_concepts = relatedConcepts.length > 0 ? relatedConcepts : null; + } + + console.log('Submitting:', submissionData); // Debug log + + const response = await fetch('/api/contribute/tool', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(submissionData) + }); + + const result = await response.json(); + + if (result.success) { + // Show success modal + if (successModal) { + const successMessage = document.getElementById('success-message'); + if (successMessage) { + successMessage.textContent = `Your ${isEdit ? 'update' : 'contribution'} has been submitted successfully and will be reviewed by the maintainers.`; + } + + if (result.prUrl) { + const prLink = document.getElementById('pr-link'); + if (prLink) { + prLink.href = result.prUrl; + prLink.style.display = 'inline-flex'; + } + } + + successModal.style.display = 'flex'; + } + } else { + let errorMessage = result.error || 'Submission failed'; + if (result.details && Array.isArray(result.details)) { + errorMessage += '\n\nDetails:\n' + result.details.join('\n'); + } + alert(errorMessage); + } + } catch (error) { + console.error('Submission error:', error); + alert('An error occurred while submitting your contribution. Please try again.'); + } finally { + // Reset loading state + if (submitBtn) submitBtn.disabled = false; + if (submitText) submitText.textContent = isEdit ? 'Update Tool' : 'Submit Contribution'; + if (submitSpinner) submitSpinner.style.display = 'none'; + } + }); + } + + // FIXED: Initialize form with proper order + function initializeForm() { + if (isEdit && editTool) { + console.log('Initializing edit form for:', editTool.name); + + // Set basic fields first + if (typeSelect) { + typeSelect.value = editTool.type; + } + + // Update field visibility FIRST + updateFieldVisibility(); + + // THEN set the platform checkboxes and access type after a brief delay + setTimeout(() => { + // FIXED: Set platform checkboxes with more specific selector + if (editTool.platforms) { + editTool.platforms.forEach(platform => { + const checkbox = document.querySelector(`input[name="platforms"][value="${platform}"]`); + if (checkbox) { + checkbox.checked = true; + console.log('Set platform checkbox:', platform); + } else { + console.warn('Platform checkbox not found:', platform); + } + }); } - successModal.style.display = 'flex'; - } else { - let errorMessage = result.error || 'Submission failed'; - if (result.details && Array.isArray(result.details)) { - errorMessage += '\n\nDetails:\n' + result.details.join('\n'); + // FIXED: Set access type value + if (editTool.accessType) { + const accessTypeSelect = document.getElementById('access-type'); + if (accessTypeSelect) { + accessTypeSelect.value = editTool.accessType; + console.log('Set access type:', editTool.accessType); + } } - alert(errorMessage); - } - } catch (error) { - console.error('Submission error:', error); - alert('An error occurred while submitting your contribution. Please try again.'); - } finally { - // Reset loading state - submitBtn.disabled = false; - submitText.textContent = isEdit ? 'Update Tool' : 'Submit Contribution'; - submitSpinner.style.display = 'none'; + + // Update YAML preview after all values are set + updateYAMLPreview(); + }, 100); + } else { + updateFieldVisibility(); } - }); + } // Initialize form - if (isEdit && editTool) { - // Pre-fill edit form - typeSelect.value = editTool.type; - updateFieldVisibility(); - - // Set checkboxes for platforms - if (editTool.platforms) { - editTool.platforms.forEach(platform => { - const checkbox = document.querySelector(`input[value="${platform}"]`); - if (checkbox) checkbox.checked = true; - }); - } - - updateYAMLPreview(); - } else { - updateFieldVisibility(); - } + initializeForm(); // Debounce utility function debounce(func, wait) { diff --git a/src/utils/gitContributions.ts b/src/utils/gitContributions.ts index 2624476..e39ae6b 100644 --- a/src/utils/gitContributions.ts +++ b/src/utils/gitContributions.ts @@ -285,120 +285,152 @@ export class GitContributionManager { } } - // Original tool contribution methods (unchanged) - async submitContribution(data: ContributionData): Promise { - const branchName = `tool-${data.type}-${Date.now()}`; +private generateToolYAML(tool: any): string { + // Clean up the tool object - remove null/undefined values + const cleanTool: any = { + name: tool.name, + type: tool.type, + description: tool.description, + domains: tool.domains || [], + phases: tool.phases || [], + skillLevel: tool.skillLevel, + url: tool.url + }; + + // Add optional fields only if they have values + if (tool.icon) cleanTool.icon = tool.icon; + if (tool.platforms && tool.platforms.length > 0) 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 && tool.related_concepts.length > 0) cleanTool.related_concepts = tool.related_concepts; + if (tool.tags && tool.tags.length > 0) cleanTool.tags = tool.tags; + + // Generate clean YAML + return dump(cleanTool, { + lineWidth: -1, + noRefs: true, + quotingType: '"', + forceQuotes: false, + indent: 2 + }); +} + +async submitContribution(data: ContributionData): Promise { + const branchName = `tool-${data.type}-${Date.now()}`; + + try { + // Create branch + await this.createBranch(branchName); + // FIXED: Don't modify tools.yaml at all - just create the tool data file + const toolYaml = this.generateToolYAML(data.tool); + const contributionFileName = `contribution-${data.type}-${data.tool.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}.yaml`; + + // Create contributions directory if it doesn't exist and write the tool data try { - // Create branch - await this.createBranch(branchName); - - // Load current tools.yaml - const toolsYamlPath = 'src/data/tools.yaml'; - const content = await this.readFile(toolsYamlPath); - const yamlData = load(content) as any; - - if (!yamlData.tools) { - yamlData.tools = []; - } - - // Apply changes - if (data.type === 'add') { - // Check if tool already exists - const existing = yamlData.tools.find((t: any) => t.name === data.tool.name); - if (existing) { - throw new Error(`Tool "${data.tool.name}" already exists`); - } - - yamlData.tools.push(data.tool); - } else if (data.type === 'edit') { - const index = yamlData.tools.findIndex((t: any) => t.name === data.tool.name); - if (index === -1) { - throw new Error(`Tool "${data.tool.name}" not found`); - } - - yamlData.tools[index] = { ...yamlData.tools[index], ...data.tool }; - } - - // Sort tools alphabetically - yamlData.tools.sort((a: any, b: any) => a.name.localeCompare(b.name)); - - // Generate updated YAML - const updatedYaml = dump(yamlData, { - lineWidth: -1, - noRefs: true, - quotingType: '"', - forceQuotes: false - }); - - // Write updated file - await this.writeFile(toolsYamlPath, updatedYaml); - - // Commit changes - const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name} + await this.readFile('contributions/.gitkeep'); + } catch { + // Directory doesn't exist, create it + await this.writeFile('contributions/.gitkeep', '# Contribution files directory\n'); + } + + // Write the tool data as a separate file for maintainers to review + await this.writeFile(`contributions/${contributionFileName}`, toolYaml); + + // Commit changes + const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name} Contributed by: ${data.metadata.submitter} Type: ${data.tool.type} -${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}`; - - await this.commitChanges(commitMessage); - - // Push branch - await this.pushBranch(branchName); - - // Create pull request - const prUrl = await this.createPullRequest( - branchName, - `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`, - this.generatePRDescription(data) - ); - - return { - success: true, - message: `Tool contribution submitted successfully`, - prUrl, - branchName - }; - - } catch (error) { - // Cleanup on failure - try { - await this.deleteBranch(branchName); - } catch (cleanupError) { - console.error('Failed to cleanup branch:', cleanupError); - } - - throw error; - } - } +${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''} - private generatePRDescription(data: ContributionData): string { +This contribution contains the raw tool data for manual review and integration.`; + + await this.commitChanges(commitMessage); + + // Push branch + await this.pushBranch(branchName); + + // Create pull request with enhanced description + const prUrl = await this.createPullRequest( + branchName, + `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`, + this.generateEnhancedPRDescription(data, toolYaml) + ); + + return { + success: true, + message: `Tool contribution submitted successfully`, + prUrl, + branchName + }; + + } catch (error) { + // Cleanup on failure + try { + await this.deleteBranch(branchName); + } catch (cleanupError) { + console.error('Failed to cleanup branch:', cleanupError); + } + + throw error; + } +} + + + private generateEnhancedPRDescription(data: ContributionData, toolYaml: string): string { return `## Tool ${data.type === 'add' ? 'Addition' : 'Update'}: ${data.tool.name} -**Type:** ${data.tool.type} -**Submitted by:** ${data.metadata.submitter} + **Type:** ${data.tool.type} + **Submitted by:** ${data.metadata.submitter} + **Action:** ${data.type === 'add' ? 'Add new tool' : 'Update existing tool'} -### Tool Details -- **Description:** ${data.tool.description} -- **Domains:** ${data.tool.domains.join(', ')} -- **Phases:** ${data.tool.phases.join(', ')} -- **Skill Level:** ${data.tool.skillLevel} -- **License:** ${data.tool.license || 'Not specified'} -- **URL:** ${data.tool.url} + ### Tool Details + - **Name:** ${data.tool.name} + - **Description:** ${data.tool.description} + - **URL:** ${data.tool.url} + - **Skill Level:** ${data.tool.skillLevel} + ${data.tool.platforms && data.tool.platforms.length > 0 ? `- **Platforms:** ${data.tool.platforms.join(', ')}` : ''} + ${data.tool.license ? `- **License:** ${data.tool.license}` : ''} + ${data.tool.accessType ? `- **Access Type:** ${data.tool.accessType}` : ''} + ${data.tool.projectUrl ? `- **Project URL:** ${data.tool.projectUrl}` : ''} + - **Domains:** ${data.tool.domains.join(', ')} + - **Phases:** ${data.tool.phases.join(', ')} + ${data.tool.tags && data.tool.tags.length > 0 ? `- **Tags:** ${data.tool.tags.join(', ')}` : ''} + ${data.tool.related_concepts && data.tool.related_concepts.length > 0 ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''} -${data.metadata.reason ? `### Reason for Contribution\n${data.metadata.reason}` : ''} + ${data.metadata.reason ? `### Reason for Contribution + ${data.metadata.reason} -### Review Checklist -- [ ] Tool information is accurate and complete -- [ ] Description is clear and informative -- [ ] Domains and phases are correctly assigned -- [ ] Tags are relevant and consistent -- [ ] License information is correct -- [ ] URLs are valid and accessible + ` : ''}### Raw Tool Data (Copy & Paste Ready) ---- -*This contribution was submitted via the CC24-Hub web interface.*`; + \`\`\`yaml + ${toolYaml}\`\`\` + + ### For Maintainers + + **To add this tool to tools.yaml:** + 1. Copy the YAML data above + 2. ${data.type === 'add' ? 'Add it to the tools array in the appropriate alphabetical position' : 'Replace the existing tool entry with this updated data'} + 3. Verify all fields are correct + 4. Test that the tool displays properly + 5. Close this PR + + ### Review Checklist + - [ ] Tool information is accurate and complete + - [ ] Description is clear and informative + - [ ] Domains and phases are correctly assigned + - [ ] Tags are relevant and consistent with existing tools + - [ ] License information is correct (for software) + - [ ] URLs are valid and accessible + - [ ] No duplicate tool entries + - [ ] YAML syntax is valid + + --- + *This contribution was submitted via the CC24-Hub web interface and contains only the raw tool data for manual integration.*`; } async checkHealth(): Promise<{healthy: boolean, issues?: string[]}> {