Browse Source

pass announcement

Nostr-Signature: 57e1440848e4b322a9b10a6dff49973f29c8dd20b85f6cc75fd40d32eb04f0e4 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 3866152051a42592e83a1850bf9f3fd49af597f7dcdb523ef39374d528f6c46df6118682cac3202c29ce89a90fec8b4284c68a57101c6c590d8d1a184cac9731
main
Silberengel 3 weeks ago
parent
commit
77f191fc51
  1. 1
      nostr/commit-signatures.jsonl
  2. 115
      src/lib/services/git/file-manager.ts
  3. 4
      src/routes/api/repos/[npub]/[repo]/branches/+server.ts
  4. 141
      src/routes/repos/[npub]/[repo]/+page.svelte
  5. 35
      src/routes/repos/[npub]/[repo]/+page.ts
  6. 9
      vite.config.ts

1
nostr/commit-signatures.jsonl

@ -66,3 +66,4 @@ @@ -66,3 +66,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771849427,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"1d4e6ff4059b064d7cdd465d623a606cfcc5d0565681a34f6384463d40cc8c71","sig":"f5fe3547289e994ff1a3b191607e76d778d318ca4538e70253406867ecef214c1be437dca373f9a461c9cf2ca2978a581b54a9d323baeb2c91851e9cc6ffbfd6"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771850840,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","rearrange repo pages"]],"content":"Signed commit: rearrange repo pages","id":"9f8b68f36189073807510a2dac268b466629ecbc6b8dca66ba809cbf3a36dab5","sig":"911debb546c23038bbf77a57bee089130c7cce3a51f2cfb385c3904ec39bc76b90dc9bef2e8e501824ecff13925523d802b6c916d07fef2718554f4f65e6f4d2"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771923126,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix new branch creation"]],"content":"Signed commit: fix new branch creation","id":"7802c9afbf005e2637282f9d06ac8130fe27bfe3a94cc67c211da51d2e9e8350","sig":"30978d6a71b4935c88ff9cd1412294d850a752977943e1aa65bcfc2290d2f2e8bbce809556849a14f0923da33b12cb53d3339741cdabab3ba949dfbb48e9cc4c"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771923236,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","clean up build warning"]],"content":"Signed commit: clean up build warning","id":"297f43968ae4bcfc8b054037b914a728eaec805770ba0c02e33aab3009c1c046","sig":"91177b6f9c4cd0d69455d5e1c109912588f05c2ddbf287d606a9687ec522ba259ed83750dfbb4b77f20e3cb82a266f251983a14405babc28c0d83eb19bf3da70"}

115
src/lib/services/git/file-manager.ts

