dsh-skill-hub 0.3.17 → 0.3.19
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/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +4 -2
- package/lib/client.js.map +1 -1
- package/lib/index.js +59 -24
- package/lib/types/client/locales/market.d.ts +1 -0
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/protocol/api.d.ts +0 -7
- package/lib/types/protocol/repo.d.ts +6 -0
- package/lib/types/protocol.d.ts +1 -1
- package/lib/types/routes/catalog-data.d.ts +11 -2
- package/lib/types/routes/route-state.d.ts +5 -0
- package/package.json +17 -16
- package/src/client/locales/market.ts +2 -0
- package/src/client/panel/RepoScanCard.tsx +2 -1
- package/src/client/panel/panel.module.css +10 -2
- package/src/protocol/api.ts +0 -8
- package/src/protocol/repo.ts +6 -0
- package/src/protocol.ts +1 -1
- package/src/repo/install.ts +5 -2
- package/src/repo.test.ts +20 -0
- package/src/routes/catalog-data.ts +13 -2
- package/src/routes/market.ts +1 -10
- package/src/routes/repo-import.ts +52 -8
- package/src/routes/route-state.test.ts +1 -0
- package/src/routes/route-state.ts +5 -0
- package/src/routes.test.ts +122 -16
package/src/routes.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { SkillDefinition, SkillSummary } from '@deepseek-ai/dsh-skill'
|
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
7
|
import { makeRoutes, type SkillHubRouteDeps } from './routes.ts'
|
|
8
8
|
import { SkillHubStore, statePath } from './store.ts'
|
|
9
|
-
import { SKILL_HUB_API,
|
|
9
|
+
import { SKILL_HUB_API, SKILL_HUB_API_ROOT, type CatalogResponse, type ConfigResponse, type ErrorResponse, type HubConfig } from './protocol.ts'
|
|
10
10
|
|
|
11
11
|
/** Minimal response double recording status/headers/body. */
|
|
12
12
|
class FakeResponse {
|
|
@@ -695,6 +695,125 @@ describe('skill-hub routes', () => {
|
|
|
695
695
|
}
|
|
696
696
|
})
|
|
697
697
|
|
|
698
|
+
// ------------------------------------ 导入去重 vs 已禁用占位(issue #16)
|
|
699
|
+
/** 造一个「已禁用同名占位」:目录还在,SKILL.md 被改名成 SKILL.md.disabled。 */
|
|
700
|
+
async function seedDisabledPlaceholder(name: string) {
|
|
701
|
+
const dir = join(home, 'skills', name)
|
|
702
|
+
await mkdir(dir, { recursive: true })
|
|
703
|
+
await writeFile(join(dir, 'SKILL.md.disabled'), `---\nname: ${name}\ndescription: stub\n---\n`, 'utf8')
|
|
704
|
+
await store.addDisabled({
|
|
705
|
+
name,
|
|
706
|
+
description: 'stub',
|
|
707
|
+
path: join(dir, 'SKILL.md.disabled'),
|
|
708
|
+
root: 'user-dsh',
|
|
709
|
+
disabledAt: Date.now(),
|
|
710
|
+
})
|
|
711
|
+
return dir
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
it('does not flag a hub-disabled placeholder as existing when discovering', async () => {
|
|
715
|
+
await seedDisabledPlaceholder('code-review')
|
|
716
|
+
stubFetch([
|
|
717
|
+
['repos/example/repo/releases/latest', jsonResponse({ tag_name: 'v1.0.0' })],
|
|
718
|
+
['repos/example/repo/git/trees/v1.0.0', jsonResponse({ tree: [
|
|
719
|
+
{ path: 'skills/code-review/SKILL.md', type: 'blob', size: 60 },
|
|
720
|
+
] })],
|
|
721
|
+
['repos/example/repo', jsonResponse({ default_branch: 'main' })],
|
|
722
|
+
])
|
|
723
|
+
try {
|
|
724
|
+
const res = new FakeResponse()
|
|
725
|
+
await routeFor(SKILL_HUB_API.repo).handler(fakeReq('GET', `${SKILL_HUB_API.repo}?repo=example/repo`), res as never)
|
|
726
|
+
expect(res.status).toBe(200)
|
|
727
|
+
const body = res.json() as import('./protocol.ts').RepoDiscoverResponse
|
|
728
|
+
expect(body.ref).toBe('v1.0.0')
|
|
729
|
+
// 禁用只该影响启用态;被算成 existing 会让勾选框直接 disabled,用户无从下手。
|
|
730
|
+
expect(body.entries).toHaveLength(1)
|
|
731
|
+
expect(body.entries[0]).toMatchObject({ name: 'code-review', existing: false })
|
|
732
|
+
} finally {
|
|
733
|
+
vi.unstubAllGlobals()
|
|
734
|
+
}
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
it('imports over a hub-disabled placeholder instead of skipping it', async () => {
|
|
738
|
+
const dir = await seedDisabledPlaceholder('code-review')
|
|
739
|
+
const skillMd = '---\nname: code-review\ndescription: Reviews code\n---\n\nbody'
|
|
740
|
+
stubFetch([
|
|
741
|
+
['repos/example/repo/git/trees/main', jsonResponse({ tree: [
|
|
742
|
+
{ path: 'skills/code-review/SKILL.md', type: 'blob', size: 60 },
|
|
743
|
+
] })],
|
|
744
|
+
['repos/example/repo/commits', jsonResponse({ sha: 'abc123', commit: { tree: { sha: 'tree1' } } })],
|
|
745
|
+
['repos/example/repo', jsonResponse({ default_branch: 'main' })],
|
|
746
|
+
['raw.githubusercontent.com/example/repo/main/skills/code-review/SKILL.md', new Response(skillMd, { status: 200 })],
|
|
747
|
+
])
|
|
748
|
+
try {
|
|
749
|
+
const res = new FakeResponse()
|
|
750
|
+
await routeFor(SKILL_HUB_API.repoImport).handler(fakeReq('POST', SKILL_HUB_API.repoImport, { repo: 'example/repo', paths: ['skills/code-review/SKILL.md'] }), res as never)
|
|
751
|
+
expect(res.status).toBe(200)
|
|
752
|
+
const created = res.json() as import('./protocol.ts').RepoImportResponse
|
|
753
|
+
let prog: import('./protocol.ts').RepoImportProgressResponse | undefined
|
|
754
|
+
for (let i = 0; i < 20; i++) {
|
|
755
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
756
|
+
const progRes = new FakeResponse()
|
|
757
|
+
await routeFor(SKILL_HUB_API.repoImportProgress).handler(fakeReq('GET', `${SKILL_HUB_API.repoImportProgress}?jobId=${created.jobId}`), progRes as never)
|
|
758
|
+
prog = progRes.json() as import('./protocol.ts').RepoImportProgressResponse
|
|
759
|
+
if (prog.status !== 'running') break
|
|
760
|
+
}
|
|
761
|
+
expect(prog!.status).toBe('done')
|
|
762
|
+
// 关键:不再被「已存在」跳掉,真技能落地。
|
|
763
|
+
expect(prog!.skipped).toEqual([])
|
|
764
|
+
expect(prog!.imported.map((r) => r.name)).toEqual(['code-review'])
|
|
765
|
+
expect(prog!.replaced).toEqual(['code-review'])
|
|
766
|
+
await expect(readFile(join(dir, 'SKILL.md'), 'utf8')).resolves.toContain('name: code-review')
|
|
767
|
+
// 旧占位进回收站(可恢复),禁用记录被清掉,不然目录会留个幽灵禁用项。
|
|
768
|
+
await expect(access(join(dir, 'SKILL.md.disabled'))).rejects.toThrow()
|
|
769
|
+
expect(await store.listDisabled()).toEqual([])
|
|
770
|
+
const trash = await store.listTrash()
|
|
771
|
+
expect(trash).toHaveLength(1)
|
|
772
|
+
expect(trash[0].name).toBe('code-review')
|
|
773
|
+
expect(trash[0].path.startsWith(join(home, 'skills', '.trash', 'code-review-'))).toBe(true)
|
|
774
|
+
expect(trash[0].sourcePath).toBe(dir)
|
|
775
|
+
expect(await store.listSources()).toHaveLength(1)
|
|
776
|
+
} finally {
|
|
777
|
+
vi.unstubAllGlobals()
|
|
778
|
+
}
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
it('still skips a same-name directory that is not a disabled placeholder', async () => {
|
|
782
|
+
// 同名目录里还有真实 SKILL.md、且没有对应的禁用记录:不是占位,不许动。
|
|
783
|
+
const dir = join(home, 'skills', 'code-review')
|
|
784
|
+
await mkdir(dir, { recursive: true })
|
|
785
|
+
await writeFile(join(dir, 'SKILL.md'), '---\nname: code-review\ndescription: real\n---\n', 'utf8')
|
|
786
|
+
stubFetch([
|
|
787
|
+
['repos/example/repo/git/trees/main', jsonResponse({ tree: [
|
|
788
|
+
{ path: 'skills/code-review/SKILL.md', type: 'blob', size: 60 },
|
|
789
|
+
] })],
|
|
790
|
+
['repos/example/repo/commits', jsonResponse({ sha: 'abc123', commit: { tree: { sha: 'tree1' } } })],
|
|
791
|
+
['repos/example/repo', jsonResponse({ default_branch: 'main' })],
|
|
792
|
+
])
|
|
793
|
+
try {
|
|
794
|
+
const res = new FakeResponse()
|
|
795
|
+
await routeFor(SKILL_HUB_API.repoImport).handler(fakeReq('POST', SKILL_HUB_API.repoImport, { repo: 'example/repo', paths: ['skills/code-review/SKILL.md'] }), res as never)
|
|
796
|
+
const created = res.json() as import('./protocol.ts').RepoImportResponse
|
|
797
|
+
let prog: import('./protocol.ts').RepoImportProgressResponse | undefined
|
|
798
|
+
for (let i = 0; i < 20; i++) {
|
|
799
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
800
|
+
const progRes = new FakeResponse()
|
|
801
|
+
await routeFor(SKILL_HUB_API.repoImportProgress).handler(fakeReq('GET', `${SKILL_HUB_API.repoImportProgress}?jobId=${created.jobId}`), progRes as never)
|
|
802
|
+
prog = progRes.json() as import('./protocol.ts').RepoImportProgressResponse
|
|
803
|
+
if (prog.status !== 'running') break
|
|
804
|
+
}
|
|
805
|
+
expect(prog!.status).toBe('done')
|
|
806
|
+
expect(prog!.skipped).toEqual([{ name: 'code-review', reason: 'exists' }])
|
|
807
|
+
expect(prog!.imported).toEqual([])
|
|
808
|
+
expect(prog!.replaced ?? []).toEqual([])
|
|
809
|
+
// 原目录原封不动,也没进回收站。
|
|
810
|
+
await expect(readFile(join(dir, 'SKILL.md'), 'utf8')).resolves.toContain('description: real')
|
|
811
|
+
expect(await store.listTrash()).toEqual([])
|
|
812
|
+
} finally {
|
|
813
|
+
vi.unstubAllGlobals()
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
|
|
698
817
|
// -------------------------------------------------------------- groups
|
|
699
818
|
it('serves groups: user tags + system collections + origins', async () => {
|
|
700
819
|
const tag = await store.saveTag({ name: 'web' })
|
|
@@ -1038,9 +1157,8 @@ describe('skill-hub API surface', () => {
|
|
|
1038
1157
|
const declared = Object.values(SKILL_HUB_API)
|
|
1039
1158
|
for (const path of declared) expect(path.startsWith(SKILL_HUB_API_ROOT + '/')).toBe(true)
|
|
1040
1159
|
// 常量与注册表互为子集:一个声明了没注册(客户端必然 404),或注册了没声明
|
|
1041
|
-
//
|
|
1042
|
-
expect([...registered].sort()).toEqual([...declared
|
|
1043
|
-
expect(registered.has(SKILL_HUB_API_DEPRECATED_MARKET_CHECK)).toBe(true)
|
|
1160
|
+
// (客户端永远调不到),都是漂移。
|
|
1161
|
+
expect([...registered].sort()).toEqual([...declared].sort())
|
|
1044
1162
|
})
|
|
1045
1163
|
|
|
1046
1164
|
it('pairs the market update check with the sync that acts on it', () => {
|
|
@@ -1050,18 +1168,6 @@ describe('skill-hub API surface', () => {
|
|
|
1050
1168
|
for (const path of [SKILL_HUB_API.marketCheck, SKILL_HUB_API.marketSync]) {
|
|
1051
1169
|
expect(path.startsWith('/api/skill-hub/market/source/')).toBe(true)
|
|
1052
1170
|
}
|
|
1053
|
-
expect(SKILL_HUB_API_DEPRECATED_MARKET_CHECK).not.toBe(SKILL_HUB_API.marketCheck)
|
|
1054
|
-
})
|
|
1055
|
-
|
|
1056
|
-
it('answers the deprecated market-check alias instead of 404', async () => {
|
|
1057
|
-
const routes = makeRoutes(deps)
|
|
1058
|
-
const route = routes.find((r) => r.path === SKILL_HUB_API_DEPRECATED_MARKET_CHECK)
|
|
1059
|
-
expect(route).toBeDefined()
|
|
1060
|
-
const res = new FakeResponse()
|
|
1061
|
-
await route?.handler(fakeReq('GET', SKILL_HUB_API_DEPRECATED_MARKET_CHECK), res as never)
|
|
1062
|
-
// Same handler as the canonical route: a list of per-source results.
|
|
1063
|
-
expect(res.status).toBe(200)
|
|
1064
|
-
expect((res.json() as { results: unknown[] }).results).toEqual([])
|
|
1065
1171
|
})
|
|
1066
1172
|
|
|
1067
1173
|
it('registers one 404 catch-all covering the whole family', async () => {
|