Browse Source

pass announcement

Nostr-Signature: 57e1440848e4b322a9b10a6dff49973f29c8dd20b85f6cc75fd40d32eb04f0e4 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 3866152051a42592e83a1850bf9f3fd49af597f7dcdb523ef39374d528f6c46df6118682cac3202c29ce89a90fec8b4284c68a57101c6c590d8d1a184cac9731
main
Silberengel 3 weeks ago
parent
commit
77f191fc51
  1. 1
      nostr/commit-signatures.jsonl
  2. 87
      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 @@
{"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":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":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":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"}

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

@ -1422,10 +1422,11 @@ export class FileManager {
async saveRepoEventToWorktree( async saveRepoEventToWorktree(
worktreePath: string, worktreePath: string,
event: NostrEvent, event: NostrEvent,
eventType: 'announcement' | 'transfer' eventType: 'announcement' | 'transfer',
): Promise<void> { skipIfExists: boolean = true
): Promise<boolean> {
try { try {
const { mkdir, writeFile } = await import('fs/promises'); const { mkdir, writeFile, readFile } = await import('fs/promises');
const { join } = await import('path'); const { join } = await import('path');
// Create nostr directory in worktree // Create nostr directory in worktree
@ -1434,15 +1435,39 @@ export class FileManager {
// Append to repo-events.jsonl with event type metadata // Append to repo-events.jsonl with event type metadata
const jsonlFile = join(nostrDir, 'repo-events.jsonl'); 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({ const eventLine = JSON.stringify({
type: eventType, type: eventType,
timestamp: event.created_at, timestamp: event.created_at,
event event
}) + '\n'; }) + '\n';
await writeFile(jsonlFile, eventLine, { flag: 'a', encoding: 'utf-8' }); await writeFile(jsonlFile, eventLine, { flag: 'a', encoding: 'utf-8' });
return true;
} catch (err) { } catch (err) {
logger.debug({ error: err, worktreePath, eventType }, 'Failed to save repo event to nostr/repo-events.jsonl'); 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 // Don't throw - this is a nice-to-have feature
return false;
} }
} }
@ -1765,7 +1790,8 @@ export class FileManager {
npub: string, npub: string,
repoName: string, repoName: string,
branchName: string, branchName: string,
fromBranch?: string fromBranch?: string,
announcement?: NostrEvent
): Promise<void> { ): Promise<void> {
// Security: Validate branch names to prevent path traversal // Security: Validate branch names to prevent path traversal
if (!isValidBranchName(branchName)) { if (!isValidBranchName(branchName)) {
@ -1796,8 +1822,12 @@ export class FileManager {
// If no branches exist, create an orphan branch (branch with no parent) // If no branches exist, create an orphan branch (branch with no parent)
if (!hasBranches) { 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'; let commitMessage = 'Initial commit';
let announcementEvent: NostrEvent | null = announcement || null;
// If announcement not provided, try to fetch it
if (!announcementEvent) {
try { try {
const { NostrClient } = await import('../nostr/nostr-client.js'); const { NostrClient } = await import('../nostr/nostr-client.js');
const { DEFAULT_NOSTR_RELAYS } = await import('../../config.js'); const { DEFAULT_NOSTR_RELAYS } = await import('../../config.js');
@ -1808,18 +1838,19 @@ export class FileManager {
const repoOwnerPubkey = requireNpubHex(npub); const repoOwnerPubkey = requireNpubHex(npub);
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS);
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, repoOwnerPubkey, eventCache); const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, repoOwnerPubkey, eventCache);
const announcement = findRepoAnnouncement(allEvents, repoName); announcementEvent = 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');
}
} catch (announcementErr) { } catch (announcementErr) {
logger.debug({ error: announcementErr, branchName }, 'Failed to fetch announcement, using default commit message'); 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) // Create worktree for the new branch directly (orphan branch)
const worktreeRoot = join(this.repoRoot, npub, `${repoName}.worktrees`); const worktreeRoot = join(this.repoRoot, npub, `${repoName}.worktrees`);
@ -1852,16 +1883,40 @@ export class FileManager {
// Note: --orphan must come before branch name, path comes last // Note: --orphan must come before branch name, path comes last
await git.raw(['worktree', 'add', '--orphan', branchName, worktreePath]); 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); const workGit: SimpleGit = simpleGit(worktreePath);
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'], { await workGit.commit(commitMessage, ['--allow-empty'], {
'--author': 'GitRepublic <noreply@gitrepublic.com>' '--author': 'GitRepublic <noreply@gitrepublic.com>'
}); });
}
// Set the default branch to the new branch in the bare repo // Set the default branch to the new branch in the bare repo
await git.raw(['symbolic-ref', 'HEAD', `refs/heads/${branchName}`]); 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 // Clean up worktree
await this.removeWorktree(repoPath, worktreePath); await this.removeWorktree(repoPath, worktreePath);

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

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

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

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

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

@ -91,35 +91,12 @@ export const load: PageLoad = async ({ params, url, parent }) => {
// The frontend will need to check access via API and show appropriate error // 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 // 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 name = announcement.tags.find((t: string[]) => t[0] === 'name')?.[1] || repo;
const description = announcement.tags.find((t: string[]) => t[0] === 'description')?.[1] || ''; const description = announcement.tags.find((t: string[]) => t[0] === 'description')?.[1] || '';
const image = announcement.tags.find((t: string[]) => t[0] === 'image')?.[1]; const image = announcement.tags.find((t: string[]) => t[0] === 'image')?.[1];
const banner = announcement.tags.find((t: string[]) => t[0] === 'banner')?.[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 // Get git domain for constructing URLs
const layoutData = await parent(); const layoutData = await parent();
const gitDomain = (layoutData as { gitDomain?: string }).gitDomain || url.host || 'localhost:6543'; const gitDomain = (layoutData as { gitDomain?: string }).gitDomain || url.host || 'localhost:6543';
@ -131,16 +108,8 @@ export const load: PageLoad = async ({ params, url, parent }) => {
description: description || `Repository: ${name}`, description: description || `Repository: ${name}`,
image: image || banner || undefined, image: image || banner || undefined,
banner: banner || image || undefined, banner: banner || image || undefined,
repoName: name,
repoDescription: description,
repoUrl, repoUrl,
repoCloneUrls: cloneUrls, announcement: announcement, // Return full announcement - component can extract what it needs
repoMaintainers: maintainers,
repoOwnerPubkey: ownerPubkey,
repoLanguage: language,
repoTopics: topics,
repoWebsite: website,
repoIsPrivate: isPrivate,
ogType: 'website' ogType: 'website'
}; };
} catch (error) { } catch (error) {

9
vite.config.ts

@ -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('try_get_request_store') && message.includes('never used') ||
message.includes('is imported from external module') && message.includes('but 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('[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({
// Suppress warnings about externalized modules (expected for SSR builds) // Suppress warnings about externalized modules (expected for SSR builds)
if ( if (
warning.code === 'MODULE_LEVEL_DIRECTIVE' || warning.code === 'MODULE_LEVEL_DIRECTIVE' ||
warning.code === 'CIRCULAR_DEPENDENCY' ||
(typeof warning.message === 'string' && ( (typeof warning.message === 'string' && (
warning.message.includes('externalized for browser compatibility') || warning.message.includes('externalized for browser compatibility') ||
warning.message.includes('try_get_request_store') && warning.message.includes('never used') || 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; return;

Loading…
Cancel
Save