Skip to content

Commit 83500ad

Browse files
authored
Merge branch 'main' into feat/valibot-api-validation
2 parents 7b488a3 + be9ae01 commit 83500ad

15 files changed

Lines changed: 406 additions & 93 deletions

File tree

.github/workflows/autofix.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515

1616
steps:
1717
- uses: actions/checkout@v6
18-
- run: npm i -g --force corepack && corepack enable
18+
- run: corepack enable
1919
- uses: actions/setup-node@v6
2020
with:
2121
node-version: lts/*

.github/workflows/ci.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ on:
88
branches:
99
- main
1010

11+
permissions:
12+
contents: read
13+
1114
jobs:
1215
lint:
1316
runs-on: ubuntu-latest
1417

1518
steps:
1619
- uses: actions/checkout@v6
17-
- run: npm i -g --force corepack && corepack enable
20+
- run: corepack enable
1821
- uses: actions/setup-node@v6
1922
with:
2023
node-version: lts/*
@@ -31,7 +34,7 @@ jobs:
3134

3235
steps:
3336
- uses: actions/checkout@v6
34-
- run: npm i -g --force corepack && corepack enable
37+
- run: corepack enable
3538
- uses: actions/setup-node@v6
3639
with:
3740
node-version: lts/*
@@ -59,7 +62,7 @@ jobs:
5962

6063
steps:
6164
- uses: actions/checkout@v6
62-
- run: npm i -g --force corepack && corepack enable
65+
- run: corepack enable
6366
- uses: actions/setup-node@v6
6467
with:
6568
node-version: lts/*

app/components/ConnectorModal.vue

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { isConnected, isConnecting, npmUser, error, hasOperations, connect, disco
66
77
const tokenInput = ref('')
88
const portInput = ref('31415')
9+
const copied = ref(false)
910
1011
async function handleConnect() {
1112
const port = Number.parseInt(portInput.value, 10) || 31415
@@ -20,6 +21,27 @@ function handleDisconnect() {
2021
disconnect()
2122
}
2223
24+
function copyCommand() {
25+
let command = executeNpmxConnectorCommand.value
26+
if (portInput.value !== '31415') {
27+
command += ` --port ${portInput.value}`
28+
}
29+
navigator.clipboard.writeText(command)
30+
copied.value = true
31+
setTimeout(() => {
32+
copied.value = false
33+
}, 2000)
34+
}
35+
36+
const selectedPM = useSelectedPackageManager()
37+
38+
const executeNpmxConnectorCommand = computed(() => {
39+
return getExecuteCommand({
40+
packageName: 'npmx-connector',
41+
packageManager: selectedPM.value,
42+
})
43+
})
44+
2345
// Reset form when modal opens
2446
watch(open, isOpen => {
2547
if (isOpen) {
@@ -103,9 +125,24 @@ watch(open, isOpen => {
103125
Run the connector on your machine to enable admin features:
104126
</p>
105127

106-
<div class="p-3 bg-[#0d0d0d] border border-border rounded-lg font-mono text-sm">
128+
<div
129+
class="flex items-center p-3 bg-[#0d0d0d] border border-border rounded-lg font-mono text-sm"
130+
>
107131
<span class="text-fg-subtle">$</span>
108-
<span class="text-fg ml-2">npx&nbsp;npmx-connector</span>
132+
<span class="text-fg ml-2">{{ executeNpmxConnectorCommand }}</span>
133+
<button
134+
type="button"
135+
:aria-label="copied ? 'Copied' : 'Copy command'"
136+
class="ml-auto text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
137+
@click="copyCommand"
138+
>
139+
<span v-if="!copied" class="i-carbon-copy block w-5 h-5" aria-hidden="true" />
140+
<span
141+
v-else
142+
class="i-carbon-checkmark block w-5 h-5 text-green-500"
143+
aria-hidden="true"
144+
/>
145+
</button>
109146
</div>
110147

111148
<p class="text-sm text-fg-muted">Then paste the token shown in your terminal:</p>

app/components/PackageDownloadStats.vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const config = computed(() => ({
7979
</div>
8080
<div class="w-full overflow-hidden">
8181
<ClientOnly>
82-
<VueUiSparkline class="max-w-full" :dataset :config />
82+
<VueUiSparkline class="w-full max-w-xs" :dataset :config />
8383
<template #fallback>
8484
<!-- Skeleton matching sparkline layout: title row + chart with data label -->
8585
<div class="min-h-[100px]">
@@ -122,8 +122,4 @@ const config = computed(() => ({
122122
Geist Mono,
123123
monospace !important;
124124
}
125-
.vue-ui-sparkline,
126-
.vue-ui-sparkline svg {
127-
max-width: 100% !important;
128-
}
129125
</style>

app/composables/useRepoMeta.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
type ProviderId = 'github' // Could be extended to support other providers (gitlab, codeforge, tangled...)
2+
export type RepoRef = { provider: ProviderId; owner: string; repo: string }
3+
4+
export type RepoMetaLinks = {
5+
repo: string
6+
stars: string
7+
forks: string
8+
watchers?: string
9+
}
10+
11+
export type RepoMeta = {
12+
provider: ProviderId
13+
url: string
14+
stars: number
15+
forks: number
16+
watchers?: number
17+
description?: string | null
18+
defaultBranch?: string
19+
links: RepoMetaLinks
20+
}
21+
22+
type UnghRepoResponse = {
23+
repo: {
24+
description?: string | null
25+
stars?: number
26+
forks?: number
27+
watchers?: number
28+
defaultBranch?: string
29+
} | null
30+
}
31+
32+
function normalizeInputToUrl(input: string): string | null {
33+
const raw = input.trim()
34+
if (!raw) return null
35+
36+
const normalized = raw.replace(/^git\+/, '')
37+
38+
if (!/^https?:\/\//i.test(normalized)) {
39+
const scp = normalized.match(/^(?:git@)?([^:/]+):(.+)$/i)
40+
if (scp?.[1] && scp?.[2]) {
41+
const host = scp[1]
42+
const path = scp[2].replace(/^\/*/, '')
43+
return `https://${host}/${path}`
44+
}
45+
}
46+
47+
return normalized
48+
}
49+
50+
type ProviderAdapter = {
51+
id: ProviderId
52+
parse(url: URL): RepoRef | null
53+
links(ref: RepoRef): RepoMetaLinks
54+
fetchMeta(ref: RepoRef, links: RepoMetaLinks): Promise<RepoMeta | null>
55+
}
56+
57+
const githubAdapter: ProviderAdapter = {
58+
id: 'github',
59+
60+
parse(url) {
61+
const host = url.hostname.toLowerCase()
62+
if (host !== 'github.com' && host !== 'www.github.com') return null
63+
64+
const parts = url.pathname.split('/').filter(Boolean)
65+
if (parts.length < 2) return null
66+
67+
const owner = decodeURIComponent(parts[0] ?? '').trim()
68+
const repo = decodeURIComponent(parts[1] ?? '')
69+
.trim()
70+
.replace(/\.git$/i, '')
71+
72+
if (!owner || !repo) return null
73+
74+
return { provider: 'github', owner, repo }
75+
},
76+
77+
links(ref) {
78+
const base = `https://github.com/${ref.owner}/${ref.repo}`
79+
return {
80+
repo: base,
81+
stars: `${base}/stargazers`,
82+
forks: `${base}/forks`,
83+
watchers: `${base}/watchers`,
84+
}
85+
},
86+
87+
async fetchMeta(ref, links) {
88+
// Using UNGH to avoid API limitations of the Github API
89+
const res = await $fetch<UnghRepoResponse>(`https://ungh.cc/repos/${ref.owner}/${ref.repo}`, {
90+
headers: { 'User-Agent': 'npmx' },
91+
}).catch(() => null)
92+
93+
const repo = res?.repo
94+
if (!repo) return null
95+
96+
return {
97+
provider: 'github',
98+
url: links.repo,
99+
stars: repo.stars ?? 0,
100+
forks: repo.forks ?? 0,
101+
watchers: repo.watchers ?? 0,
102+
description: repo.description ?? null,
103+
defaultBranch: repo.defaultBranch,
104+
links,
105+
}
106+
},
107+
}
108+
109+
const providers: readonly ProviderAdapter[] = [githubAdapter] as const
110+
111+
function parseRepoFromUrl(input: string): RepoRef | null {
112+
const normalized = normalizeInputToUrl(input)
113+
if (!normalized) return null
114+
115+
try {
116+
const url = new URL(normalized)
117+
for (const provider of providers) {
118+
const ref = provider.parse(url)
119+
if (ref) return ref
120+
}
121+
return null
122+
} catch {
123+
return null
124+
}
125+
}
126+
127+
async function fetchRepoMeta(ref: RepoRef): Promise<RepoMeta | null> {
128+
const adapter = providers.find(provider => provider.id === ref.provider)
129+
if (!adapter) return null
130+
131+
const links = adapter.links(ref)
132+
return await adapter.fetchMeta(ref, links)
133+
}
134+
135+
export function useRepoMeta(repositoryUrl: MaybeRefOrGetter<string | null | undefined>) {
136+
const repoRef = computed(() => {
137+
const url = toValue(repositoryUrl)
138+
if (!url) return null
139+
return parseRepoFromUrl(url)
140+
})
141+
142+
const { data, pending, error, refresh } = useLazyAsyncData<RepoMeta | null>(
143+
() =>
144+
repoRef.value
145+
? `repo-meta:${repoRef.value.provider}:${repoRef.value.owner}/${repoRef.value.repo}`
146+
: 'repo-meta:none',
147+
async () => {
148+
const ref = repoRef.value
149+
if (!ref) return null
150+
return await fetchRepoMeta(ref)
151+
},
152+
)
153+
154+
const meta = computed<RepoMeta | null>(() => data.value ?? null)
155+
156+
return {
157+
repoRef,
158+
meta,
159+
160+
stars: computed(() => meta.value?.stars ?? 0),
161+
forks: computed(() => meta.value?.forks ?? 0),
162+
watchers: computed(() => meta.value?.watchers ?? 0),
163+
164+
starsLink: computed(() => meta.value?.links.stars ?? null),
165+
forksLink: computed(() => meta.value?.links.forks ?? null),
166+
watchersLink: computed(() => meta.value?.links.watchers ?? null),
167+
repoLink: computed(() => meta.value?.links.repo ?? null),
168+
169+
pending,
170+
error,
171+
refresh,
172+
}
173+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { useLocalStorage } from '@vueuse/core'
2+
3+
export function useSelectedPackageManager() {
4+
return useLocalStorage<PackageManagerId>('npmx-pm', 'npm')
5+
}

0 commit comments

Comments
 (0)