diff --git a/services/githubService.ts b/services/githubService.ts index 42253b4..0642808 100644 --- a/services/githubService.ts +++ b/services/githubService.ts @@ -465,3 +465,132 @@ export const deleteRepositorySecret = async ( throw new Error(data?.message || 'Failed to delete repository secret'); } }; + +// ============ File Operations API ============ + +interface FileContent { + content: string; + sha: string; +} + +/** + * Fetch the content of a file from a repository + */ +export const fetchFileContent = async ( + token: string, + owner: string, + repo: string, + path: string +): Promise => { + const response = await fetch( + `${GITHUB_API_BASE}/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, + { + headers: { + Authorization: `token ${token}`, + Accept: 'application/vnd.github.v3+json', + }, + cache: 'no-cache', + } + ); + + if (!response.ok) { + throw new Error('Failed to fetch file content'); + } + + const data = await response.json(); + + // Decode base64 content + const content = atob(data.content.replace(/\n/g, '')); + + return { + content, + sha: data.sha, + }; +}; + +/** + * Create or update a file in a repository + */ +export const createOrUpdateFile = async ( + token: string, + owner: string, + repo: string, + path: string, + content: string, + message: string, + sha?: string +): Promise => { + const body: any = { + message, + content: btoa(content), // Encode to base64 + }; + + if (sha) { + body.sha = sha; + } + + const response = await fetch( + `${GITHUB_API_BASE}/repos/${owner}/${repo}/contents/${encodeURIComponent(path)}`, + { + method: 'PUT', + headers: { + Authorization: `token ${token}`, + Accept: 'application/vnd.github.v3+json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + } + ); + + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data?.message || 'Failed to create or update file'); + } +}; + +/** + * Copy workflows from friuns/VibeGithub to a target repository + */ +export const copyVibeGithubWorkflows = async ( + token: string, + targetOwner: string, + targetRepo: string +): Promise => { + // Fetch workflow files from friuns/VibeGithub + const sourceWorkflows = await fetchRepoWorkflowFiles(token, 'friuns', 'VibeGithub'); + + // Copy each workflow file + for (const workflow of sourceWorkflows) { + try { + // Fetch the content of the source workflow + const { content } = await fetchFileContent(token, 'friuns', 'VibeGithub', workflow.path); + + // Try to get the current file in target repo to check if it exists + let existingSha: string | undefined; + try { + const existing = await fetchFileContent(token, targetOwner, targetRepo, workflow.path); + existingSha = existing.sha; + } catch (err) { + // File doesn't exist, that's fine + } + + // Create or update the file in target repo + const message = existingSha + ? `Update workflow: ${workflow.name}` + : `Add workflow: ${workflow.name}`; + + await createOrUpdateFile( + token, + targetOwner, + targetRepo, + workflow.path, + content, + message, + existingSha + ); + } catch (err) { + console.error(`Failed to copy workflow ${workflow.name}:`, err); + throw new Error(`Failed to copy workflow ${workflow.name}: ${err.message}`); + } + } +}; diff --git a/views/RepoDetail.tsx b/views/RepoDetail.tsx index 7d7be9e..b0db637 100644 --- a/views/RepoDetail.tsx +++ b/views/RepoDetail.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; import { Repository, Issue, WorkflowFile, RepoSecret } from '../types'; -import { fetchIssues, createIssue, fetchAllWorkflowFiles, fetchRepositorySecrets, setRepositorySecret, deleteRepositorySecret } from '../services/githubService'; +import { fetchIssues, createIssue, fetchAllWorkflowFiles, fetchRepositorySecrets, setRepositorySecret, deleteRepositorySecret, copyVibeGithubWorkflows } from '../services/githubService'; import { Button } from '../components/Button'; import { ToastContainer, useToast } from '../components/Toast'; import { ArrowLeft, Plus, MessageCircle, AlertCircle, CheckCircle2, X, RefreshCw, FileCode, ChevronDown, ChevronUp, Key, Trash2, Eye, EyeOff, Shield } from 'lucide-react'; @@ -49,6 +49,9 @@ export const RepoDetail: React.FC = ({ token, repo, onBack, onI const [newSecretValue, setNewSecretValue] = useState(''); const [showSecretValue, setShowSecretValue] = useState(false); const [savingSecret, setSavingSecret] = useState(false); + + // Copy Workflows State + const [copyingWorkflows, setCopyingWorkflows] = useState(false); const [deletingSecret, setDeletingSecret] = useState(null); const [autoSetOAuthChecked, setAutoSetOAuthChecked] = useState(false); @@ -225,6 +228,20 @@ export const RepoDetail: React.FC = ({ token, repo, onBack, onI } }; + const handleCopyWorkflows = async () => { + setCopyingWorkflows(true); + try { + await copyVibeGithubWorkflows(token, repo.owner.login, repo.name); + // Reload workflow files to include the new ones + await loadWorkflowFiles(); + // Show success message - since we don't have a success toast, we'll just not show error + } catch (err) { + showError('Failed to copy workflows'); + } finally { + setCopyingWorkflows(false); + } + }; + return ( @@ -248,9 +265,12 @@ export const RepoDetail: React.FC = ({ token, repo, onBack, onI - + +