opencode-plugin-hub 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/gui-experimental.cjs +89 -32
- package/package.json +1 -1
- package/src/gui/catalog.ts +43 -14
- package/src/gui/page.tsx +286 -171
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-plugin-hub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Plugin manager for OpenCode Desktop: browse community plugins, search npm, inspect README and downloads, install and update without editing JSON.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/gui/catalog.ts
CHANGED
|
@@ -45,13 +45,30 @@ interface SnapshotEntry {
|
|
|
45
45
|
tags: string[]
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
interface Source {
|
|
49
|
+
url: string
|
|
50
|
+
/** Extra `Accept` value this source needs. The GitHub contents API only returns the raw file for its raw media type. */
|
|
51
|
+
accept?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
55
|
+
* The same curated list, reachable several ways. `raw.githubusercontent.com` is TLS-reset on some
|
|
56
|
+
* networks (the handshake is dropped with "connection reset by peer") while `api.github.com` stays
|
|
57
|
+
* reachable, so every repository is offered through both: the CDN first — no rate limit, and it is
|
|
58
|
+
* the canonical copy — then the contents API as a fallback. The fork occasionally carries newer
|
|
59
|
+
* entries and only matters when the upstream copies fail.
|
|
51
60
|
*/
|
|
52
|
-
const SOURCES = [
|
|
53
|
-
"https://raw.githubusercontent.com/awesome-opencode/awesome-opencode/main/dist/registry.json",
|
|
54
|
-
|
|
61
|
+
const SOURCES: Source[] = [
|
|
62
|
+
{ url: "https://raw.githubusercontent.com/awesome-opencode/awesome-opencode/main/dist/registry.json" },
|
|
63
|
+
{
|
|
64
|
+
url: "https://api.github.com/repos/awesome-opencode/awesome-opencode/contents/dist/registry.json?ref=main",
|
|
65
|
+
accept: "application/vnd.github.raw+json",
|
|
66
|
+
},
|
|
67
|
+
{ url: "https://raw.githubusercontent.com/Gracdjd/awesome-opencode/main/dist/registry.json" },
|
|
68
|
+
{
|
|
69
|
+
url: "https://api.github.com/repos/Gracdjd/awesome-opencode/contents/dist/registry.json?ref=main",
|
|
70
|
+
accept: "application/vnd.github.raw+json",
|
|
71
|
+
},
|
|
55
72
|
]
|
|
56
73
|
|
|
57
74
|
/** Re-fetch the remote list at most this often; otherwise the cache wins. */
|
|
@@ -133,11 +150,12 @@ function toEntries(raw: RawEntry[], installed: Set<string>): CatalogEntryValue[]
|
|
|
133
150
|
|
|
134
151
|
async function fetchRemote(): Promise<{ entries: RawEntry[]; url: string }> {
|
|
135
152
|
let lastError = ""
|
|
136
|
-
for (const
|
|
153
|
+
for (const source of SOURCES) {
|
|
137
154
|
try {
|
|
138
|
-
const
|
|
155
|
+
const headers: Record<string, string> = { accept: source.accept ?? "application/json" }
|
|
156
|
+
const response = await fetch(source.url, { signal: AbortSignal.timeout(12_000), headers })
|
|
139
157
|
if (!response.ok) {
|
|
140
|
-
lastError = `${new URL(url).host} 返回 ${response.status}`
|
|
158
|
+
lastError = `${new URL(source.url).host} 返回 ${response.status}`
|
|
141
159
|
continue
|
|
142
160
|
}
|
|
143
161
|
const body = (await response.json()) as unknown
|
|
@@ -145,7 +163,7 @@ async function fetchRemote(): Promise<{ entries: RawEntry[]; url: string }> {
|
|
|
145
163
|
lastError = "registry 格式不是数组"
|
|
146
164
|
continue
|
|
147
165
|
}
|
|
148
|
-
return { entries: body as RawEntry[], url }
|
|
166
|
+
return { entries: body as RawEntry[], url: source.url }
|
|
149
167
|
} catch (error) {
|
|
150
168
|
lastError = describe(error)
|
|
151
169
|
}
|
|
@@ -182,6 +200,9 @@ async function writeCache(entries: RawEntry[], url: string): Promise<number> {
|
|
|
182
200
|
return fetchedAt
|
|
183
201
|
}
|
|
184
202
|
|
|
203
|
+
/** The first source's URL, used when a cached payload predates the `url` field. */
|
|
204
|
+
const PRIMARY_URL = SOURCES[0]!.url
|
|
205
|
+
|
|
185
206
|
/**
|
|
186
207
|
* The curated community list. Network first (cached for half a day), then the cache, then the
|
|
187
208
|
* snapshot baked into this bundle — so the page is never empty, even offline.
|
|
@@ -193,7 +214,7 @@ export async function browse(installed: Set<string>, force: boolean): Promise<Ca
|
|
|
193
214
|
return {
|
|
194
215
|
entries: toEntries(cache.entries, installed),
|
|
195
216
|
source: "cache",
|
|
196
|
-
registry: cache.url ??
|
|
217
|
+
registry: cache.url ?? PRIMARY_URL,
|
|
197
218
|
fetchedAt: cache.fetchedAt,
|
|
198
219
|
error: "",
|
|
199
220
|
}
|
|
@@ -213,7 +234,7 @@ export async function browse(installed: Set<string>, force: boolean): Promise<Ca
|
|
|
213
234
|
return {
|
|
214
235
|
entries: toEntries(cache.entries, installed),
|
|
215
236
|
source: "cache",
|
|
216
|
-
registry: cache.url ??
|
|
237
|
+
registry: cache.url ?? PRIMARY_URL,
|
|
217
238
|
fetchedAt: cache.fetchedAt,
|
|
218
239
|
error: describe(error),
|
|
219
240
|
}
|
|
@@ -221,7 +242,7 @@ export async function browse(installed: Set<string>, force: boolean): Promise<Ca
|
|
|
221
242
|
return {
|
|
222
243
|
entries: toEntries(SNAPSHOT.map(toRaw), installed),
|
|
223
244
|
source: "builtin",
|
|
224
|
-
registry:
|
|
245
|
+
registry: PRIMARY_URL,
|
|
225
246
|
fetchedAt: 0,
|
|
226
247
|
error: describe(error),
|
|
227
248
|
}
|
|
@@ -240,6 +261,14 @@ function toRaw(entry: SnapshotEntry): RawEntry {
|
|
|
240
261
|
}
|
|
241
262
|
|
|
242
263
|
function describe(error: unknown): string {
|
|
243
|
-
|
|
244
|
-
|
|
264
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
265
|
+
// `fetch failed` is undici's wrapper for every socket-level failure; on its own it says nothing
|
|
266
|
+
// about what to do. Name the likely causes so the notice is actionable instead of alarming.
|
|
267
|
+
if (/fetch failed|network|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket hang up/i.test(message)) {
|
|
268
|
+
return "无法连接插件目录(网络受限或连接被重置),已改用本地缓存"
|
|
269
|
+
}
|
|
270
|
+
if (/aborted|timeout/i.test(message)) {
|
|
271
|
+
return "连接插件目录超时,已改用本地缓存"
|
|
272
|
+
}
|
|
273
|
+
return message
|
|
245
274
|
}
|