Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions COPY.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ These exact strings. Do not paraphrase them per page.
| Nav: setup guides | All setup guides | guidance only: the link under the Developers menu cards. Links `/developers` |
| Skillgen lead | Keep your package skill current. After each release tag, Skillgen opens a pull request that updates the Skill in your repository. You review it and decide what merges. | guidance only: the `/skillgen` line under the H1. Its first two sentences are the page's meta description. A "Make a skill" link above the H1 returns to `/make-skill` |
| Skillgen opt-in | Skillgen opens a pull request after each release tag. It runs only on the repositories you turn on here. | guidance only: the intro of the Skillgen view on `/me?view=skillgen`. The `/skillgen` step 02 says the same in its own words: Skillgen never runs on a repository you did not turn on |
| Public repository settings | Your public repository settings | heading for `OwnedSkillsPrompt`. Describe the setting without claiming a scan added repositories. |
| Public repository settings: privacy notice | When indexing is on, skilld checks your public GitHub repositories for SKILL.md files. Each Skill it finds gets a public page. You can turn indexing off here. | explanation of public indexing, including when the saved setting is off. |
| Public repository settings: loading | Loading your repository settings. | status while the saved account setting loads. |
| Public repository settings: load error | Could not load your repository settings. Try again. | alert with a Retry action. Hide scan and privacy actions until the setting loads. |
| Public repository settings: save error | Could not turn off indexing. Indexing is still on. Try again. | alert after a failed privacy save. Preserve the saved setting and allow retry. |
| Skillgen groups | Ready Β· Needs a change Β· Turn on all (n) Β· n of n on | guidance only: the two groups on `/me?view=skillgen`. Ready rows say "Updates <package> on each new tag." when on and "Can update <package> when you turn it on." when off. Needs a change rows show the refusal sentence from `skillgenRefusal` |
| CLI H1 | The skilld CLI | guidance only: the `/cli` H1 and its OG title |
| CLI line | Search, run, install, and keep Skills current. | guidance only: the `/cli` line under the H1. It is the CLI's own `--help` line, so change it in the CLI first |
Expand Down Expand Up @@ -201,6 +206,7 @@ Use `7-day social mentions Β· {date}` as the footer. Format the UTC date as `2 O
| Star import date | Last complete GitHub import: {date} | A bounded partial import has no completion date. |
| Star import partial | Checked the first 1,000 GitHub stars. Some starred Repositories were not checked. | The page limit leaves coverage partial. |
| Account without likes, with watches | Your watched Repositories are ready Β· {n} watched {Repository/Repositories}. Like a Skill to keep it here. Β· Browse skills Β· View watched Repositories | Show existing account value when the liked Skills view is empty. |
| Stop watching a Repository | Stop watching {owner}/{repo} | Remove the Repository watch. Email preferences stay separate. |

## Register by context

Expand Down
46 changes: 46 additions & 0 deletions app/components/GitHubAvatar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script setup lang="ts">
import { githubAvatarProxyUrl } from '#shared/image-proxy'

const { owner, size = 32 } = defineProps<{ owner: string, size?: 28 | 32 | 36 }>()
const emit = defineEmits<{ avatarError: [] }>()
const avatar = useTemplateRef<HTMLImageElement>('avatar')
const failed = ref(false)
const initials = computed(() => owner.slice(0, 2).toUpperCase())
const src = computed(() => githubAvatarProxyUrl(owner, size * 2))

function fail() {
failed.value = true
emit('avatarError')
}

watch(() => owner, () => {
failed.value = false
})
onMounted(() => {
const image = avatar.value
if (image?.complete && image.naturalWidth === 0)
fail()
})
</script>

<template>
<span
class="inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-default bg-muted font-mono text-xs text-muted"
:style="{ width: `${size}px`, height: `${size}px` }"
aria-hidden="true"
>
<span v-if="failed">{{ initials }}</span>
<img
v-else
ref="avatar"
:src
alt=""
:width="size"
:height="size"
class="size-full object-cover"
loading="lazy"
decoding="async"
@error="fail"
>
</span>
</template>
45 changes: 5 additions & 40 deletions app/components/skill-card/_SkillCardIdentity.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,35 +23,18 @@ const emit = defineEmits<{
avatarError: []
}>()

const avatar = useTemplateRef<HTMLImageElement>('avatar')

// An avatar can fail before this card hydrates, such as in a row that
// hydrates only once it scrolls into view. Its error event then fired with no
// listener, so the mount reads the image instead.
onMounted(() => {
const image = avatar.value
if (image?.complete && image.naturalWidth === 0)
emit('avatarError')
})

/** The author names the owner, so the byline needs only the Repository after it. */
const where = computed(() => view.author ? view.repo : view.source)
</script>

<template>
<div class="skill-id" :class="[`skill-id--${size}`, wrap && 'skill-id--wrap']">
<img
<GitHubAvatar
v-if="view.byline === 'full'"
ref="avatar"
:src="view.avatar(size)"
alt=""
:width="size"
:height="size"
class="skill-id__avatar"
loading="lazy"
decoding="async"
@error="emit('avatarError')"
>
:owner="view.owner"
:size
@avatar-error="emit('avatarError')"
/>
<div class="skill-id__text">
<p class="skill-id__name-line">
<NuxtLink :to="view.href" :aria-label="view.label" class="skill-id__name">
Expand Down Expand Up @@ -87,28 +70,10 @@ const where = computed(() => view.author ? view.repo : view.source)
gap: 0.75rem;
}

.skill-id__avatar {
flex: none;
inline-size: var(--skill-id-size);
block-size: var(--skill-id-size);
border: 1px solid var(--ui-border);
border-radius: 9999px;
background: var(--ui-bg-muted);
}

.skill-id--28 {
--skill-id-size: 1.75rem;
gap: 0.625rem;
}

.skill-id--32 {
--skill-id-size: 2rem;
}

.skill-id--36 {
--skill-id-size: 2.25rem;
}

.skill-id__text {
min-inline-size: 0;
flex: 1;
Expand Down
2 changes: 0 additions & 2 deletions app/types/skill-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ export interface SkillCardView {
/** `owner/repo`. */
source: string
byline: SkillCardByline
/** Square GitHub avatar URL at twice the CSS size. */
avatar: (cssSize: number) => string
description: string | null
/** The metric the context asked for, so a row can drop its metric column when it is `none`. */
metricKind: SkillCardMetric
Expand Down
2 changes: 0 additions & 2 deletions app/utils/skill-card-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type {
SkillCardSkill,
SkillCardView,
} from '~/types/skill-card'
import { githubAvatarProxyUrl } from '#shared/image-proxy'
import { skillRunCmd } from '#shared/skill-commands'
import { formatGithubStars } from './github-stars'
import { resolveAuthorName } from './skill-byline'
Expand Down Expand Up @@ -99,7 +98,6 @@ export function buildSkillCardView(skill: SkillCardSkill, options: SkillCardOpti
author,
source: `${skill.owner}/${skill.repo}`,
byline: options.byline,
avatar: cssSize => githubAvatarProxyUrl(skill.owner, cssSize * 2),
description: (options.description ?? defaultSkillCardDescription(options.layout)) ? (skill.description?.trim() || null) : null,
metricKind: options.metric,
metric: metricView(skill, options.metric),
Expand Down
1 change: 1 addition & 0 deletions layers/identity/app/components/OnboardingSteps.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const steps = [
<li
v-for="(s, i) in steps"
:key="s.n"
:aria-current="s.n === step ? 'step' : undefined"
class="flex items-center gap-2"
>
<span
Expand Down
44 changes: 33 additions & 11 deletions layers/identity/app/components/OwnedSkillsPrompt.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
<script setup lang="ts">
import type { OwnedRepoScanNotice } from '../utils/owned-repo-scan'
import { identityAccountQueries } from '../queries/account'
import { identityAccountQueries, identityAccountQueryOptions } from '../queries/account'
import { ownedRepoScanFailureNotice, ownedRepoScanNotice } from '../utils/owned-repo-scan'

/**
* Tells the signed-in account that its public repositories are indexed, and
* offers the switch that stops it.
*
* Sign-in already ran the scan, because SKILL.md files in a public repository
* are already public and indexing them is how the registry grows. So this is a
* notice with a way out, not a question.
* Show the saved public-repository setting, not an assumed scan outcome.
*/
const { login } = defineProps<{ login: string }>()

Expand All @@ -18,9 +13,12 @@ type PromptState
| { _tag: 'scanning' }
| { _tag: 'finished', notice: OwnedRepoScanNotice }
| { _tag: 'turned_off' }
| { _tag: 'turn_off_failed' }

const state = ref<PromptState>({ _tag: 'indexing' })
const rpc = useNuxtRpc()
const { data: me, status: accountStatus, error: accountError, refresh: retryAccount } = await useNuxtRpcQuery(identityAccountQueries.me(), identityAccountQueryOptions)
const indexingOff = computed(() => state.value._tag === 'turned_off' || me.value?.repo_indexing === false)

const scanMutation = useNuxtMutation({
mutation: () => rpc.execute(identityAccountQueries.scanOwnedRepos()),
Expand All @@ -31,6 +29,8 @@ const privacyMutation = useNuxtMutation({
})

async function checkNow() {
if (!me.value || accountError.value || indexingOff.value || state.value._tag === 'scanning' || privacyMutation.pending.value)
return
state.value = { _tag: 'scanning' }
const outcome = await scanMutation.mutateSafe()
state.value = {
Expand All @@ -40,9 +40,17 @@ async function checkNow() {
}

async function turnOff() {
if (!me.value || accountError.value || indexingOff.value || state.value._tag === 'scanning' || privacyMutation.pending.value)
return
const outcome = await privacyMutation.mutateSafe()
if (outcome._tag === 'ok')
if (outcome._tag === 'ok') {
state.value = { _tag: 'turned_off' }
// A confirmed save updates the shared static account cache for later mounts.
setQueryData(serializeNuxtRpcKey(identityAccountQueries.me().key), { ...me.value, repo_indexing: false })
}
else {
state.value = { _tag: 'turn_off_failed' }
}
}

const route = useRoute()
Expand All @@ -56,18 +64,30 @@ const signInPath = computed(() => loginUrl({ returnTo: route.fullPath }))
aria-labelledby="owned-skills-prompt-heading"
>
<h2 id="owned-skills-prompt-heading" class="text-base font-semibold text-pretty">
Your public repositories are in skilld.dev
Your public repository settings
</h2>
<p class="mt-2 text-sm leading-relaxed text-muted text-pretty">
skilld checks your public GitHub repositories for SKILL.md files. Each Skill it finds gets a public page. If you would rather it did not, turn it off here.
When indexing is on, skilld checks your public GitHub repositories for SKILL.md files. Each Skill it finds gets a public page. You can turn indexing off here.
</p>

<p v-if="state._tag === 'turned_off'" class="mt-4 flex items-start gap-2 text-sm text-muted" role="status" aria-live="polite">
<p v-if="accountStatus === 'pending' || accountStatus === 'idle'" class="mt-4 text-sm text-muted" role="status">
Loading your repository settings.
</p>
<div v-else-if="accountError || !me" class="mt-4">
<p role="alert" class="text-sm text-muted">
Could not load your repository settings. Try again.
</p>
<UButton label="Retry" color="neutral" variant="outline" class="mt-3 min-h-11" @click="retryAccount()" />
</div>
<p v-else-if="indexingOff" class="mt-4 flex items-start gap-2 text-sm text-muted" role="status" aria-live="polite">
<UIcon name="i-lucide-circle-check" class="mt-0.5 size-4 shrink-0" aria-hidden="true" />
Indexing is off. Skills already added stay until you remove the SKILL.md file, or ask us to.
</p>

<template v-else>
<p v-if="state._tag === 'turn_off_failed'" class="mt-4 text-sm text-muted" role="alert">
Could not turn off indexing. Indexing is still on. Try again.
</p>
<div v-if="state._tag === 'finished'" class="mt-4" role="status" aria-live="polite">
<p
class="flex items-start gap-2 text-sm"
Expand Down Expand Up @@ -114,6 +134,7 @@ const signInPath = computed(() => loginUrl({ returnTo: route.fullPath }))
variant="outline"
class="min-h-11"
:loading="state._tag === 'scanning'"
:disabled="privacyMutation.pending.value"
@click="checkNow"
/>
<UButton
Expand All @@ -123,6 +144,7 @@ const signInPath = computed(() => loginUrl({ returnTo: route.fullPath }))
variant="ghost"
class="min-h-11"
:loading="privacyMutation.pending.value"
:disabled="state._tag === 'scanning'"
@click="turnOff"
/>
<span v-if="state._tag === 'scanning'" class="text-xs text-muted" role="status" aria-live="polite">
Expand Down
12 changes: 7 additions & 5 deletions layers/identity/app/pages/me/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ async function deleteAccount() {
<p class="data-label">
Email
</p>
<p class="mt-2 break-all font-mono text-sm">
<p class="mt-2 wrap-anywhere font-mono text-sm">
{{ me?.digest_email || me?.email || 'No email set' }}
</p>
<UButton
Expand Down Expand Up @@ -594,10 +594,11 @@ async function deleteAccount() {
<li
v-for="source in subs.items"
:key="`${source.owner}/${source.repo}`"
class="flex min-w-0 items-center gap-2 border-b border-default py-3 last:border-b-0"
class="flex min-w-0 items-center gap-3 border-b border-default py-3 last:border-b-0"
>
<GitHubAvatar :owner="source.owner" :size="32" />
<div class="min-w-0 flex-1">
<NuxtLink :to="repoHubPath(source.owner, source.repo)" class="flex min-h-11 items-center truncate font-mono text-sm hover:text-primary">
<NuxtLink :to="repoHubPath(source.owner, source.repo)" class="flex min-h-11 items-center wrap-anywhere font-mono text-sm font-semibold text-highlighted hover:text-primary">
{{ source.owner }}/{{ source.repo }}
</NuxtLink>
<p class="mt-1 text-sm text-muted">
Expand All @@ -608,9 +609,10 @@ async function deleteAccount() {
v-if="source.source !== 'like'"
color="neutral"
variant="ghost"
icon="i-lucide-x"
icon="i-lucide-bell-off"
class="min-h-11 min-w-11 shrink-0"
:aria-label="`Remove ${source.owner}/${source.repo} from your digest`"
:aria-label="`Stop watching ${source.owner}/${source.repo}`"
:title="`Stop watching ${source.owner}/${source.repo}`"
:disabled="removeSubscriptionMutation.pending.value"
:loading="removeSubscriptionMutation.pending.value && unwatchingRepository === `${source.owner}/${source.repo}`"
@click="unwatch(source.owner, source.repo)"
Expand Down
30 changes: 16 additions & 14 deletions layers/identity/app/pages/onboarding/discover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,6 @@ useSeoMeta({ title: 'Discover skills', robots: 'noindex' })
<p class="mt-2 text-sm text-muted">
We check up to 1,000 starred GitHub Repositories against the registry. Choose which to watch. Email updates are optional.
</p>
<OwnedSkillsPrompt
v-if="user?.login"
:login="user.login"
class="mt-6"
/>

<div class="mt-4 flex flex-wrap items-center gap-3">
<UButton
:loading="syncing"
Expand All @@ -140,20 +134,22 @@ useSeoMeta({ title: 'Discover skills', robots: 'noindex' })
:label="data?.syncedAt ? 'Import stars again' : 'Import stars'"
@click="syncStars"
/>
<span v-if="syncing && syncProgress" class="text-xs text-muted">
Page {{ syncProgress.page || '…' }} Β· {{ syncProgress.total }} Repositories checked Β· {{ syncProgress.matched }} match the registry
</span>
<span v-else-if="data?.syncedAt" class="text-xs text-muted">
Last GitHub import
<NuxtTime :datetime="data.syncedAt * 1000" locale="en-AU" time-zone="UTC" date-style="medium" time-style="short" /> UTC
<span class="text-xs text-muted" role="status" aria-live="polite" aria-atomic="true">
<template v-if="syncing && syncProgress">
Page {{ syncProgress.page || '…' }} Β· {{ syncProgress.total }} Repositories checked Β· {{ syncProgress.matched }} match the registry
</template>
<template v-else-if="data?.syncedAt">
Last GitHub import
<NuxtTime :datetime="data.syncedAt * 1000" locale="en-AU" time-zone="UTC" date-style="medium" time-style="short" /> UTC
</template>
</span>
</div>

<p v-if="data?.partial" class="mt-3 text-sm text-muted" role="status">
Checked the first 1,000 GitHub stars. Some starred Repositories were not checked.
</p>

<div v-if="syncError" class="mt-3 rounded border border-error/40 bg-error/5 p-2 text-xs text-error">
<div v-if="syncError" class="mt-3 rounded border border-error/40 bg-error/5 p-2 text-xs text-error" role="alert">
{{ syncError }}
</div>

Expand Down Expand Up @@ -212,9 +208,10 @@ useSeoMeta({ title: 'Discover skills', robots: 'noindex' })
class="mt-1 size-4 accent-primary"
@change="toggle(`${r.owner}/${r.repo}`)"
>
<GitHubAvatar :owner="r.owner" :size="32" class="mt-0.5" />
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="font-mono text-sm">{{ r.owner }}/{{ r.repo }}</span>
<span class="min-w-0 wrap-anywhere font-mono text-sm font-semibold text-highlighted">{{ r.owner }}/{{ r.repo }}</span>
<span v-if="r.watching" class="rounded bg-elevated px-1.5 py-0.5 text-xs text-muted">
Already watching
</span>
Expand Down Expand Up @@ -249,5 +246,10 @@ useSeoMeta({ title: 'Discover skills', robots: 'noindex' })
@click="watchSelected"
/>
</div>
<OwnedSkillsPrompt
v-if="user?.login"
:login="user.login"
class="mt-8"
/>
</section>
</template>
Loading
Loading