diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 4852667..b1bd0e3 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -122,3 +122,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772271656,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"f4a5e0d3e2aa7d0d99803f26008ab68e40551e36362bb6d04acf639c5b78d959","sig":"59da9e59a6fb5648f4c889e0045b571e0d2d66a555100d60dec373455309a640bea89e4bb3a42a0e502aa4d2091e4b698203721e79b346ff30e6b2bcdc5f48b3"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772274086,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"32794e047f06902ad610f918834efb113f41eace26a53a3f0fad083b9d8323dc","sig":"3859f0de3de0f8a742b6fbe7709c5a5625f4d5612a936fd81f38a7e1231ee810b50a69c1ed5d23c8a6670b4cbc9ea3d4bd39d6fa9e6207802f45995689b924a9"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772293551,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","remove polling"]],"content":"Signed commit: remove polling","id":"40f01e84f96661bb7fea13aa63c7da428118061b0a1470a11890d4f9cd6d685b","sig":"dbb6947defac6c7f92a3cf6f72352a94ffe2c4b33e65f8410518a40406c93f1f5a3e13e81f2f04f676d826e6cf03ec802328f5228300f80a8114fa3fd26eaeff"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772296288,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","administer the repos"]],"content":"Signed commit: administer the repos","id":"8825fb9bd01e099c1369f0c9ea1429dedd0a0116d103b4a640752c0a830fbc61","sig":"676f0817f817204ad910a70540399f71743a54453ae209535dcb30356d042b049138d9cfdeec08c4b7da03bb6bb51c71477bbf8d2f58bd4b602b9f69af4b3405"} diff --git a/src/lib/services/git/file-manager.ts b/src/lib/services/git/file-manager.ts index d2c66c6..17c1fe3 100644 --- a/src/lib/services/git/file-manager.ts +++ b/src/lib/services/git/file-manager.ts @@ -445,10 +445,37 @@ export class FileManager { logger.info({ npub, repoName, branchName }, '[FileManager.createBranch] Step 2: Creating empty commit'); // Create an empty commit pointing to the empty tree with author information - // Use --author flag to specify author identity (required when git config is not set) - const authorString = `${authorName} <${authorEmail}>`; - const commitHash = await git.raw(['commit-tree', '-m', `Initial commit on ${branchName}`, '--author', authorString, emptyTreeHash]); - const commit = commitHash.trim(); + // git commit-tree doesn't support --author flag, so we use environment variables + const { spawn } = await import('child_process'); + const commit = await new Promise((resolve, reject) => { + const env = { + ...process.env, + GIT_AUTHOR_NAME: authorName, + GIT_AUTHOR_EMAIL: authorEmail, + GIT_COMMITTER_NAME: authorName, + GIT_COMMITTER_EMAIL: authorEmail + }; + const proc = spawn('git', ['commit-tree', '-m', `Initial commit on ${branchName}`, emptyTreeHash], { + cwd: repoPath, + env + }); + let output = ''; + proc.stdout.on('data', (data) => { output += data.toString(); }); + proc.stderr.on('data', (data) => { + const error = data.toString(); + if (error.trim()) { + logger.warn({ npub, repoName, branchName, error }, '[FileManager.createBranch] commit-tree stderr'); + } + }); + proc.on('close', (code) => { + if (code === 0) { + resolve(output.trim()); + } else { + reject(new Error(`commit-tree failed with code ${code}: ${output || 'no output'}`)); + } + }); + proc.on('error', reject); + }); logger.info({ npub, repoName, branchName, commit }, '[FileManager.createBranch] Step 2 complete: empty commit created'); logger.info({ npub, repoName, branchName, commit }, '[FileManager.createBranch] Step 3: Creating branch ref pointing to empty commit'); diff --git a/src/lib/services/git/file-manager/commit-operations.ts b/src/lib/services/git/file-manager/commit-operations.ts index f095daf..080a61a 100644 --- a/src/lib/services/git/file-manager/commit-operations.ts +++ b/src/lib/services/git/file-manager/commit-operations.ts @@ -85,17 +85,83 @@ export async function getCommitHistory(options: CommitHistoryOptions): Promise l.trim()); + const commits = lines.map(line => { + const [hash, ...rest] = line.split('|'); + const message = rest.slice(0, -3).join('|'); // Message might contain | + const authorName = rest[rest.length - 3]; + const authorEmail = rest[rest.length - 2]; + const date = rest[rest.length - 1]; + return { + hash: hash || '', + message: message || '', + author: `${authorName || 'Unknown'} <${authorEmail || ''}>`, + date: date || new Date().toISOString(), + files: [] // Can't get files from raw log easily + }; + }).filter(c => c.hash); + + logger.operation('Commit history retrieved via raw git', { npub, repoName, count: commits.length }); + return commits; + } + } catch (rawErr) { + logger.error({ error: rawErr, npub, repoName, branch }, 'All methods failed to get commit history'); + throw branchErr; // Throw original error + } + } + } + + // Ensure log.all exists and has data + if (!log || !log.all || log.all.length === 0) { + logger.warn({ npub, repoName, branch, logResult: log }, 'git.log() returned empty results despite commits existing'); + // Try one more time with raw command + try { + const rawLog = await git.raw(['log', '--all', `--max-count=${limit}`, '--format=%H|%s|%an|%ae|%ai', ...(path ? ['--', path] : [])]); + if (rawLog && rawLog.trim()) { + const lines = rawLog.trim().split('\n').filter(l => l.trim()); + const commits = lines.map(line => { + const [hash, ...rest] = line.split('|'); + const message = rest.slice(0, -3).join('|'); + const authorName = rest[rest.length - 3]; + const authorEmail = rest[rest.length - 2]; + const date = rest[rest.length - 1]; + return { + hash: hash || '', + message: message || '', + author: `${authorName || 'Unknown'} <${authorEmail || ''}>`, + date: date || new Date().toISOString(), + files: [] + }; + }).filter(c => c.hash); + + logger.operation('Commit history retrieved via raw git (fallback)', { npub, repoName, count: commits.length }); + return commits; + } + } catch (rawErr) { + logger.error({ error: rawErr, npub, repoName, branch }, 'Raw git command also failed'); } + return []; } const commits = log.all.map(commit => ({ diff --git a/src/lib/services/git/repo-manager.ts b/src/lib/services/git/repo-manager.ts index ca088a6..3b70dbb 100644 --- a/src/lib/services/git/repo-manager.ts +++ b/src/lib/services/git/repo-manager.ts @@ -10,7 +10,7 @@ */ import { existsSync, mkdirSync, accessSync, constants } from 'fs'; -import { join } from 'path'; +import { join, resolve } from 'path'; import { spawn } from 'child_process'; import type { NostrEvent } from '../../types/nostr.js'; import { GIT_DOMAIN } from '../../config.js'; @@ -213,6 +213,11 @@ export class RepoManager { announcementEvent: NostrEvent, preferredDefaultBranch?: string ): Promise { + // Declare variables outside try block so they're accessible in finally + const { FileManager } = await import('./file-manager.js'); + const fileManager = new FileManager(this.repoRoot); + let workDir: string | undefined; + try { // Get default branch from preferred branch, git config, environment, or use 'master' // Check preferred branch first (from user settings), then git's init.defaultBranch config @@ -291,23 +296,57 @@ You can use this read-me file to explain the purpose of this repo to everyone wh Your commits will all be signed by your Nostr keys and saved to the event files in the ./nostr folder. `; - // Use FileManager to create the initial branch and files - const { FileManager } = await import('./file-manager.js'); - const fileManager = new FileManager(this.repoRoot); + // Create both README.md and announcement in a single initial commit + // We'll use a worktree to write both files and commit them together + // If no branches exist, we'll create an orphan branch directly in the worktree + logger.info({ npub, repoName, defaultBranch }, 'Creating worktree for initial commit'); + + const { writeFile: writeFileFs, mkdir: mkdirFs } = await import('fs/promises'); + const { join } = await import('path'); + const { spawn } = await import('child_process'); - // If no branches exist, create an orphan branch - // We already checked for existing branches above, so if existingBranches is empty, create one if (existingBranches.length === 0) { - // Create orphan branch first (pass undefined for fromBranch to create orphan) - await fileManager.createBranch(npub, repoName, defaultBranch, undefined); + // No branches exist - create worktree with orphan branch + // We need to create the worktree manually with --orphan flag + logger.info({ npub, repoName, defaultBranch }, 'No branches exist, creating orphan branch in worktree'); + + // Create a temporary worktree directory + // Use absolute path to ensure git worktree can find it + const worktreeBase = resolve(this.repoRoot, 'worktrees', npub, repoName); + await mkdirFs(worktreeBase, { recursive: true }); + workDir = resolve(worktreeBase, `worktree-${Date.now()}`); + + // Create worktree with orphan branch + // Note: --orphan requires -b flag to specify the branch name + // git worktree add requires an absolute path + await new Promise((resolvePromise, reject) => { + const proc = spawn('git', ['worktree', 'add', '--orphan', '-b', defaultBranch, workDir!], { + cwd: repoPath, + stdio: ['ignore', 'pipe', 'pipe'] + }); + + let stdout = ''; + let stderr = ''; + proc.stdout?.on('data', (data: Buffer) => { stdout += data.toString(); }); + proc.stderr?.on('data', (data: Buffer) => { stderr += data.toString(); }); + + proc.on('close', (code: number | null) => { + if (code === 0) { + resolvePromise(); + } else { + reject(new Error(`git worktree add --orphan failed with code ${code}: ${stderr || stdout}`)); + } + }); + proc.on('error', reject); + }); + + logger.info({ npub, repoName, defaultBranch, workDir }, 'Orphan branch worktree created successfully'); + } else { + // Branch exists - use normal worktree + workDir = await fileManager.getWorktree(repoPath, defaultBranch, npub, repoName); + logger.info({ npub, repoName, defaultBranch, workDir }, 'Worktree created successfully'); } - // Create both README.md and announcement in the initial commit - // We'll use a worktree to write both files and commit them together - const workDir = await fileManager.getWorktree(repoPath, defaultBranch, npub, repoName); - const { writeFile: writeFileFs } = await import('fs/promises'); - const { join } = await import('path'); - // Write README.md const readmePath = join(workDir, 'README.md'); await writeFileFs(readmePath, readmeContent, 'utf-8'); @@ -335,19 +374,35 @@ Your commits will all be signed by your Nostr keys and saved to the event files logger.warn({ repoPath, npub, repoName, error: configError }, 'Failed to set git config, commit may fail'); } - // Commit files together - await workGit.commit('Initial commit', filesToAdd, { + // Commit files together with "Initial commit to GitRepublic" message + // This will be the first and only commit on the branch + await workGit.commit('Initial commit to GitRepublic', filesToAdd, { '--author': `${authorName} <${authorEmail}>` }); - // Clean up worktree - await fileManager.removeWorktree(repoPath, workDir); - logger.info({ npub, repoName, branch: defaultBranch }, 'Created initial branch and README.md'); } catch (err) { - // Log but don't fail - initial README creation is nice-to-have + // This is a critical error - we need the initial branch and commit for the repo to be usable const sanitizedErr = sanitizeError(err); - logger.warn({ error: sanitizedErr, repoPath, npub, repoName }, 'Failed to create initial branch and README, continuing anyway'); + logger.error({ + error: sanitizedErr, + repoPath, + npub, + repoName, + errorMessage: err instanceof Error ? err.message : String(err), + errorStack: err instanceof Error ? err.stack : undefined + }, 'CRITICAL: Failed to create initial branch and README - repository will be empty'); + // Re-throw so caller can handle it appropriately + throw err; + } finally { + // Clean up worktree (always, even on error) + if (workDir) { + try { + await fileManager.removeWorktree(repoPath, workDir); + } catch (cleanupErr) { + logger.warn({ error: cleanupErr, workDir, repoPath }, 'Failed to clean up worktree (non-critical)'); + } + } } } @@ -513,34 +568,70 @@ Your commits will all be signed by your Nostr keys and saved to the event files ): Promise<{ success: boolean; needsAnnouncement?: boolean; announcement?: NostrEvent; error?: string; cloneUrls?: string[]; remoteUrls?: string[] }> { const repoPath = join(this.repoRoot, npub, `${repoName}.git`); - // If repo already exists, check if it has an announcement + // If repo already exists, check if it has commits (not just the directory) if (existsSync(repoPath)) { - const hasAnnouncement = await this.announcementManager.hasAnnouncementInRepoFile(repoPath); - if (hasAnnouncement) { - return { success: true }; - } - - // Repo exists but no announcement - use provided announcement or try to fetch from relays - let announcementToUse: NostrEvent | null | undefined = announcementEvent; - if (!announcementToUse) { - const { requireNpubHex: requireNpubHexUtil } = await import('../../utils/npub-utils.js'); - const repoOwnerPubkey = requireNpubHexUtil(npub); - announcementToUse = await this.announcementManager.fetchAnnouncementFromRelays(repoOwnerPubkey, repoName); - } - - if (announcementToUse) { - // Save announcement to repo asynchronously (non-blocking) - // We have the announcement from relays, so this is just for offline papertrail - this.announcementManager.ensureAnnouncementInRepo(repoPath, announcementToUse) - .catch((err) => { - logger.warn({ error: err, repoPath, eventId: announcementToUse?.id }, - 'Failed to save announcement to repo (non-blocking, announcement available from relays)'); - }); - return { success: true, announcement: announcementToUse }; + try { + const git = simpleGit(repoPath); + // Check if repo has any commits + const commitCountStr = await git.raw(['rev-list', '--count', '--all']).catch(() => '0'); + const commitCount = parseInt(commitCountStr.trim(), 10); + const hasCommits = !isNaN(commitCount) && commitCount > 0; + + if (hasCommits) { + // Repo has commits, check if it has an announcement + const hasAnnouncement = await this.announcementManager.hasAnnouncementInRepoFile(repoPath); + if (hasAnnouncement) { + return { success: true }; + } + + // Repo has commits but no announcement - use provided announcement or try to fetch from relays + let announcementToUse: NostrEvent | null | undefined = announcementEvent; + if (!announcementToUse) { + const { requireNpubHex: requireNpubHexUtil } = await import('../../utils/npub-utils.js'); + const repoOwnerPubkey = requireNpubHexUtil(npub); + announcementToUse = await this.announcementManager.fetchAnnouncementFromRelays(repoOwnerPubkey, repoName); + } + + if (announcementToUse) { + // Save announcement to repo asynchronously (non-blocking) + this.announcementManager.ensureAnnouncementInRepo(repoPath, announcementToUse) + .catch((err) => { + logger.warn({ error: err, repoPath, eventId: announcementToUse?.id }, + 'Failed to save announcement to repo (non-blocking, announcement available from relays)'); + }); + return { success: true, announcement: announcementToUse }; + } + + // Repo has commits but no announcement found - needs announcement + return { success: false, needsAnnouncement: true }; + } else { + // Repo exists but is empty - remove it so we can clone fresh + logger.info({ npub, repoName }, 'Repository exists but is empty, removing to clone fresh'); + try { + const { rmSync } = await import('fs'); + rmSync(repoPath, { recursive: true, force: true }); + logger.info({ npub, repoName }, 'Removed empty repository directory'); + } catch (rmErr) { + logger.warn({ error: rmErr, npub, repoName }, 'Failed to remove empty repository, will try to fetch into it'); + // Continue - might be able to fetch into existing empty repo + } + // Fall through to fetch from remotes below + } + } catch (err) { + // Error checking commits - assume empty and try to fetch + logger.warn({ error: err, npub, repoName }, 'Error checking if repo has commits, will try to fetch from remotes'); + // Try to remove and clone fresh + try { + const { rmSync } = await import('fs'); + rmSync(repoPath, { recursive: true, force: true }); + logger.info({ npub, repoName }, 'Removed repository directory after error checking commits'); + } catch (rmErr) { + logger.warn({ error: rmErr, npub, repoName }, 'Failed to remove repository after error'); + } + // Fall through to fetch from remotes below } - // Repo exists but no announcement found - needs announcement - return { success: false, needsAnnouncement: true }; + // Repo exists but is empty - continue to fetch from remotes below } // If no announcement provided, try to fetch from relays @@ -778,13 +869,92 @@ Your commits will all be signed by your Nostr keys and saved to the event files throw new Error('Repository clone completed but repository path does not exist'); } - // Ensure announcement is saved to nostr/repo-events.jsonl (non-blocking - repo is usable without it) - // Fire and forget - we have the announcement from relays, so this is just for offline papertrail - this.announcementManager.ensureAnnouncementInRepo(repoPath, announcementEvent) - .catch((verifyError) => { - // Announcement file creation is optional - log but don't fail - logger.warn({ error: verifyError, npub, repoName }, 'Failed to ensure announcement in repo, but repository is usable'); - }); + // After cloning, ensure default branch, README, and announcement are committed + try { + const repoGit = simpleGit(repoPath); + + // Check if repo has any commits + let hasCommits = false; + try { + const commitCountStr = await repoGit.raw(['rev-list', '--count', '--all']).catch(() => '0'); + const commitCount = parseInt(commitCountStr.trim(), 10); + hasCommits = !isNaN(commitCount) && commitCount > 0; + } catch { + hasCommits = false; + } + + // Get default branch preference + let defaultBranch = preferredDefaultBranch || process.env.DEFAULT_BRANCH || 'master'; + + // Check existing branches + let existingBranches: string[] = []; + try { + const branches = await repoGit.branch(['-a']); + existingBranches = branches.all + .map(b => b.replace(/^remotes\/origin\//, '').replace(/^remotes\//, '').replace(/^refs\/heads\//, '')) + .filter(b => !b.includes('HEAD')); + existingBranches = [...new Set(existingBranches)]; + + // If we have a preferred branch and it exists, use it + if (preferredDefaultBranch && existingBranches.includes(preferredDefaultBranch)) { + defaultBranch = preferredDefaultBranch; + } else if (existingBranches.length > 0) { + // Prefer existing branches that match common defaults + const preferredBranches = preferredDefaultBranch + ? [preferredDefaultBranch, defaultBranch, 'main', 'master', 'dev'] + : [defaultBranch, 'main', 'master', 'dev']; + for (const preferred of preferredBranches) { + if (existingBranches.includes(preferred)) { + defaultBranch = preferred; + break; + } + } + // If no match, use the first existing branch + if (!existingBranches.includes(defaultBranch)) { + defaultBranch = existingBranches[0]; + } + } + } catch { + // No branches exist yet + } + + // If repo has no commits, create initial branch and commit README + announcement + if (!hasCommits) { + logger.info({ npub, repoName, defaultBranch }, 'Repository has no commits, creating initial branch and commit'); + try { + await this.createInitialBranchAndReadme(repoPath, npub, repoName, announcementEvent, preferredDefaultBranch); + logger.info({ npub, repoName, defaultBranch }, 'Successfully created initial branch and commit'); + } catch (createError) { + logger.error({ + error: createError, + npub, + repoName, + defaultBranch, + errorMessage: createError instanceof Error ? createError.message : String(createError), + errorStack: createError instanceof Error ? createError.stack : undefined + }, 'Failed to create initial branch and commit - this is critical'); + // Re-throw so the outer catch can handle it + throw createError; + } + } else { + // Repo has commits - ensure default branch exists and README/announcement are committed + logger.info({ npub, repoName, defaultBranch, hasCommits }, 'Repository has commits, ensuring default branch and files'); + + // Ensure announcement is committed (blocking - we want it in the repo) + // This will use worktrees to checkout the default branch and commit + await this.announcementManager.ensureAnnouncementInRepo(repoPath, announcementEvent, undefined, preferredDefaultBranch); + + // Ensure README exists and is committed (also uses worktrees) + await this.ensureReadmeExists(repoPath, npub, repoName, announcementEvent, preferredDefaultBranch); + } + } catch (postCloneError) { + // Log but don't fail - repo is cloned and usable + logger.warn({ + error: postCloneError, + npub, + repoName + }, 'Failed to set up default branch/README/announcement after clone, but repository is usable'); + } logger.info({ npub, repoName }, 'Successfully fetched repository on-demand'); return { success: true, announcement: announcementEvent }; diff --git a/src/routes/api/repos/[npub]/[repo]/readme/+server.ts b/src/routes/api/repos/[npub]/[repo]/readme/+server.ts index b8b5c95..c0f007b 100644 --- a/src/routes/api/repos/[npub]/[repo]/readme/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/readme/+server.ts @@ -120,11 +120,16 @@ export const GET: RequestHandler = createRepoGetHandler( return json({ found: false }); } + // Determine content type + const isMarkdown = readmePath?.toLowerCase().endsWith('.md') || readmePath?.toLowerCase().endsWith('.markdown'); + const isAsciiDoc = readmePath?.toLowerCase().endsWith('.adoc') || readmePath?.toLowerCase().endsWith('.asciidoc'); + return json({ found: true, content: readmeContent, path: readmePath, - isMarkdown: readmePath?.toLowerCase().endsWith('.md') || readmePath?.toLowerCase().endsWith('.markdown') + type: isMarkdown ? 'markdown' : (isAsciiDoc ? 'asciidoc' : 'text'), + isMarkdown: isMarkdown // Keep for backward compatibility }); }, { operation: 'getReadme', requireRepoExists: false, requireRepoAccess: false } // README should be publicly accessible diff --git a/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte b/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte index c8ec635..9aabac7 100644 --- a/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte +++ b/src/routes/repos/[npub]/[repo]/components/DocsTab.svelte @@ -88,20 +88,29 @@ // ALWAYS load README FIRST and display immediately if available // README is standard documentation and should always be shown try { - const readmeResponse = await fetch(`/api/repos/${npub}/${repo}/readme?ref=${currentBranch || 'HEAD'}`); + const readmeUrl = `/api/repos/${npub}/${repo}/readme?ref=${currentBranch || 'HEAD'}`; + logger.debug({ npub, repo, branch: currentBranch, url: readmeUrl }, 'Fetching README'); + const readmeResponse = await fetch(readmeUrl); if (readmeResponse.ok) { const readmeData = await readmeResponse.json(); + logger.debug({ npub, repo, readmeData }, 'README API response'); if (readmeData.content) { documentationContent = readmeData.content; documentationKind = readmeData.type || 'markdown'; selectedDoc = 'README.md'; hasReadme = true; loading = false; // Stop showing loading once README is loaded - logger.debug({ npub, repo }, 'README loaded and displayed'); + logger.debug({ npub, repo, contentLength: readmeData.content.length }, 'README loaded and displayed'); + } else if (readmeData.found === false) { + logger.debug({ npub, repo, branch: currentBranch }, 'README not found in repository'); + } else { + logger.warn({ npub, repo, readmeData }, 'README API returned unexpected format'); } + } else { + logger.debug({ npub, repo, status: readmeResponse.status, statusText: readmeResponse.statusText }, 'README API request failed'); } } catch (readmeErr) { - logger.debug({ error: readmeErr, npub, repo }, 'No README found'); + logger.debug({ error: readmeErr, npub, repo, branch: currentBranch }, 'Error fetching README'); } // Now check for docs folder in the background (but don't replace README) diff --git a/src/routes/repos/[npub]/[repo]/services/repo-operations.ts b/src/routes/repos/[npub]/[repo]/services/repo-operations.ts index efee88f..25da9f1 100644 --- a/src/routes/repos/[npub]/[repo]/services/repo-operations.ts +++ b/src/routes/repos/[npub]/[repo]/services/repo-operations.ts @@ -215,7 +215,19 @@ export async function cloneRepository( requestBody.defaultBranch = defaultBranch; } - const data = await apiPost<{ alreadyExists?: boolean }>(`/api/repos/${state.npub}/${state.repo}/clone`, requestBody); + logger.debug({ + npub: state.npub, + repo: state.repo, + hasProofEvent: !!proofEvent, + defaultBranch + }, '[Clone] Sending clone request to server'); + + const cloneUrl = `/api/repos/${state.npub}/${state.repo}/clone`; + logger.debug({ url: cloneUrl }, '[Clone] POST request URL'); + + const data = await apiPost<{ alreadyExists?: boolean }>(cloneUrl, requestBody); + + logger.debug({ data }, '[Clone] Clone request successful'); if (data.alreadyExists) { alert('Repository already exists locally.'); @@ -238,6 +250,12 @@ export async function cloneRepository( } } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to clone repository'; + logger.error({ + error: err, + npub: state.npub, + repo: state.repo, + errorMessage + }, '[Clone] Clone request failed'); alert(`Error: ${errorMessage}`); console.error('Error cloning repository:', err); } finally { diff --git a/src/routes/repos/[npub]/[repo]/utils/safe-wrappers.ts b/src/routes/repos/[npub]/[repo]/utils/safe-wrappers.ts index 7a10954..1ee486e 100644 --- a/src/routes/repos/[npub]/[repo]/utils/safe-wrappers.ts +++ b/src/routes/repos/[npub]/[repo]/utils/safe-wrappers.ts @@ -5,15 +5,30 @@ /** * Safely execute an async function, returning a resolved promise if window is undefined + * + * This function is designed to: + * 1. Prevent SSR errors by checking for window availability + * 2. Catch and log errors without crashing the app + * 3. Return resolved promises even on error to prevent unhandled rejections + * + * Note: Errors are logged but not re-thrown to prevent unhandled promise rejections + * in event handlers. The wrapped functions should handle their own errors (e.g., show alerts). */ export function safeAsync( fn: () => Promise ): Promise { if (typeof window === 'undefined') return Promise.resolve(); try { - return fn(); + return fn().catch((err) => { + // Log async errors but don't re-throw to prevent unhandled rejections + // The wrapped functions should handle their own errors (e.g., show alerts) + console.error('Error in safe async function:', err); + // Return resolved promise to prevent unhandled rejection + return Promise.resolve(); + }); } catch (err) { - console.warn('Error in safe async function:', err); + // Synchronous errors - log and return resolved promise to prevent crashes + console.warn('Synchronous error in safe async function:', err); return Promise.resolve(); } }