mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
4097fc6506
Open the skills market on a bundled catalog of 398 curated ClawHub and SkillHub skills in 13 categories instead of querying both registries live. Live search stays available as an explicit "search all markets" scope whose results are marked as not curated. - Server: catalog scope (default) with category filter, filtering before pagination and batched install state; catalog metadata overlaid on live detail; per-scanner ClawHub reports, changelog and page URL; manual catalog refresh script. - Pin ClawHub reads and installs to the card's owner so a same-slug copy by another author is never shown or installed in its place. - Desktop: category chips, curated cards with tags, locale-aware summaries, detail page with stats, security report, changelog, capability panel and triggers; install confirmation requires acknowledgement for unaudited or flagged skills. - Docs: rewrite the skills market section and refresh screenshots.
98 lines
3.9 KiB
TypeScript
98 lines
3.9 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Regenerate the Skills Market curated catalog snapshot.
|
|
*
|
|
* bun run scripts/market-catalog-refresh.ts # rewrite skills.json
|
|
* bun run scripts/market-catalog-refresh.ts --check # report only, write nothing
|
|
*
|
|
* Reads `src/server/services/market/catalog/curation.json` (the editorial
|
|
* source) and re-reads what upstream owns for every entry. Upstream bases
|
|
* follow `HAHA_MARKET_BASE_CLAWHUB` / `HAHA_MARKET_BASE_SKILLHUB`.
|
|
*
|
|
* This talks to the live registries, so it is a maintainer tool only: no test
|
|
* or CI lane runs it, and it refuses to run when `CI` is set.
|
|
*/
|
|
|
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import {
|
|
buildCatalogSnapshot,
|
|
clawhubLatestVersion,
|
|
curationEntriesToRead,
|
|
readClawhubEntry,
|
|
readSkillhubEntry,
|
|
type CatalogRead,
|
|
type CurationEntry,
|
|
type CurationFile,
|
|
} from '../src/server/services/market/catalog/catalogSnapshot.js'
|
|
import { getProviderBase } from '../src/server/services/market/providerFetch.js'
|
|
|
|
if (process.env.CI) {
|
|
console.log('market-catalog-refresh: skipped (CI is set; this script reads live registries)')
|
|
process.exit(0)
|
|
}
|
|
|
|
const CATALOG_DIR = join(import.meta.dir, '..', 'src', 'server', 'services', 'market', 'catalog')
|
|
const CURATION = join(CATALOG_DIR, 'curation.json')
|
|
const SNAPSHOT = join(CATALOG_DIR, 'skills.json')
|
|
const CONCURRENCY = 6
|
|
const checkOnly = process.argv.includes('--check')
|
|
|
|
type JsonResult = Record<string, any>
|
|
|
|
async function getJson(url: string): Promise<JsonResult> {
|
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
try {
|
|
const response = await fetch(url, { headers: { 'user-agent': 'cc-haha-market-catalog' } })
|
|
if (response.status === 429 || response.status >= 500) throw new Error(`HTTP ${response.status}`)
|
|
if (!response.ok) return { _status: response.status }
|
|
return (await response.json()) as JsonResult
|
|
} catch (error) {
|
|
if (attempt === 3) return { _error: String(error) }
|
|
await new Promise((resolve) => setTimeout(resolve, 1500 * (attempt + 1)))
|
|
}
|
|
}
|
|
return {}
|
|
}
|
|
|
|
async function readClawhub(entry: CurationEntry): Promise<CatalogRead> {
|
|
const base = getProviderBase('clawhub')
|
|
const owner = `?owner=${encodeURIComponent(entry.owner)}`
|
|
const detail = await getJson(`${base}/api/v1/skills/${encodeURIComponent(entry.slug)}${owner}`)
|
|
const version = clawhubLatestVersion(detail)
|
|
const versionDetail = detail.skill && version
|
|
? await getJson(`${base}/api/v1/skills/${encodeURIComponent(entry.slug)}/versions/${encodeURIComponent(version)}${owner}`)
|
|
: {}
|
|
return readClawhubEntry(entry, detail, versionDetail)
|
|
}
|
|
|
|
async function readSkillhub(entry: CurationEntry): Promise<CatalogRead> {
|
|
const base = getProviderBase('skillhub')
|
|
return readSkillhubEntry(entry, await getJson(`${base}/api/v1/skills/${encodeURIComponent(entry.slug)}`))
|
|
}
|
|
|
|
async function pool<T, R>(items: T[], worker: (item: T) => Promise<R>): Promise<R[]> {
|
|
const results = new Array<R>(items.length)
|
|
let next = 0
|
|
await Promise.all(Array.from({ length: CONCURRENCY }, async () => {
|
|
while (next < items.length) {
|
|
const index = next++
|
|
results[index] = await worker(items[index]!)
|
|
}
|
|
}))
|
|
return results
|
|
}
|
|
|
|
const curation = JSON.parse(await readFile(CURATION, 'utf8')) as CurationFile
|
|
const toRead = curationEntriesToRead(curation)
|
|
const results = await pool(toRead, (entry) => entry.source === 'clawhub' ? readClawhub(entry) : readSkillhub(entry))
|
|
const reads = new Map<string, CatalogRead>(toRead.map((entry, index) => [`${entry.source}:${entry.slug}`, results[index]!]))
|
|
|
|
const { snapshot, problems } = buildCatalogSnapshot(curation, reads, Date.now())
|
|
console.log(`catalog: ${snapshot.skills.length}/${curation.skills.length} entries kept`)
|
|
for (const problem of problems) console.log(` dropped ${problem}`)
|
|
if (!checkOnly) {
|
|
await writeFile(SNAPSHOT, `${JSON.stringify(snapshot, null, 1)}\n`)
|
|
console.log(`wrote ${SNAPSHOT}`)
|
|
}
|