This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

web/components: tailor the empty repo state to ssh key presence

the four-step 'generate a key, add it to settings, set your remote, push' walk
through is only useful to an owner without a key on their account. owners who
already have one now get the quick-setup commands instead, in the shape github
and codeberg use: create-and-push, or push an existing repository.

key presence comes from sh.tangled.publicKey.countKeys, fetched lazily and only
for the owner; a failed lookup falls back to the key-generation instructions
since those are the safe ones to show. the new hasSshKey prop overrides the
lookup so both variants are storyable without a backend.

Signed-off-by: eti <eti@eti.tf>

author
eti
committer
dawn
date (Jul 30, 2026, 7:45 PM +0300) commit bd3fb911 parent 33139281 change-id skpwwlku
+106 -23
+6 -1
web/src/lib/components/repo/EmptyRepo.stories.svelte
··· 26 26 27 27 <Story name="Owner instructions" asChild> 28 28 <StoryAuthProvider initial={{ did: ownerDid, handle: "dawn" }}> 29 - <EmptyRepo repo={ownerRepo} /> 29 + <EmptyRepo repo={ownerRepo} hasSshKey={false} /> 30 + </StoryAuthProvider> 31 + </Story> 32 + <Story name="Owner with SSH key" asChild> 33 + <StoryAuthProvider initial={{ did: ownerDid, handle: "dawn" }}> 34 + <EmptyRepo repo={ownerRepo} hasSshKey={true} /> 30 35 </StoryAuthProvider> 31 36 </Story> 32 37 <Story name="Visitor" asChild>
+100 -22
web/src/lib/components/repo/EmptyRepo.svelte
··· 1 1 <script lang="ts"> 2 2 import { resolve } from "$app/paths"; 3 3 import { getAuth } from "$lib/auth.svelte"; 4 + import { createBobbinClient } from "$lib/api/client"; 5 + import { count } from "$lib/api/count"; 4 6 import type { RepoInfo } from "./types"; 5 7 6 8 interface Props { 7 9 repo: RepoInfo; 10 + /** 11 + * Overrides the SSH key lookup, for deterministic stories/tests. 12 + * Left unset in real usage, in which case it's figured out from the API. 13 + */ 14 + hasSshKey?: boolean; 8 15 } 9 16 10 - let { repo }: Props = $props(); 17 + let { repo, hasSshKey }: Props = $props(); 11 18 12 19 const auth = getAuth(); 13 20 const isOwner = $derived(auth.currentDid === repo.ownerDid); ··· 18 25 .split(/[:/]/)[0] 19 26 ); 20 27 const remote = $derived(`git@${sshHost}:${repo.repoDid ?? `${repo.ownerHandle}/${repo.name}`}`); 28 + 29 + type KeyStatus = "checking" | "has-key" | "no-key"; 30 + 31 + let keyStatus = $state<KeyStatus>("checking"); 32 + 33 + $effect(() => { 34 + if (hasSshKey !== undefined) { 35 + keyStatus = hasSshKey ? "has-key" : "no-key"; 36 + return; 37 + } 38 + 39 + if (!isOwner) return; 40 + const did = auth.currentDid; 41 + if (!did) return; 42 + 43 + let cancelled = false; 44 + keyStatus = "checking"; 45 + 46 + (async () => { 47 + try { 48 + const ctx = createBobbinClient({ serviceUrl: auth.bobbinUrl }); 49 + const result = await count(ctx, "sh.tangled.publicKey.countKeys", did); 50 + if (!cancelled) keyStatus = result.count > 0 ? "has-key" : "no-key"; 51 + } catch { 52 + // safe fallback: guide the owner through generating a key. 53 + if (!cancelled) keyStatus = "no-key"; 54 + } 55 + })(); 56 + 57 + return () => { 58 + cancelled = true; 59 + }; 60 + }); 21 61 </script> 22 62 23 63 {#snippet bullet(n: number)} ··· 28 68 </span> 29 69 {/snippet} 30 70 71 + {#snippet codeBlock(lines: string[])} 72 + <pre class="overflow-x-auto rounded bg-background-inset p-3 font-mono text-sm">{lines.join( 73 + "\n" 74 + )}</pre> 75 + {/snippet} 76 + 31 77 {#if isOwner} 32 - <div class="flex w-full place-content-center"> 33 - <div class="flex w-fit flex-col gap-4 py-6 text-sm"> 34 - <p>This is an empty repository. To get started:</p> 35 - <p> 36 - {@render bullet(1)}First, generate a new 37 - <a 38 - href="https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key" 39 - rel="noopener" 40 - class="underline">SSH key pair</a 41 - >. 42 - </p> 43 - <p> 44 - {@render bullet(2)}Then add the public key from the 45 - <a href={resolve("/settings/keys")} class="underline">keys page</a> in your settings. 46 - </p> 47 - <p> 48 - {@render bullet(3)}Configure your remote to 49 - <code class="rounded bg-background-inset px-1 font-mono">{remote}</code> 50 - </p> 51 - <p>{@render bullet(4)}Push!</p> 78 + {#if keyStatus === "checking"} 79 + <div class="py-6" aria-hidden="true"></div> 80 + {:else if keyStatus === "has-key"} 81 + <div class="flex w-full place-content-center"> 82 + <div class="flex w-fit flex-col gap-5 py-6 text-sm"> 83 + <p>This is an empty repository.</p> 84 + <div class="flex flex-col gap-2"> 85 + <p class="text-foreground-subtle">…create a new repository on the command line</p> 86 + {@render codeBlock([ 87 + `echo "# ${repo.name}" >> README.md`, 88 + "git init", 89 + "git add README.md", 90 + 'git commit -m "initial commit"', 91 + `git branch -M ${repo.defaultBranch}`, 92 + `git remote add origin ${remote}`, 93 + `git push -u origin ${repo.defaultBranch}` 94 + ])} 95 + </div> 96 + <div class="flex flex-col gap-2"> 97 + <p class="text-foreground-subtle"> 98 + …or push an existing repository from the command line 99 + </p> 100 + {@render codeBlock([ 101 + `git remote add origin ${remote}`, 102 + `git branch -M ${repo.defaultBranch}`, 103 + `git push -u origin ${repo.defaultBranch}` 104 + ])} 105 + </div> 106 + </div> 107 + </div> 108 + {:else} 109 + <div class="flex w-full place-content-center"> 110 + <div class="flex w-fit flex-col gap-4 py-6 text-sm"> 111 + <p>This is an empty repository. To get started:</p> 112 + <p> 113 + {@render bullet(1)}First, generate a new 114 + <a 115 + href="https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key" 116 + rel="noopener" 117 + class="underline">SSH key pair</a 118 + >. 119 + </p> 120 + <p> 121 + {@render bullet(2)}Then add the public key from the 122 + <a href={resolve("/settings/keys")} class="underline">keys page</a> in your settings. 123 + </p> 124 + <p> 125 + {@render bullet(3)}Configure your remote to 126 + <code class="rounded bg-background-inset px-1 font-mono">{remote}</code> 127 + </p> 128 + <p>{@render bullet(4)}Push!</p> 129 + </div> 52 130 </div> 53 - </div> 131 + {/if} 54 132 {:else} 55 133 <p class="py-6 text-center text-foreground-subtle">This is an empty repository.</p> 56 134 {/if}