@ -1422,10 +1422,11 @@ export class FileManager { @@ -1422,10 +1422,11 @@ export class FileManager {
async saveRepoEventToWorktree(
worktreePath: string,
event: NostrEvent,
eventType: 'announcement' | 'transfer'
): Promise<void> {
eventType: 'announcement' | 'transfer',
skipIfExists: boolean = true
): Promise<boolean> {
try {
const { mkdir, writeFile } = await import('fs/promises');
const { mkdir, writeFile, readFile } = await import('fs/promises');
const { join } = await import('path');
// Create nostr directory in worktree
@ -1434,15 +1435,39 @@ export class FileManager { @@ -1434,15 +1435,39 @@ export class FileManager {
// Append to repo-events.jsonl with event type metadata
const jsonlFile = join(nostrDir, 'repo-events.jsonl');
// Check if event already exists if skipIfExists is true
if (skipIfExists) {
try {
const existingContent = await readFile(jsonlFile, 'utf-8');
const lines = existingContent.trim().split('\n').filter(l => l.trim());
for (const line of lines) {
try {
const parsed = JSON.parse(line);
if (parsed.event && parsed.event.id === event.id) {
logger.debug({ eventId: event.id, worktreePath }, 'Event already exists in nostr/repo-events.jsonl, skipping');
return false;
}
} catch {
// Skip invalid JSON lines
}
}
} catch {
// File doesn't exist yet, that's fine
}
}
const eventLine = JSON.stringify({
type: eventType,
timestamp: event.created_at,
event
}) + '\n';
await writeFile(jsonlFile, eventLine, { flag: 'a', encoding: 'utf-8' });
return true;
} catch (err) {
logger.debug({ error: err, worktreePath, eventType }, 'Failed to save repo event to nostr/repo-events.jsonl');
// Don't throw - this is a nice-to-have feature
return false;
}
}
@ -1765,7 +1790,8 @@ export class FileManager { @@ -1765,7 +1790,8 @@ export class FileManager {
npub: string,
repoName: string,
branchName: string,
fromBranch?: string
fromBranch?: string,
announcement?: NostrEvent
): Promise<void> {
// Security: Validate branch names to prevent path traversal
if (!isValidBranchName(branchName)) {
@ -1796,29 +1822,34 @@ export class FileManager { @@ -1796,29 +1822,34 @@ export class FileManager {
// If no branches exist, create an orphan branch (branch with no parent)
if (!hasBranches) {
// Fetch repo announcement to use as initial commit message
// Use provided announcement or fetch repo announcement to use as initial commit message and save to file
let commitMessage = 'Initial commit';
try {
const { NostrClient } = await import('../nostr/nostr-client.js');
const { DEFAULT_NOSTR_RELAYS } = await import('../../config.js');
const { fetchRepoAnnouncementsWithCache, findRepoAnnouncement } = await import('../../utils/nostr-utils.js');
const { eventCache } = await import('../nostr/event-cache.js');
const { requireNpubHex } = await import('../../utils/npub-utils.js');
const repoOwnerPubkey = requireNpubHex(npub);
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS);
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, repoOwnerPubkey, eventCache);
const announcement = findRepoAnnouncement(allEvents, repoName);
if (announcement) {
// Format announcement as commit message
const name = announcement.tags.find((t: string[]) => t[0] === 'name')?.[1] || repoName;
const description = announcement.tags.find((t: string[]) => t[0] === 'description')?.[1] || '';
commitMessage = `Repository announcement: ${name}${description ? '\n\n' + description : ''}\n\nEvent ID: ${announcement.id}`;
logger.debug({ branchName, announcementId: announcement.id }, 'Using repo announcement as initial commit message');
let announcementEvent: NostrEvent | null = announcement || null;
// If announcement not provided, try to fetch it
if (!announcementEvent) {
try {
const { NostrClient } = await import('../nostr/nostr-client.js');
const { DEFAULT_NOSTR_RELAYS } = await import('../../config.js');
const { fetchRepoAnnouncementsWithCache, findRepoAnnouncement } = await import('../../utils/nostr-utils.js');
const { eventCache } = await import('../nostr/event-cache.js');
const { requireNpubHex } = await import('../../utils/npub-utils.js');
const repoOwnerPubkey = requireNpubHex(npub);
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS);
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, repoOwnerPubkey, eventCache);
announcementEvent = findRepoAnnouncement(allEvents, repoName);
} catch (announcementErr) {
logger.debug({ error: announcementErr, branchName }, 'Failed to fetch announcement, using default commit message');
}
} catch (announcementErr) {
logger.debug({ error: announcementErr, branchName }, 'Failed to fetch announcement, using default commit message');
}
if (announcementEvent) {
// Format announcement as commit message
const name = announcementEvent.tags.find((t: string[]) => t[0] === 'name')?.[1] || repoName;
const description = announcementEvent.tags.find((t: string[]) => t[0] === 'description')?.[1] || '';
commitMessage = `Repository announcement: ${name}${description ? '\n\n' + description : ''}\n\nEvent ID: ${announcementEvent.id}`;
logger.debug({ branchName, announcementId: announcementEvent.id }, 'Using repo announcement as initial commit message');
}
// Create worktree for the new branch directly (orphan branch)
@ -1852,16 +1883,40 @@ export class FileManager { @@ -1852,16 +1883,40 @@ export class FileManager {
// Note: --orphan must come before branch name, path comes last
await git.raw(['worktree', 'add', '--orphan', branchName, worktreePath]);
// Create initial empty commit with announcement as message
// Save announcement to nostr/repo-events.jsonl if we have it
let announcementSaved = false;
if (announcementEvent) {
try {
announcementSaved = await this.saveRepoEventToWorktree(worktreePath, announcementEvent, 'announcement', true);
logger.debug({ branchName, announcementId: announcementEvent.id }, 'Saved announcement to nostr/repo-events.jsonl');
} catch (saveErr) {
logger.warn({ error: saveErr, branchName }, 'Failed to save announcement to nostr/repo-events.jsonl, continuing with empty commit');
}
}
// Stage files if announcement was saved
const workGit: SimpleGit = simpleGit(worktreePath);
await workGit.commit(commitMessage, ['--allow-empty'], {
'--author': 'GitRepublic <noreply@gitrepublic.com>'
});
const filesToAdd: string[] = [];
if (announcementSaved) {
filesToAdd.push('nostr/repo-events.jsonl');
}
// Create initial commit with announcement file (if saved) or empty commit
if (filesToAdd.length > 0) {
await workGit.add(filesToAdd);
await workGit.commit(commitMessage, filesToAdd, {
'--author': 'GitRepublic <noreply@gitrepublic.com>'
});
} else {
await workGit.commit(commitMessage, ['--allow-empty'], {
'--author': 'GitRepublic <noreply@gitrepublic.com>'
});
}
// Set the default branch to the new branch in the bare repo
await git.raw(['symbolic-ref', 'HEAD', `refs/heads/${branchName}`]);
logger.debug({ branchName }, 'Created orphan branch with initial commit');
logger.debug({ branchName, announcementSaved }, 'Created orphan branch with initial commit');
// Clean up worktree
await this.removeWorktree(repoPath, worktreePath);

4
src/routes/api/repos/[npub]/[repo]/branches/+server.ts

@ -161,7 +161,7 @@ export const GET: RequestHandler = createRepoGetHandler( @@ -161,7 +161,7 @@ export const GET: RequestHandler = createRepoGetHandler(
export const POST: RequestHandler = createRepoPostHandler(
async (context: RepoRequestContext, event: RequestEvent) => {
const body = await event.request.json();
const { branchName, fromBranch } = body;
const { branchName, fromBranch, announcement } = body;
if (!branchName) {
throw handleValidationError('Missing branchName parameter', { operation: 'createBranch', npub: context.npub, repo: context.repo });
@ -269,7 +269,7 @@ export const POST: RequestHandler = createRepoPostHandler( @@ -269,7 +269,7 @@ export const POST: RequestHandler = createRepoPostHandler(
}
// If repo has no branches, sourceBranch will be undefined/null, which createBranch will handle correctly
await fileManager.createBranch(context.npub, context.repo, branchName, sourceBranch);
await fileManager.createBranch(context.npub, context.repo, branchName, sourceBranch, announcement);
return json({ success: true, message: 'Branch created successfully' });
},
{ operation: 'createBranch', requireRepoExists: false } // Allow creating branches in empty repos

141
src/routes/repos/[npub]/[repo]/+page.svelte

@ -30,22 +30,37 @@ @@ -30,22 +30,37 @@
description?: string;
image?: string;
banner?: string;
repoName?: string;
repoDescription?: string;
repoUrl?: string;
repoCloneUrls?: string[];
repoMaintainers?: string[];
repoOwnerPubkey?: string;
repoLanguage?: string;
repoTopics?: string[];
repoWebsite?: string;
repoIsPrivate?: boolean;
announcement?: NostrEvent;
gitDomain?: string;
});
const npub = ($page.params as { npub?: string; repo?: string }).npub || '';
const repo = ($page.params as { npub?: string; repo?: string }).repo || '';
// Extract fields from announcement for convenience
const repoAnnouncement = $derived(pageData.announcement);
const repoName = $derived(repoAnnouncement?.tags.find((t: string[]) => t[0] === 'name')?.[1] || repo);
const repoDescription = $derived(repoAnnouncement?.tags.find((t: string[]) => t[0] === 'description')?.[1] || '');
const repoCloneUrls = $derived(repoAnnouncement?.tags
.filter((t: string[]) => t[0] === 'clone')
.flatMap((t: string[]) => t.slice(1))
.filter((url: string) => url && typeof url === 'string') as string[] || []);
const repoMaintainers = $derived(repoAnnouncement?.tags
.filter((t: string[]) => t[0] === 'maintainers')
.flatMap((t: string[]) => t.slice(1))
.filter((m: string) => m && typeof m === 'string') as string[] || []);
const repoOwnerPubkeyDerived = $derived(repoAnnouncement?.pubkey || '');
const repoLanguage = $derived(repoAnnouncement?.tags.find((t: string[]) => t[0] === 'language')?.[1]);
const repoTopics = $derived(repoAnnouncement?.tags
.filter((t: string[]) => t[0] === 't' && t[1] !== 'private')
.map((t: string[]) => t[1])
.filter((t: string) => t && typeof t === 'string') as string[] || []);
const repoWebsite = $derived(repoAnnouncement?.tags.find((t: string[]) => t[0] === 'website')?.[1]);
const repoIsPrivate = $derived(repoAnnouncement?.tags.some((t: string[]) =>
(t[0] === 'private' && t[1] === 'true') || (t[0] === 't' && t[1] === 'private')
) || false);
let loading = $state(true);
let error = $state<string | null>(null);
let repoNotFound = $state(false); // Track if repository doesn't exist
@ -91,7 +106,7 @@ @@ -91,7 +106,7 @@
// 1. We have page data
// 2. Effect hasn't run yet for this repo
// 3. We're not currently loading
if ((data.repoOwnerPubkey || (data.repoMaintainers && data.repoMaintainers.length > 0)) &&
if ((repoOwnerPubkeyDerived || (repoMaintainers && repoMaintainers.length > 0)) &&
!maintainersEffectRan &&
!loadingMaintainers) {
maintainersEffectRan = true; // Mark as ran to prevent re-running
@ -813,8 +828,8 @@ @@ -813,8 +828,8 @@
let repoImage = $state<string | null>(null);
let repoBanner = $state<string | null>(null);
// Repository owner pubkey (decoded from npub)
let repoOwnerPubkey = $state<string | null>(null);
// Repository owner pubkey (decoded from npub) - kept for backward compatibility with some functions
let repoOwnerPubkeyState = $state<string | null>(null);
// Mobile view toggle for file list/file viewer
let showFileListOnMobile = $state(true);
@ -836,7 +851,7 @@ @@ -836,7 +851,7 @@
// Load clone URL reachability status
async function loadCloneUrlReachability(forceRefresh: boolean = false) {
if (!pageData.repoCloneUrls || pageData.repoCloneUrls.length === 0) {
if (!repoCloneUrls || repoCloneUrls.length === 0) {
return;
}
@ -1422,7 +1437,7 @@ @@ -1422,7 +1437,7 @@
// If repo is not cloned, check if API fallback is available
if (!wasCloned) {
// Try to detect API fallback by checking if we have clone URLs
if (pageData.repoCloneUrls && pageData.repoCloneUrls.length > 0) {
if (repoCloneUrls && repoCloneUrls.length > 0) {
// We have clone URLs, so API fallback might work - will be detected when loadBranches() runs
apiFallbackAvailable = null; // Will be set to true if a subsequent request succeeds
} else {
@ -1569,7 +1584,7 @@ @@ -1569,7 +1584,7 @@
const events = await client.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
authors: [repoOwnerPubkey],
authors: [repoOwnerPubkeyDerived],
'#d': [repo],
limit: 1
}
@ -1702,7 +1717,7 @@ @@ -1702,7 +1717,7 @@
const events = await client.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
authors: [repoOwnerPubkey],
authors: [repoOwnerPubkeyDerived],
'#d': [repo],
limit: 1
}
@ -1804,7 +1819,7 @@ @@ -1804,7 +1819,7 @@
const events = await client.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
authors: [repoOwnerPubkey],
authors: [repoOwnerPubkeyDerived],
'#d': [repo],
limit: 1
}
@ -1946,7 +1961,7 @@ @@ -1946,7 +1961,7 @@
try {
// Check if repo is private and user has access
const data = $page.data as typeof pageData;
if (data.repoIsPrivate) {
if (repoIsPrivate) {
// Check access via API
const accessResponse = await fetch(`/api/repos/${npub}/${repo}/access`, {
headers: buildApiHeaders()
@ -1974,7 +1989,7 @@ @@ -1974,7 +1989,7 @@
const announcementEvents = await client.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
authors: [repoOwnerPubkey],
authors: [repoOwnerPubkeyDerived],
'#d': [repo],
limit: 1
}
@ -2095,7 +2110,7 @@ @@ -2095,7 +2110,7 @@
if (!repoImage && !repoBanner) {
const data = $page.data as typeof pageData;
// Check access for private repos
if (data.repoIsPrivate) {
if (repoIsPrivate) {
const headers: Record<string, string> = {};
if (userPubkey) {
try {
@ -2129,7 +2144,7 @@ @@ -2129,7 +2144,7 @@
const events = await client.fetchEvents([
{
kinds: [30617], // REPO_ANNOUNCEMENT
authors: [repoOwnerPubkey],
authors: [repoOwnerPubkeyDerived],
'#d': [repo],
limit: 1
}
@ -2187,8 +2202,8 @@ @@ -2187,8 +2202,8 @@
try {
const decoded = nip19.decode(npub);
if (decoded.type === 'npub') {
repoOwnerPubkey = decoded.data as string;
repoAddress = `${KIND.REPO_ANNOUNCEMENT}:${repoOwnerPubkey}:${repo}`;
repoOwnerPubkeyState = decoded.data as string;
repoAddress = `${KIND.REPO_ANNOUNCEMENT}:${repoOwnerPubkeyState}:${repo}`;
}
} catch (err) {
console.warn('Failed to decode npub for bookmark address:', err);
@ -2436,7 +2451,7 @@ @@ -2436,7 +2451,7 @@
}
async function copyEventId() {
if (!repoAddress || !repoOwnerPubkey) {
if (!repoAddress || !repoOwnerPubkeyDerived) {
alert('Repository address not available');
return;
}
@ -2565,11 +2580,11 @@ @@ -2565,11 +2580,11 @@
console.error('Failed to load maintainers:', err);
maintainersLoaded = false; // Reset flag on error
// Fallback to pageData if available
if (pageData.repoOwnerPubkey) {
allMaintainers = [{ pubkey: pageData.repoOwnerPubkey, isOwner: true }];
if (pageData.repoMaintainers) {
for (const maintainer of pageData.repoMaintainers) {
if (maintainer.toLowerCase() !== pageData.repoOwnerPubkey.toLowerCase()) {
if (repoOwnerPubkeyDerived) {
allMaintainers = [{ pubkey: repoOwnerPubkeyDerived, isOwner: true }];
if (repoMaintainers) {
for (const maintainer of repoMaintainers) {
if (maintainer.toLowerCase() !== repoOwnerPubkeyDerived.toLowerCase()) {
allMaintainers.push({ pubkey: maintainer, isOwner: false });
}
}
@ -2605,7 +2620,7 @@ @@ -2605,7 +2620,7 @@
}
async function generateAnnouncementFileForRepo() {
if (!pageData.repoOwnerPubkey || !userPubkeyHex) {
if (!repoOwnerPubkeyDerived || !userPubkeyHex) {
error = 'Unable to generate announcement file: missing repository or user information';
return;
}
@ -2616,7 +2631,7 @@ @@ -2616,7 +2631,7 @@
const events = await nostrClient.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
authors: [pageData.repoOwnerPubkey],
authors: [repoOwnerPubkeyDerived],
'#d': [repo],
limit: 1
}
@ -2654,7 +2669,7 @@ @@ -2654,7 +2669,7 @@
return;
}
if (!pageData.repoOwnerPubkey || userPubkeyHex !== pageData.repoOwnerPubkey) {
if (!repoOwnerPubkeyDerived || userPubkeyHex !== repoOwnerPubkeyDerived) {
alert('Only the repository owner can delete the announcement');
return;
}
@ -2678,7 +2693,7 @@ @@ -2678,7 +2693,7 @@
const events = await nostrClient.fetchEvents([
{
kinds: [KIND.REPO_ANNOUNCEMENT],
authors: [pageData.repoOwnerPubkey],
authors: [repoOwnerPubkeyDerived],
'#d': [repo],
limit: 1
}
@ -2703,7 +2718,7 @@ @@ -2703,7 +2718,7 @@
content: `Requesting deletion of repository announcement for ${repo}`,
tags: [
['e', announcement.id], // Reference to the announcement event
['a', `${KIND.REPO_ANNOUNCEMENT}:${pageData.repoOwnerPubkey}:${repo}`], // Repository address
['a', `${KIND.REPO_ANNOUNCEMENT}:${repoOwnerPubkeyDerived}:${repo}`], // Repository address
['k', KIND.REPO_ANNOUNCEMENT.toString()] // Kind of event being deleted
]
};
@ -2814,7 +2829,7 @@ @@ -2814,7 +2829,7 @@
const errorText = await response.text().catch(() => '');
if (errorText.includes('not cloned locally')) {
// Repository is not cloned - check if API fallback might be available
if (pageData.repoCloneUrls && pageData.repoCloneUrls.length > 0) {
if (repoCloneUrls && repoCloneUrls.length > 0) {
// We have clone URLs, so API fallback might work - mark as unknown for now
// It will be set to true if a subsequent request succeeds
apiFallbackAvailable = null;
@ -2877,7 +2892,7 @@ @@ -2877,7 +2892,7 @@
const errorText = await response.text().catch(() => '');
if (errorText.includes('not cloned locally')) {
// Repository is not cloned - check if API fallback might be available
if (pageData.repoCloneUrls && pageData.repoCloneUrls.length > 0) {
if (repoCloneUrls && repoCloneUrls.length > 0) {
// We have clone URLs, so API fallback might work - mark as unknown for now
// It will be set to true if a subsequent request succeeds
apiFallbackAvailable = null;
@ -3800,13 +3815,17 @@ @@ -3800,13 +3815,17 @@
// Otherwise, use the selected branch or current branch
let fromBranch: string | undefined = newBranchFrom || currentBranch || undefined;
// Only include fromBranch if repo has branches
const requestBody: { branchName: string; fromBranch?: string } = {
// Include announcement if available (for empty repos)
const requestBody: { branchName: string; fromBranch?: string; announcement?: NostrEvent } = {
branchName: newBranchName
};
if (branches.length > 0 && fromBranch) {
requestBody.fromBranch = fromBranch;
}
// Pass announcement if available (especially useful for empty repos)
if (repoAnnouncement) {
requestBody.announcement = repoAnnouncement;
}
const response = await fetch(`/api/repos/${npub}/${repo}/branches`, {
method: 'POST',
@ -4452,8 +4471,8 @@ @@ -4452,8 +4471,8 @@
<!-- OpenGraph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:title" content={pageData.title || `${pageData.repoName || repo} - Repository`} />
<meta property="og:description" content={pageData.description || pageData.repoDescription || `Repository: ${pageData.repoName || repo}`} />
<meta property="og:title" content={pageData.title || `${repoName} - Repository`} />
<meta property="og:description" content={pageData.description || repoDescription || `Repository: ${repoName}`} />
<meta property="og:url" content={pageData.repoUrl || `https://${$page.url.host}${$page.url.pathname}`} />
{#if (pageData.image || repoImage) && String(pageData.image || repoImage).trim()}
<meta property="og:image" content={pageData.image || repoImage} />
@ -4465,8 +4484,8 @@ @@ -4465,8 +4484,8 @@
<!-- Twitter Card -->
<meta name="twitter:card" content={repoBanner || repoImage ? "summary_large_image" : "summary"} />
<meta name="twitter:title" content={pageData.title || `${pageData.repoName || repo} - Repository`} />
<meta name="twitter:description" content={pageData.description || pageData.repoDescription || `Repository: ${pageData.repoName || repo}`} />
<meta name="twitter:title" content={pageData.title || `${repoName} - Repository`} />
<meta name="twitter:description" content={pageData.description || repoDescription || `Repository: ${repoName}`} />
{#if pageData.banner || repoBanner}
<meta name="twitter:image" content={pageData.banner || repoBanner} />
{:else if pageData.image || repoImage}
@ -4486,18 +4505,18 @@ @@ -4486,18 +4505,18 @@
</div>
{/if}
{#if repoOwnerPubkey}
{#if repoOwnerPubkeyDerived}
<RepoHeaderEnhanced
repoName={pageData.repoName || repo}
repoDescription={pageData.repoDescription}
repoName={repoName}
repoDescription={repoDescription}
ownerNpub={npub}
ownerPubkey={repoOwnerPubkey}
ownerPubkey={repoOwnerPubkeyDerived}
isMaintainer={isMaintainer}
isPrivate={pageData.repoIsPrivate || false}
cloneUrls={pageData.repoCloneUrls || []}
isPrivate={repoIsPrivate}
cloneUrls={repoCloneUrls}
branches={branches}
currentBranch={currentBranch}
topics={pageData.repoTopics || []}
topics={repoTopics}
defaultBranch={defaultBranch}
isRepoCloned={isRepoCloned}
copyingCloneUrl={copyingCloneUrl}
@ -4532,8 +4551,8 @@ @@ -4532,8 +4551,8 @@
showCreateBranchDialog = true;
}}
onSettings={() => goto(`/signup?npub=${npub}&repo=${repo}`)}
onGenerateVerification={pageData.repoOwnerPubkey && userPubkeyHex === pageData.repoOwnerPubkey && verificationStatus?.verified !== true ? generateAnnouncementFileForRepo : undefined}
onDeleteAnnouncement={pageData.repoOwnerPubkey && userPubkeyHex === pageData.repoOwnerPubkey ? deleteAnnouncement : undefined}
onGenerateVerification={repoOwnerPubkeyDerived && userPubkeyHex === repoOwnerPubkeyDerived && verificationStatus?.verified !== true ? generateAnnouncementFileForRepo : undefined}
onDeleteAnnouncement={repoOwnerPubkeyDerived && userPubkeyHex === repoOwnerPubkeyDerived ? deleteAnnouncement : undefined}
deletingAnnouncement={deletingAnnouncement}
hasUnlimitedAccess={hasUnlimitedAccess($userStore.userLevel)}
needsClone={needsClone}
@ -4544,26 +4563,26 @@ @@ -4544,26 +4563,26 @@
<!-- Additional repo metadata (website, clone URLs with verification) -->
{#if pageData.repoWebsite || (pageData.repoCloneUrls && pageData.repoCloneUrls.length > 0) || pageData.repoLanguage || (pageData.repoTopics && pageData.repoTopics.length > 0) || forkInfo?.isFork}
{#if repoWebsite || (repoCloneUrls && repoCloneUrls.length > 0) || repoLanguage || (repoTopics && repoTopics.length > 0) || forkInfo?.isFork}
<div class="repo-metadata-section">
{#if pageData.repoWebsite}
{#if repoWebsite}
<div class="repo-website">
<a href={pageData.repoWebsite} target="_blank" rel="noopener noreferrer">
<a href={repoWebsite} target="_blank" rel="noopener noreferrer">
<img src="/icons/external-link.svg" alt="" class="icon-inline" />
{pageData.repoWebsite}
{repoWebsite}
</a>
</div>
{/if}
{#if pageData.repoLanguage}
{#if repoLanguage}
<span class="repo-language">
<img src="/icons/file-text.svg" alt="" class="icon-inline" />
{pageData.repoLanguage}
{repoLanguage}
</span>
{/if}
{#if forkInfo?.isFork && forkInfo.originalRepo}
<span class="fork-badge">Forked from <a href={`/repos/${forkInfo.originalRepo.npub}/${forkInfo.originalRepo.repo}`}>{forkInfo.originalRepo.repo}</a></span>
{/if}
{#if pageData.repoCloneUrls && pageData.repoCloneUrls.length > 0}
{#if repoCloneUrls && repoCloneUrls.length > 0}
<div class="repo-clone-urls">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<button
@ -4600,7 +4619,7 @@ @@ -4600,7 +4619,7 @@
{copyingCloneUrl ? 'Copying...' : 'Copy Clone URL'}
</button>
{/if}
{#each (showAllCloneUrls ? pageData.repoCloneUrls : pageData.repoCloneUrls.slice(0, 3)) as cloneUrl}
{#each (showAllCloneUrls ? repoCloneUrls : repoCloneUrls.slice(0, 3)) as cloneUrl}
{@const cloneVerification = verificationStatus?.cloneVerifications?.find(cv => {
const normalizeUrl = (url: string) => url.replace(/\/$/, '').toLowerCase().replace(/^https?:\/\//, '');
const normalizedCv = normalizeUrl(cv.url);
@ -4670,13 +4689,13 @@ @@ -4670,13 +4689,13 @@
{/if}
</div>
{/each}
{#if pageData.repoCloneUrls.length > 3}
{#if repoCloneUrls.length > 3}
<button
class="clone-more"
onclick={() => showAllCloneUrls = !showAllCloneUrls}
title={showAllCloneUrls ? 'Show fewer' : 'Show all clone URLs'}
>
{showAllCloneUrls ? `-${pageData.repoCloneUrls.length - 3} less` : `+${pageData.repoCloneUrls.length - 3} more`}
{showAllCloneUrls ? `-${repoCloneUrls.length - 3} less` : `+${repoCloneUrls.length - 3} more`}
</button>
{/if}
</div>

35
src/routes/repos/[npub]/[repo]/+page.ts

@ -91,34 +91,11 @@ export const load: PageLoad = async ({ params, url, parent }) => { @@ -91,34 +91,11 @@ export const load: PageLoad = async ({ params, url, parent }) => {
// The frontend will need to check access via API and show appropriate error
// We still expose basic metadata (name) but the API will enforce access
// Extract basic info for title/description (minimal extraction for metadata)
const name = announcement.tags.find((t: string[]) => t[0] === 'name')?.[1] || repo;
const description = announcement.tags.find((t: string[]) => t[0] === 'description')?.[1] || '';
const image = announcement.tags.find((t: string[]) => t[0] === 'image')?.[1];
const banner = announcement.tags.find((t: string[]) => t[0] === 'banner')?.[1];
// Debug: log image and banner tags if found
if (image) console.log('[Page Load] Found image tag:', image);
if (banner) console.log('[Page Load] Found banner tag:', banner);
if (!image && !banner) {
console.log('[Page Load] No image or banner tags found. Available tags:',
announcement.tags.filter((t: string[]) => t[0] === 'image' || t[0] === 'banner').map((t: string[]) => t[0]));
}
const cloneUrls = announcement.tags
.filter((t: string[]) => t[0] === 'clone')
.flatMap((t: string[]) => t.slice(1))
.filter((url: string) => url && typeof url === 'string') as string[];
const maintainers = announcement.tags
.filter((t: string[]) => t[0] === 'maintainers')
.flatMap((t: string[]) => t.slice(1))
.filter((m: string) => m && typeof m === 'string') as string[];
// Owner is the author of the announcement event
const ownerPubkey = announcement.pubkey;
const language = announcement.tags.find((t: string[]) => t[0] === 'language')?.[1];
const topics = announcement.tags
.filter((t: string[]) => t[0] === 't' && t[1] !== 'private')
.map((t: string[]) => t[1])
.filter((t: string) => t && typeof t === 'string') as string[];
const website = announcement.tags.find((t: string[]) => t[0] === 'website')?.[1];
// Get git domain for constructing URLs
const layoutData = await parent();
@ -131,16 +108,8 @@ export const load: PageLoad = async ({ params, url, parent }) => { @@ -131,16 +108,8 @@ export const load: PageLoad = async ({ params, url, parent }) => {
description: description || `Repository: ${name}`,
image: image || banner || undefined,
banner: banner || image || undefined,
repoName: name,
repoDescription: description,
repoUrl,
repoCloneUrls: cloneUrls,
repoMaintainers: maintainers,
repoOwnerPubkey: ownerPubkey,
repoLanguage: language,
repoTopics: topics,
repoWebsite: website,
repoIsPrivate: isPrivate,
announcement: announcement, // Return full announcement - component can extract what it needs
ogType: 'website'
};
} catch (error) {

9
vite.config.ts

@ -16,7 +16,9 @@ if (process.env.NODE_ENV === 'production' || process.argv.includes('build')) { @@ -16,7 +16,9 @@ if (process.env.NODE_ENV === 'production' || process.argv.includes('build')) {
message.includes('try_get_request_store') && message.includes('never used') ||
message.includes('is imported from external module') && message.includes('but never used') ||
(message.includes('[plugin:vite:reporter]') && message.includes('is dynamically imported by') && message.includes('but also statically imported by')) ||
(message.includes('dynamic import will not move module into another chunk'))
(message.includes('dynamic import will not move module into another chunk')) ||
message.includes("The 'this' keyword is equivalent to 'undefined'") ||
message.includes('Circular dependency') && message.includes('@asciidoctor/opal-runtime')
);
};
@ -100,10 +102,13 @@ export default defineConfig({ @@ -100,10 +102,13 @@ export default defineConfig({
// Suppress warnings about externalized modules (expected for SSR builds)
if (
warning.code === 'MODULE_LEVEL_DIRECTIVE' ||
warning.code === 'CIRCULAR_DEPENDENCY' ||
(typeof warning.message === 'string' && (
warning.message.includes('externalized for browser compatibility') ||
warning.message.includes('try_get_request_store') && warning.message.includes('never used') ||
warning.message.includes('is imported from external module') && warning.message.includes('but never used')
warning.message.includes('is imported from external module') && warning.message.includes('but never used') ||
warning.message.includes("The 'this' keyword is equivalent to 'undefined'") ||
(warning.message.includes('Circular dependency') && warning.message.includes('@asciidoctor/opal-runtime'))
))
) {
return;

Loading…
Cancel
Save