simplify tool contribution
This commit is contained in:
		
							parent
							
								
									d80a4d85eb
								
							
						
					
					
						commit
						6892aaf7de
					
				@ -115,17 +115,6 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Validate related concepts exist
 | 
			
		||||
    if (tool.related_concepts && tool.related_concepts.length > 0) {
 | 
			
		||||
      const existingConcepts = new Set(
 | 
			
		||||
        existingData.tools.filter((t: any) => t.type === 'concept').map((t: any) => t.name)
 | 
			
		||||
      );
 | 
			
		||||
      const invalidConcepts = tool.related_concepts.filter((c: string) => !existingConcepts.has(c));
 | 
			
		||||
      if (invalidConcepts.length > 0) {
 | 
			
		||||
        errors.push(`Referenced concepts not found: ${invalidConcepts.join(', ')}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return { valid: errors.length === 0, errors };
 | 
			
		||||
    
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
@ -199,23 +188,21 @@ export const POST: APIRoute = async ({ request }) => {
 | 
			
		||||
 | 
			
		||||
    // CRITICAL FIX: Enhanced error handling for Git operations
 | 
			
		||||
    try {
 | 
			
		||||
      // Submit contribution via Git (now creates issue instead of PR)
 | 
			
		||||
      const gitManager = new GitContributionManager();
 | 
			
		||||
      const result = await gitManager.submitContribution(contributionData);
 | 
			
		||||
 | 
			
		||||
      if (result.success) {
 | 
			
		||||
        // Log successful contribution
 | 
			
		||||
        console.log(`[CONTRIBUTION SUCCESS] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail} - PR: ${result.prUrl}`);
 | 
			
		||||
        console.log(`[CONTRIBUTION] Issue created for "${validatedData.tool.name}" by ${userEmail} - Issue: ${result.issueUrl}`);
 | 
			
		||||
        
 | 
			
		||||
        // ENSURE proper success response
 | 
			
		||||
        return apiResponse.created({
 | 
			
		||||
          success: true,
 | 
			
		||||
          message: result.message,
 | 
			
		||||
          prUrl: result.prUrl,
 | 
			
		||||
          branchName: result.branchName
 | 
			
		||||
          issueUrl: result.issueUrl,
 | 
			
		||||
          issueNumber: result.issueNumber
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        // Log failed contribution
 | 
			
		||||
        console.error(`[CONTRIBUTION FAILED] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail}: ${result.message}`);
 | 
			
		||||
        console.error(`[CONTRIBUTION FAILED] "${validatedData.tool.name}" by ${userEmail}: ${result.message}`);
 | 
			
		||||
        
 | 
			
		||||
        return apiServerError.internal(`Contribution failed: ${result.message}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -30,8 +30,8 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
        <form id="kb-form" style="padding: 2rem;">
 | 
			
		||||
          <!-- Tool Selection -->
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <label for="tool-name" class="form-label required">Related Tool</label>
 | 
			
		||||
            <select id="tool-name" name="toolName" required class="form-input">
 | 
			
		||||
            <label for="tool-name" class="form-label">Related Tool</label>
 | 
			
		||||
            <select id="tool-name" name="toolName" class="form-input">
 | 
			
		||||
              <option value="">Select a tool</option>
 | 
			
		||||
              {sortedTools.map(tool => (
 | 
			
		||||
                <option value={tool.name}>{tool.name} ({tool.type})</option>
 | 
			
		||||
@ -41,12 +41,11 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
 | 
			
		||||
          <!-- Article Title -->
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <label for="title" class="form-label required">Article Title</label>
 | 
			
		||||
            <label for="title" class="form-label">Article Title</label>
 | 
			
		||||
            <input 
 | 
			
		||||
              type="text" 
 | 
			
		||||
              id="title" 
 | 
			
		||||
              name="title" 
 | 
			
		||||
              required 
 | 
			
		||||
              name="title"  
 | 
			
		||||
              maxlength="100"
 | 
			
		||||
              placeholder="Clear, descriptive title for your article"
 | 
			
		||||
              class="form-input"
 | 
			
		||||
@ -55,11 +54,10 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
 | 
			
		||||
          <!-- Description -->
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <label for="description" class="form-label required">Description</label>
 | 
			
		||||
            <label for="description" class="form-label">Description</label>
 | 
			
		||||
            <textarea 
 | 
			
		||||
              id="description" 
 | 
			
		||||
              name="description" 
 | 
			
		||||
              required 
 | 
			
		||||
              name="description"  
 | 
			
		||||
              maxlength="300"
 | 
			
		||||
              rows="3"
 | 
			
		||||
              placeholder="Brief summary of what this article covers (20-300 characters)"
 | 
			
		||||
@ -117,8 +115,8 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
 | 
			
		||||
          <!-- Difficulty Level -->
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <label for="difficulty" class="form-label required">Difficulty Level</label>
 | 
			
		||||
            <select id="difficulty" name="difficulty" required class="form-input">
 | 
			
		||||
            <label for="difficulty" class="form-label">Difficulty Level</label>
 | 
			
		||||
            <select id="difficulty" name="difficulty" class="form-input">
 | 
			
		||||
              <option value="">Select difficulty</option>
 | 
			
		||||
              <option value="novice">Novice - No prior experience needed</option>
 | 
			
		||||
              <option value="beginner">Beginner - Basic familiarity helpful</option>
 | 
			
		||||
@ -168,7 +166,7 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
 | 
			
		||||
          <!-- Submit Button -->
 | 
			
		||||
          <div style="display: flex; gap: 1rem; margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--color-border);">
 | 
			
		||||
            <button type="submit" id="submit-btn" class="btn btn-accent" style="flex: 1;" disabled>
 | 
			
		||||
            <button type="submit" id="submit-btn" class="btn btn-accent" style="flex: 1;">
 | 
			
		||||
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
 | 
			
		||||
              </svg>
 | 
			
		||||
@ -204,85 +202,116 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // State management
 | 
			
		||||
  let uploadedFiles: Array<{id: string, file: File, name: string, uploaded: boolean, url?: string}> = [];
 | 
			
		||||
 | 
			
		||||
  // DOM elements
 | 
			
		||||
  const form = document.getElementById('kb-form') as HTMLFormElement;
 | 
			
		||||
  const submitBtn = document.getElementById('submit-btn') as HTMLButtonElement;
 | 
			
		||||
  const fileInput = document.getElementById('file-input') as HTMLInputElement;
 | 
			
		||||
  const uploadArea = document.getElementById('upload-area') as HTMLElement;
 | 
			
		||||
  const fileList = document.getElementById('file-list') as HTMLElement;
 | 
			
		||||
  const filesContainer = document.getElementById('files-container') as HTMLElement;
 | 
			
		||||
 | 
			
		||||
  // Form validation
 | 
			
		||||
  function validateForm(): boolean {
 | 
			
		||||
    const toolName = (document.getElementById('tool-name') as HTMLSelectElement).value;
 | 
			
		||||
    const title = (document.getElementById('title') as HTMLInputElement).value;
 | 
			
		||||
    const description = (document.getElementById('description') as HTMLTextAreaElement).value;
 | 
			
		||||
    const content = (document.getElementById('content') as HTMLTextAreaElement).value;
 | 
			
		||||
    const externalLink = (document.getElementById('external-link') as HTMLInputElement).value;
 | 
			
		||||
    const difficulty = (document.getElementById('difficulty') as HTMLSelectElement).value;
 | 
			
		||||
 | 
			
		||||
    const hasContent = content.trim().length > 0 || uploadedFiles.length > 0 || externalLink.trim().length > 0;
 | 
			
		||||
    
 | 
			
		||||
    return Boolean(toolName) && Boolean(title) && Boolean(description) && Boolean(difficulty) && hasContent;
 | 
			
		||||
  // FIXED: Properly typed interfaces for TypeScript compliance
 | 
			
		||||
  interface UploadedFile {
 | 
			
		||||
    id: string;
 | 
			
		||||
    file: File;
 | 
			
		||||
    name: string;
 | 
			
		||||
    uploaded: boolean;
 | 
			
		||||
    url?: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Update submit button state
 | 
			
		||||
  function updateSubmitButton() {
 | 
			
		||||
    if (submitBtn) {
 | 
			
		||||
      submitBtn.disabled = !validateForm();
 | 
			
		||||
  // Extend Window interface for global functions
 | 
			
		||||
  declare global {
 | 
			
		||||
    interface Window {
 | 
			
		||||
      removeFile: (fileId: string) => void;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // File upload handling
 | 
			
		||||
  function setupFileUpload() {
 | 
			
		||||
    if (!fileInput || !uploadArea) return;
 | 
			
		||||
  // FIXED: State management with proper typing
 | 
			
		||||
  let uploadedFiles: UploadedFile[] = [];
 | 
			
		||||
 | 
			
		||||
    uploadArea.addEventListener('click', () => fileInput.click());
 | 
			
		||||
    
 | 
			
		||||
    uploadArea.addEventListener('dragover', (e) => {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      uploadArea.style.borderColor = 'var(--color-accent)';
 | 
			
		||||
  // FIXED: Properly typed element selection with specific HTML element types
 | 
			
		||||
  const elements = {
 | 
			
		||||
    form: document.getElementById('kb-form') as HTMLFormElement | null,
 | 
			
		||||
    submitBtn: document.getElementById('submit-btn') as HTMLButtonElement | null,
 | 
			
		||||
    fileInput: document.getElementById('file-input') as HTMLInputElement | null,
 | 
			
		||||
    uploadArea: document.getElementById('upload-area') as HTMLElement | null,
 | 
			
		||||
    fileList: document.getElementById('file-list') as HTMLElement | null,
 | 
			
		||||
    filesContainer: document.getElementById('files-container') as HTMLElement | null
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Check for critical elements
 | 
			
		||||
  const criticalElements: Array<keyof typeof elements> = ['form', 'submitBtn'];
 | 
			
		||||
  const missingElements = criticalElements.filter(key => !elements[key]);
 | 
			
		||||
  
 | 
			
		||||
  if (missingElements.length > 0) {
 | 
			
		||||
    console.error('[KB FORM ERROR] Missing critical elements:', missingElements);
 | 
			
		||||
  } else {
 | 
			
		||||
    console.log('[KB FORM DEBUG] All critical elements found, initializing form');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function validateForm(): boolean {
 | 
			
		||||
    return true; // Always return true - no validation
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Update submit button state with null checks
 | 
			
		||||
  function updateSubmitButton(): void {
 | 
			
		||||
    if (elements.submitBtn) {
 | 
			
		||||
      const isValid = validateForm();
 | 
			
		||||
      elements.submitBtn.disabled = !isValid;
 | 
			
		||||
      console.log('[KB FORM DEBUG] Button state:', isValid ? 'enabled' : 'disabled');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // File upload handling with proper null checks
 | 
			
		||||
  function setupFileUpload(): void {
 | 
			
		||||
    if (!elements.fileInput || !elements.uploadArea) return;
 | 
			
		||||
 | 
			
		||||
    elements.uploadArea.addEventListener('click', () => {
 | 
			
		||||
      if (elements.fileInput) {
 | 
			
		||||
        elements.fileInput.click();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    uploadArea.addEventListener('dragleave', () => {
 | 
			
		||||
      uploadArea.style.borderColor = 'var(--color-border)';
 | 
			
		||||
    elements.uploadArea.addEventListener('dragover', (e: DragEvent) => {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      if (elements.uploadArea) {
 | 
			
		||||
        elements.uploadArea.style.borderColor = 'var(--color-accent)';
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    uploadArea.addEventListener('drop', (e) => {
 | 
			
		||||
    elements.uploadArea.addEventListener('dragleave', () => {
 | 
			
		||||
      if (elements.uploadArea) {
 | 
			
		||||
        elements.uploadArea.style.borderColor = 'var(--color-border)';
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    elements.uploadArea.addEventListener('drop', (e: DragEvent) => {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      uploadArea.style.borderColor = 'var(--color-border)';
 | 
			
		||||
      if (elements.uploadArea) {
 | 
			
		||||
        elements.uploadArea.style.borderColor = 'var(--color-border)';
 | 
			
		||||
      }
 | 
			
		||||
      if (e.dataTransfer?.files) {
 | 
			
		||||
        handleFiles(Array.from(e.dataTransfer.files));
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    fileInput.addEventListener('change', (e) => {
 | 
			
		||||
    elements.fileInput.addEventListener('change', (e: Event) => {
 | 
			
		||||
      const target = e.target as HTMLInputElement;
 | 
			
		||||
      if (target.files) {
 | 
			
		||||
      if (target?.files) {
 | 
			
		||||
        handleFiles(Array.from(target.files));
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function handleFiles(files: File[]) {
 | 
			
		||||
  function handleFiles(files: File[]): void {
 | 
			
		||||
    files.forEach(file => {
 | 
			
		||||
      const fileId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
 | 
			
		||||
      uploadedFiles.push({
 | 
			
		||||
      const newFile: UploadedFile = {
 | 
			
		||||
        id: fileId,
 | 
			
		||||
        file,
 | 
			
		||||
        name: file.name,
 | 
			
		||||
        uploaded: false
 | 
			
		||||
      });
 | 
			
		||||
      };
 | 
			
		||||
      uploadedFiles.push(newFile);
 | 
			
		||||
      uploadFile(fileId);
 | 
			
		||||
    });
 | 
			
		||||
    renderFileList();
 | 
			
		||||
    updateSubmitButton();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function uploadFile(fileId: string) {
 | 
			
		||||
  async function uploadFile(fileId: string): Promise<void> {
 | 
			
		||||
    const fileItem = uploadedFiles.find(f => f.id === fileId);
 | 
			
		||||
    if (!fileItem) return;
 | 
			
		||||
 | 
			
		||||
@ -310,18 +339,18 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function removeFile(fileId: string) {
 | 
			
		||||
  function removeFile(fileId: string): void {
 | 
			
		||||
    uploadedFiles = uploadedFiles.filter(f => f.id !== fileId);
 | 
			
		||||
    renderFileList();
 | 
			
		||||
    updateSubmitButton();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function renderFileList() {
 | 
			
		||||
    if (!filesContainer || !fileList) return;
 | 
			
		||||
  function renderFileList(): void {
 | 
			
		||||
    if (!elements.filesContainer || !elements.fileList) return;
 | 
			
		||||
 | 
			
		||||
    if (uploadedFiles.length > 0) {
 | 
			
		||||
      fileList.style.display = 'block';
 | 
			
		||||
      filesContainer.innerHTML = uploadedFiles.map(file => `
 | 
			
		||||
      elements.fileList.style.display = 'block';
 | 
			
		||||
      elements.filesContainer.innerHTML = uploadedFiles.map(file => `
 | 
			
		||||
        <div class="file-item" style="display: flex; align-items: center; gap: 1rem; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: 0.25rem; margin-bottom: 0.5rem;">
 | 
			
		||||
          <div style="flex: 1;">
 | 
			
		||||
            <strong>${file.name}</strong>
 | 
			
		||||
@ -333,29 +362,32 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
              }
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <button type="button" onclick="removeFile('${file.id}')" class="btn btn-small" style="background: var(--color-danger); color: white;">Remove</button>
 | 
			
		||||
          <button type="button" onclick="window.removeFile('${file.id}')" class="btn btn-small" style="background: var(--color-danger); color: white;">Remove</button>
 | 
			
		||||
        </div>
 | 
			
		||||
      `).join('');
 | 
			
		||||
    } else {
 | 
			
		||||
      fileList.style.display = 'none';
 | 
			
		||||
      elements.fileList.style.display = 'none';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Form submission
 | 
			
		||||
  async function handleSubmit(e: Event) {
 | 
			
		||||
  async function handleSubmit(e: Event): Promise<void> {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    console.log('[KB FORM DEBUG] Form submitted');
 | 
			
		||||
    
 | 
			
		||||
    if (!submitBtn || !form || submitBtn.disabled) return;
 | 
			
		||||
    if (!elements.submitBtn || !elements.form) {
 | 
			
		||||
      console.log('[KB FORM DEBUG] Submission blocked - form missing');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    submitBtn.classList.add('loading');
 | 
			
		||||
    submitBtn.innerHTML = '⏳ Submitting...';
 | 
			
		||||
    elements.submitBtn.classList.add('loading');
 | 
			
		||||
    elements.submitBtn.innerHTML = '⏳ Submitting...';
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const formData = new FormData(form);
 | 
			
		||||
      const formData = new FormData(elements.form);
 | 
			
		||||
      
 | 
			
		||||
      // Process categories and tags
 | 
			
		||||
      const categoriesValue = formData.get('categories') as string || '';
 | 
			
		||||
      const tagsValue = formData.get('tags') as string || '';
 | 
			
		||||
      // Process categories and tags with proper null handling
 | 
			
		||||
      const categoriesValue = (formData.get('categories') as string) || '';
 | 
			
		||||
      const tagsValue = (formData.get('tags') as string) || '';
 | 
			
		||||
      
 | 
			
		||||
      const categories = categoriesValue.split(',').map(s => s.trim()).filter(s => s);
 | 
			
		||||
      const tags = tagsValue.split(',').map(s => s.trim()).filter(s => s);
 | 
			
		||||
@ -365,12 +397,15 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
      // Add uploaded files
 | 
			
		||||
      formData.set('uploadedFiles', JSON.stringify(uploadedFiles.filter(f => f.uploaded)));
 | 
			
		||||
 | 
			
		||||
      console.log('[KB FORM DEBUG] Submitting to API...');
 | 
			
		||||
      const response = await fetch('/api/contribute/knowledgebase', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        body: formData
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      console.log('[KB FORM DEBUG] Response status:', response.status);
 | 
			
		||||
      const result = await response.json();
 | 
			
		||||
      console.log('[KB FORM DEBUG] Response data:', result);
 | 
			
		||||
 | 
			
		||||
      if (result.success) {
 | 
			
		||||
        // Show success modal
 | 
			
		||||
@ -389,8 +424,10 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
          successModal.style.display = 'flex';
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Reset form
 | 
			
		||||
        form.reset();
 | 
			
		||||
        // Reset form with proper typing
 | 
			
		||||
        if (elements.form) {
 | 
			
		||||
          elements.form.reset();
 | 
			
		||||
        }
 | 
			
		||||
        uploadedFiles = [];
 | 
			
		||||
        renderFileList();
 | 
			
		||||
        updateSubmitButton();
 | 
			
		||||
@ -399,14 +436,17 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('[KB FORM ERROR] Submission error:', error);
 | 
			
		||||
      showMessage('error', 'An error occurred during submission');
 | 
			
		||||
    } finally {
 | 
			
		||||
      submitBtn.classList.remove('loading');
 | 
			
		||||
      submitBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/></svg> Submit Article';
 | 
			
		||||
      if (elements.submitBtn) {
 | 
			
		||||
        elements.submitBtn.classList.remove('loading');
 | 
			
		||||
        elements.submitBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/></svg> Submit Article';
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function showMessage(type: 'success' | 'error' | 'warning', message: string) {
 | 
			
		||||
  function showMessage(type: 'success' | 'error' | 'warning', message: string): void {
 | 
			
		||||
    const container = document.getElementById('message-container');
 | 
			
		||||
    if (!container) return;
 | 
			
		||||
 | 
			
		||||
@ -423,57 +463,10 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
    setTimeout(() => messageEl.remove(), 5000);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Event listeners
 | 
			
		||||
  if (form) {
 | 
			
		||||
    form.addEventListener('submit', handleSubmit);
 | 
			
		||||
    form.addEventListener('input', updateSubmitButton);
 | 
			
		||||
    form.addEventListener('change', updateSubmitButton);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Make removeFile available globally
 | 
			
		||||
  (window as any).removeFile = removeFile;
 | 
			
		||||
  // Make removeFile available globally with proper typing
 | 
			
		||||
  window.removeFile = removeFile;
 | 
			
		||||
 | 
			
		||||
  // Initialize
 | 
			
		||||
  setupFileUpload();
 | 
			
		||||
  updateSubmitButton();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  .upload-area {
 | 
			
		||||
    border: 2px dashed var(--color-border);
 | 
			
		||||
    border-radius: 0.5rem;
 | 
			
		||||
    padding: 2rem;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    transition: var(--transition-fast);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .upload-area:hover {
 | 
			
		||||
    border-color: var(--color-accent);
 | 
			
		||||
    background-color: var(--color-background-secondary);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .upload-placeholder svg {
 | 
			
		||||
    color: var(--color-text-tertiary);
 | 
			
		||||
    margin-bottom: 0.5rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .file-item {
 | 
			
		||||
    animation: fadeIn 0.3s ease;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes fadeIn {
 | 
			
		||||
    from { opacity: 0; transform: translateY(-10px); }
 | 
			
		||||
    to { opacity: 1; transform: translateY(0); }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes slideIn {
 | 
			
		||||
    from { transform: translateX(100%); opacity: 0; }
 | 
			
		||||
    to { transform: translateX(0); opacity: 1; }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .btn.loading {
 | 
			
		||||
    opacity: 0.7;
 | 
			
		||||
    pointer-events: none;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
  console.log('[KB FORM DEBUG] Form initialization complete');
 | 
			
		||||
</script>
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,8 +1,5 @@
 | 
			
		||||
// src/utils/gitContributions.ts - Enhanced for Phase 3 with YAML preservation
 | 
			
		||||
import { execSync, spawn } from 'child_process';
 | 
			
		||||
import { promises as fs } from 'fs';
 | 
			
		||||
import { load, dump } from 'js-yaml';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
// src/utils/gitContributions.ts - MINIMAL: Issues only, no filesystem/git operations
 | 
			
		||||
import { dump } from 'js-yaml';
 | 
			
		||||
 | 
			
		||||
export interface ContributionData {
 | 
			
		||||
  type: 'add' | 'edit';
 | 
			
		||||
@ -34,413 +31,63 @@ export interface ContributionData {
 | 
			
		||||
export interface GitOperationResult {
 | 
			
		||||
  success: boolean;
 | 
			
		||||
  message: string;
 | 
			
		||||
  prUrl?: string;
 | 
			
		||||
  branchName?: string;
 | 
			
		||||
  issueUrl?: string;
 | 
			
		||||
  issueNumber?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface GitConfig {
 | 
			
		||||
  localRepoPath: string;
 | 
			
		||||
  provider: 'gitea' | 'github' | 'gitlab';
 | 
			
		||||
  apiEndpoint: string;
 | 
			
		||||
  apiToken: string;
 | 
			
		||||
  repoUrl: string;
 | 
			
		||||
  repoOwner: string;
 | 
			
		||||
  repoName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class GitContributionManager {
 | 
			
		||||
  protected config: GitConfig;
 | 
			
		||||
  private activeBranches = new Set<string>();
 | 
			
		||||
  private config: GitConfig;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    const repoUrl = process.env.GIT_REPO_URL || '';
 | 
			
		||||
    const { owner, name } = this.parseRepoUrl(repoUrl);
 | 
			
		||||
    
 | 
			
		||||
    this.config = {
 | 
			
		||||
      localRepoPath: process.env.LOCAL_REPO_PATH || '/var/git/cc24-hub',
 | 
			
		||||
      provider: (process.env.GIT_PROVIDER as any) || 'gitea',
 | 
			
		||||
      apiEndpoint: process.env.GIT_API_ENDPOINT || '',
 | 
			
		||||
      apiToken: process.env.GIT_API_TOKEN || '',
 | 
			
		||||
      repoUrl,
 | 
			
		||||
      repoOwner: owner,
 | 
			
		||||
      repoName: name
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (!this.config.apiEndpoint || !this.config.apiToken || !this.config.repoUrl) {
 | 
			
		||||
    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<GitOperationResult> {
 | 
			
		||||
    try {
 | 
			
		||||
      // Parse URLs like: https://git.cc24.dev/mstoeck3/cc24-hub.git
 | 
			
		||||
      const match = url.match(/\/([^\/]+)\/([^\/]+?)(?:\.git)?$/);
 | 
			
		||||
      if (!match) {
 | 
			
		||||
        throw new Error('Invalid repository URL format');
 | 
			
		||||
      }
 | 
			
		||||
      return { owner: match[1], name: match[2] };
 | 
			
		||||
      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(`Failed to parse repository URL: ${url}`);
 | 
			
		||||
      throw new Error(`Issue creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Enhanced git operations for Phase 3
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create a new branch
 | 
			
		||||
   */
 | 
			
		||||
  protected async createBranch(branchName: string): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      // Ensure we're on main and up to date
 | 
			
		||||
      execSync('git checkout main', { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
      execSync('git pull origin main', { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
      
 | 
			
		||||
      // Create and checkout new branch
 | 
			
		||||
      execSync(`git checkout -b "${branchName}"`, { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
      
 | 
			
		||||
      this.activeBranches.add(branchName);
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw new Error(`Failed to create branch ${branchName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Write file to repository
 | 
			
		||||
   */
 | 
			
		||||
  protected async writeFile(filePath: string, content: string): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const fullPath = path.join(this.config.localRepoPath, filePath);
 | 
			
		||||
      const dirPath = path.dirname(fullPath);
 | 
			
		||||
      
 | 
			
		||||
      // Ensure directory exists
 | 
			
		||||
      await fs.mkdir(dirPath, { recursive: true });
 | 
			
		||||
      
 | 
			
		||||
      // Write file
 | 
			
		||||
      await fs.writeFile(fullPath, content, 'utf8');
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw new Error(`Failed to write file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Read file from repository
 | 
			
		||||
   */
 | 
			
		||||
  protected async readFile(filePath: string): Promise<string> {
 | 
			
		||||
    try {
 | 
			
		||||
      const fullPath = path.join(this.config.localRepoPath, filePath);
 | 
			
		||||
      return await fs.readFile(fullPath, 'utf8');
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw new Error(`Failed to read file ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Commit changes with message
 | 
			
		||||
   */
 | 
			
		||||
  protected async commitChanges(message: string): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      // Add all changes
 | 
			
		||||
      execSync('git add .', { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
      
 | 
			
		||||
      // Check if there are any changes to commit
 | 
			
		||||
      try {
 | 
			
		||||
        execSync('git diff --cached --exit-code', { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
        // If we get here, there are no changes
 | 
			
		||||
        throw new Error('No changes to commit');
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        // This is expected - it means there are changes to commit
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Set git config if not already set
 | 
			
		||||
      try {
 | 
			
		||||
        execSync('git config user.email "contributions@cc24-hub.local"', { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
        execSync('git config user.name "CC24-Hub Contributions"', { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
      } catch {
 | 
			
		||||
        // Config might already be set
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Commit changes
 | 
			
		||||
      execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw new Error(`Failed to commit changes: ${error instanceof Error ? error.message : 'Unknown error'}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Push branch to remote
 | 
			
		||||
   */
 | 
			
		||||
  protected async pushBranch(branchName: string): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      execSync(`git push -u origin "${branchName}"`, { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw new Error(`Failed to push branch ${branchName}: ${error instanceof Error ? error.message : 'Unknown error'}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Delete branch (cleanup)
 | 
			
		||||
   */
 | 
			
		||||
  protected async deleteBranch(branchName: string): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      // Switch to main first
 | 
			
		||||
      execSync('git checkout main', { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
      
 | 
			
		||||
      // Delete local branch
 | 
			
		||||
      execSync(`git branch -D "${branchName}"`, { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
      
 | 
			
		||||
      // Delete remote branch if it exists
 | 
			
		||||
      try {
 | 
			
		||||
        execSync(`git push origin --delete "${branchName}"`, { cwd: this.config.localRepoPath, stdio: 'pipe' });
 | 
			
		||||
      } catch {
 | 
			
		||||
        // Branch might not exist on remote yet
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      this.activeBranches.delete(branchName);
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.warn(`Failed to cleanup branch ${branchName}:`, error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create pull request
 | 
			
		||||
   */
 | 
			
		||||
  protected async createPullRequest(branchName: string, title: string, body: string): Promise<string> {
 | 
			
		||||
    try {
 | 
			
		||||
      let apiUrl: string;
 | 
			
		||||
      let requestBody: any;
 | 
			
		||||
 | 
			
		||||
      switch (this.config.provider) {
 | 
			
		||||
        case 'gitea':
 | 
			
		||||
          apiUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}/pulls`;
 | 
			
		||||
          requestBody = {
 | 
			
		||||
            title,
 | 
			
		||||
            body,
 | 
			
		||||
            head: branchName,
 | 
			
		||||
            base: 'main'
 | 
			
		||||
          };
 | 
			
		||||
          break;
 | 
			
		||||
        
 | 
			
		||||
        case 'github':
 | 
			
		||||
          apiUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}/pulls`;
 | 
			
		||||
          requestBody = {
 | 
			
		||||
            title,
 | 
			
		||||
            body,
 | 
			
		||||
            head: branchName,
 | 
			
		||||
            base: 'main'
 | 
			
		||||
          };
 | 
			
		||||
          break;
 | 
			
		||||
          
 | 
			
		||||
        case 'gitlab':
 | 
			
		||||
          apiUrl = `${this.config.apiEndpoint}/projects/${encodeURIComponent(this.config.repoOwner + '/' + this.config.repoName)}/merge_requests`;
 | 
			
		||||
          requestBody = {
 | 
			
		||||
            title,
 | 
			
		||||
            description: body,
 | 
			
		||||
            source_branch: branchName,
 | 
			
		||||
            target_branch: 'main'
 | 
			
		||||
          };
 | 
			
		||||
          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(`PR creation failed (${response.status}): ${errorText}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const prData = await response.json();
 | 
			
		||||
      
 | 
			
		||||
      // Extract PR URL based on provider
 | 
			
		||||
      let prUrl: string;
 | 
			
		||||
      switch (this.config.provider) {
 | 
			
		||||
        case 'gitea':
 | 
			
		||||
        case 'github':
 | 
			
		||||
          prUrl = prData.html_url || prData.url;
 | 
			
		||||
          break;
 | 
			
		||||
        case 'gitlab':
 | 
			
		||||
          prUrl = prData.web_url;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          throw new Error('Unknown provider response format');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return prUrl;
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw new Error(`Failed to create pull request: ${error instanceof Error ? error.message : 'Unknown error'}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * CRITICAL FIX: Preserve YAML formatting while updating tools
 | 
			
		||||
   * This prevents the complete rewrite that destroys multiline descriptions
 | 
			
		||||
   */
 | 
			
		||||
  private async preserveYamlFormat(toolsPath: string, newTool: any, isEdit: boolean): Promise<string> {
 | 
			
		||||
    const originalContent = await this.readFile(toolsPath);
 | 
			
		||||
    const yamlData: any = load(originalContent);
 | 
			
		||||
 | 
			
		||||
    if (!yamlData.tools || !Array.isArray(yamlData.tools)) {
 | 
			
		||||
      throw new Error('Invalid tools.yaml format');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isEdit) {
 | 
			
		||||
      // Find and replace existing tool
 | 
			
		||||
      const toolIndex = yamlData.tools.findIndex((t: any) => 
 | 
			
		||||
        (t.name || '').toLowerCase() === newTool.name.toLowerCase()
 | 
			
		||||
      );
 | 
			
		||||
      
 | 
			
		||||
      if (toolIndex >= 0) {
 | 
			
		||||
        yamlData.tools[toolIndex] = newTool;
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new Error('Tool to edit not found');
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      // Add new tool - insert alphabetically
 | 
			
		||||
      const insertIndex = yamlData.tools.findIndex((t: any) => 
 | 
			
		||||
        (t.name || '').toLowerCase() > newTool.name.toLowerCase()
 | 
			
		||||
      );
 | 
			
		||||
      
 | 
			
		||||
      if (insertIndex >= 0) {
 | 
			
		||||
        yamlData.tools.splice(insertIndex, 0, newTool);
 | 
			
		||||
      } else {
 | 
			
		||||
        yamlData.tools.push(newTool);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Split original content into sections to preserve formatting
 | 
			
		||||
    const lines = originalContent.split('\n');
 | 
			
		||||
    const toolsStartIndex = lines.findIndex(line => line.trim() === 'tools:');
 | 
			
		||||
    
 | 
			
		||||
    if (toolsStartIndex === -1) {
 | 
			
		||||
      // Fallback to full rewrite if structure is unexpected
 | 
			
		||||
      console.warn('Could not find tools section, falling back to full YAML rewrite');
 | 
			
		||||
      return dump(yamlData, { 
 | 
			
		||||
        lineWidth: -1, 
 | 
			
		||||
        noRefs: true, 
 | 
			
		||||
        quotingType: '"', 
 | 
			
		||||
        forceQuotes: false, 
 | 
			
		||||
        indent: 2
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Preserve header (everything before tools:)
 | 
			
		||||
    const header = lines.slice(0, toolsStartIndex + 1).join('\n');
 | 
			
		||||
    
 | 
			
		||||
    // Find footer (domains, phases, etc.)
 | 
			
		||||
    const domainsStartIndex = lines.findIndex(line => line.trim() === 'domains:');
 | 
			
		||||
    const footer = domainsStartIndex >= 0 ? '\n' + lines.slice(domainsStartIndex).join('\n') : '';
 | 
			
		||||
 | 
			
		||||
    // Generate only the tools section with proper formatting
 | 
			
		||||
    const toolsYaml = yamlData.tools.map((tool: any) => {
 | 
			
		||||
      return this.formatToolYaml(tool);
 | 
			
		||||
    }).join('');
 | 
			
		||||
 | 
			
		||||
    return header + '\n' + toolsYaml + footer;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Format a single tool entry preserving multiline descriptions
 | 
			
		||||
   */
 | 
			
		||||
  private formatToolYaml(tool: any): string {
 | 
			
		||||
    let toolEntry = `  - name: "${tool.name}"\n`;
 | 
			
		||||
    
 | 
			
		||||
    if (tool.icon) toolEntry += `    icon: "${tool.icon}"\n`;
 | 
			
		||||
    toolEntry += `    type: ${tool.type}\n`;
 | 
			
		||||
    
 | 
			
		||||
    // PRESERVE multiline description format for longer descriptions
 | 
			
		||||
    if (tool.description && tool.description.length > 80) {
 | 
			
		||||
      toolEntry += `    description: >-\n`;
 | 
			
		||||
      const words: string[] = tool.description.split(' ');
 | 
			
		||||
      const lines: string[] = [];
 | 
			
		||||
      let currentLine: string = '';
 | 
			
		||||
      
 | 
			
		||||
      words.forEach((word: string) => {
 | 
			
		||||
        if ((currentLine + ' ' + word).length > 80) {
 | 
			
		||||
          if (currentLine) lines.push(currentLine);
 | 
			
		||||
          currentLine = word;
 | 
			
		||||
        } else {
 | 
			
		||||
          currentLine = currentLine ? currentLine + ' ' + word : word;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      if (currentLine) lines.push(currentLine);
 | 
			
		||||
      
 | 
			
		||||
      lines.forEach((line: string) => {
 | 
			
		||||
        toolEntry += `      ${line}\n`;
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      toolEntry += `    description: "${tool.description}"\n`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add array fields
 | 
			
		||||
    if (tool.domains && tool.domains.length > 0) {
 | 
			
		||||
      toolEntry += `    domains:\n`;
 | 
			
		||||
      tool.domains.forEach((domain: string) => {
 | 
			
		||||
        toolEntry += `      - ${domain}\n`;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (tool.phases && tool.phases.length > 0) {
 | 
			
		||||
      toolEntry += `    phases:\n`;
 | 
			
		||||
      tool.phases.forEach((phase: string) => {
 | 
			
		||||
        toolEntry += `      - ${phase}\n`;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (tool.platforms && tool.platforms.length > 0) {
 | 
			
		||||
      toolEntry += `    platforms:\n`;
 | 
			
		||||
      tool.platforms.forEach((platform: string) => {
 | 
			
		||||
        toolEntry += `      - ${platform}\n`;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (tool.related_concepts && tool.related_concepts.length > 0) {
 | 
			
		||||
      toolEntry += `    related_concepts:\n`;
 | 
			
		||||
      tool.related_concepts.forEach((concept: string) => {
 | 
			
		||||
        toolEntry += `      - ${concept}\n`;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (tool['domain-agnostic-software'] && tool['domain-agnostic-software'].length > 0) {
 | 
			
		||||
      toolEntry += `    domain-agnostic-software:\n`;
 | 
			
		||||
      tool['domain-agnostic-software'].forEach((item: string) => {
 | 
			
		||||
        toolEntry += `      - ${item}\n`;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add scalar fields
 | 
			
		||||
    toolEntry += `    skillLevel: ${tool.skillLevel}\n`;
 | 
			
		||||
    if (tool.accessType) toolEntry += `    accessType: ${tool.accessType}\n`;
 | 
			
		||||
    toolEntry += `    url: ${tool.url}\n`;
 | 
			
		||||
    if (tool.projectUrl) toolEntry += `    projectUrl: ${tool.projectUrl}\n`;
 | 
			
		||||
    if (tool.license) toolEntry += `    license: ${tool.license}\n`;
 | 
			
		||||
    if (tool.knowledgebase) toolEntry += `    knowledgebase: ${tool.knowledgebase}\n`;
 | 
			
		||||
 | 
			
		||||
    if (tool.tags && tool.tags.length > 0) {
 | 
			
		||||
      toolEntry += `    tags:\n`;
 | 
			
		||||
      tool.tags.forEach((tag: string) => {
 | 
			
		||||
        toolEntry += `      - ${tag}\n`;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return toolEntry;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private generateToolYAML(tool: any): string {
 | 
			
		||||
    // Clean up the tool object - remove null/undefined values
 | 
			
		||||
  private generateYAML(tool: any): string {
 | 
			
		||||
    // Clean tool object
 | 
			
		||||
    const cleanTool: any = {
 | 
			
		||||
      name: tool.name,
 | 
			
		||||
      type: tool.type,
 | 
			
		||||
@ -451,125 +98,116 @@ export class GitContributionManager {
 | 
			
		||||
      url: tool.url
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Add optional fields only if they have values
 | 
			
		||||
    // Add optional fields
 | 
			
		||||
    if (tool.icon) cleanTool.icon = tool.icon;
 | 
			
		||||
    if (tool.platforms && tool.platforms.length > 0) cleanTool.platforms = tool.platforms;
 | 
			
		||||
    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 && tool.related_concepts.length > 0) cleanTool.related_concepts = tool.related_concepts;
 | 
			
		||||
    if (tool.tags && tool.tags.length > 0) cleanTool.tags = tool.tags;
 | 
			
		||||
    if (tool.related_concepts?.length) cleanTool.related_concepts = tool.related_concepts;
 | 
			
		||||
    if (tool.tags?.length) cleanTool.tags = tool.tags;
 | 
			
		||||
    if (tool['domain-agnostic-software']?.length) {
 | 
			
		||||
      cleanTool['domain-agnostic-software'] = tool['domain-agnostic-software'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Generate clean YAML
 | 
			
		||||
    return dump(cleanTool, {
 | 
			
		||||
      lineWidth: -1,
 | 
			
		||||
      noRefs: true,
 | 
			
		||||
      quotingType: '"',
 | 
			
		||||
      forceQuotes: false,
 | 
			
		||||
      indent: 2
 | 
			
		||||
    });
 | 
			
		||||
    }).trim();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async submitContribution(data: ContributionData): Promise<GitOperationResult> {
 | 
			
		||||
    const branchName = `tool-${data.type}-${Date.now()}`;
 | 
			
		||||
  private async createIssue(data: ContributionData, toolYaml: string): Promise<string> {
 | 
			
		||||
    const title = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}`;
 | 
			
		||||
    const body = this.generateIssueBody(data, toolYaml);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await this.createBranch(branchName);
 | 
			
		||||
    let apiUrl: string;
 | 
			
		||||
    let requestBody: any;
 | 
			
		||||
 | 
			
		||||
      const toolsPath = 'src/data/tools.yaml';
 | 
			
		||||
    switch (this.config.provider) {
 | 
			
		||||
      case 'gitea':
 | 
			
		||||
        apiUrl = `${this.config.apiEndpoint}/repos/${this.config.repoOwner}/${this.config.repoName}/issues`;
 | 
			
		||||
        requestBody = { title, body };
 | 
			
		||||
        break;
 | 
			
		||||
      
 | 
			
		||||
      // CRITICAL FIX: Use format-preserving method instead of dump()
 | 
			
		||||
      const newYaml = await this.preserveYamlFormat(toolsPath, data.tool, data.type === 'edit');
 | 
			
		||||
      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}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      await this.writeFile(toolsPath, newYaml);
 | 
			
		||||
    const response = await fetch(apiUrl, {
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      headers: {
 | 
			
		||||
        'Authorization': `Bearer ${this.config.apiToken}`,
 | 
			
		||||
        'Content-Type': 'application/json'
 | 
			
		||||
      },
 | 
			
		||||
      body: JSON.stringify(requestBody)
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
      const commitMessage = `${data.type === 'add' ? 'Add' : 'Update'} tool: ${data.tool.name}
 | 
			
		||||
    if (!response.ok) {
 | 
			
		||||
      const errorText = await response.text();
 | 
			
		||||
      throw new Error(`HTTP ${response.status}: ${errorText}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
Submitted by: ${data.metadata.submitter}
 | 
			
		||||
${data.metadata.reason ? `Reason: ${data.metadata.reason}` : ''}`;
 | 
			
		||||
 | 
			
		||||
      await this.commitChanges(commitMessage);
 | 
			
		||||
 | 
			
		||||
      await this.pushBranch(branchName);
 | 
			
		||||
 | 
			
		||||
      // Generate tool YAML for PR description
 | 
			
		||||
      const toolYaml = this.generateToolYAML(data.tool);
 | 
			
		||||
 | 
			
		||||
      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;
 | 
			
		||||
    const issueData = await response.json();
 | 
			
		||||
    
 | 
			
		||||
    // Extract issue URL
 | 
			
		||||
    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 generateEnhancedPRDescription(data: ContributionData, toolYaml: string): string {
 | 
			
		||||
    return `## Tool ${data.type === 'add' ? 'Addition' : 'Update'}: ${data.tool.name}
 | 
			
		||||
  private generateIssueBody(data: ContributionData, toolYaml: string): string {
 | 
			
		||||
    return `## ${data.type === 'add' ? 'Add' : 'Update'} Tool: ${data.tool.name}
 | 
			
		||||
 | 
			
		||||
**Type:** ${data.tool.type}  
 | 
			
		||||
**Submitted by:** ${data.metadata.submitter}  
 | 
			
		||||
**Action:** ${data.type === 'add' ? 'Add new tool' : 'Update existing tool'}
 | 
			
		||||
**Type:** ${data.tool.type}  
 | 
			
		||||
**Action:** ${data.type}
 | 
			
		||||
 | 
			
		||||
### Tool Details
 | 
			
		||||
### Tool Information
 | 
			
		||||
- **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.platforms?.length ? `- **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.tool.domains?.length ? `- **Domains:** ${data.tool.domains.join(', ')}` : ''}
 | 
			
		||||
${data.tool.phases?.length ? `- **Phases:** ${data.tool.phases.join(', ')}` : ''}
 | 
			
		||||
 | 
			
		||||
${data.metadata.reason ? `### Reason for Contribution
 | 
			
		||||
${data.metadata.reason ? `### Reason
 | 
			
		||||
${data.metadata.reason}
 | 
			
		||||
 | 
			
		||||
` : ''}### Raw Tool Data (Copy & Paste Ready)
 | 
			
		||||
` : ''}### Copy-Paste YAML
 | 
			
		||||
 | 
			
		||||
\`\`\`yaml
 | 
			
		||||
${toolYaml}\`\`\`
 | 
			
		||||
  - ${toolYaml.split('\n').join('\n    ')}
 | 
			
		||||
\`\`\`
 | 
			
		||||
 | 
			
		||||
### 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
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
*This contribution was submitted via the CC24-Hub web interface and contains only the raw tool data for manual integration.*`;
 | 
			
		||||
*Submitted via CC24-Hub contribution form*`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user