dsh-skill-hub 0.0.1 → 0.2.0

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.
Files changed (88) hide show
  1. package/CONTRIBUTING.md +56 -0
  2. package/README.md +355 -20
  3. package/cordis.patch.yml +12 -0
  4. package/lib/client.js +4514 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/index.js +3119 -0
  7. package/lib/invariant.js +6 -0
  8. package/lib/types/client/SkillHubSettingsCard.d.ts +53 -0
  9. package/lib/types/client/api-config-scope.d.ts +48 -0
  10. package/lib/types/client/api.d.ts +64 -0
  11. package/lib/types/client/grouping.d.ts +64 -0
  12. package/lib/types/client/helpers.d.ts +11 -0
  13. package/lib/types/client/index.d.ts +42 -0
  14. package/lib/types/client/locales.d.ts +216 -0
  15. package/lib/types/client/market-catalog.d.ts +14 -0
  16. package/lib/types/client/panel/DisabledRow.d.ts +12 -0
  17. package/lib/types/client/panel/GroupSummary.d.ts +10 -0
  18. package/lib/types/client/panel/MarketView.d.ts +11 -0
  19. package/lib/types/client/panel/ScenesView.d.ts +11 -0
  20. package/lib/types/client/panel/SkillDetailView.d.ts +31 -0
  21. package/lib/types/client/panel/SkillHubPanel.d.ts +18 -0
  22. package/lib/types/client/panel/SkillRow.d.ts +11 -0
  23. package/lib/types/client/panel/SourceStatusBadge.d.ts +18 -0
  24. package/lib/types/client/panel/SourcesView.d.ts +10 -0
  25. package/lib/types/client/panel/TagEditorView.d.ts +29 -0
  26. package/lib/types/client/panel/dialogs.d.ts +73 -0
  27. package/lib/types/client/panel/format.d.ts +14 -0
  28. package/lib/types/client/panel/useSkillHub.d.ts +183 -0
  29. package/lib/types/client/settings-card.d.ts +81 -0
  30. package/lib/types/client/settings-form.d.ts +95 -0
  31. package/lib/types/client/settings-nav-icon.d.ts +8 -0
  32. package/lib/types/index.d.ts +36 -0
  33. package/lib/types/invariant.d.ts +3 -0
  34. package/lib/types/protocol.d.ts +571 -0
  35. package/lib/types/provider.d.ts +47 -0
  36. package/lib/types/repo.d.ts +133 -0
  37. package/lib/types/routes.d.ts +51 -0
  38. package/lib/types/skillfs.d.ts +95 -0
  39. package/lib/types/stats.d.ts +49 -0
  40. package/lib/types/store.d.ts +142 -0
  41. package/lib/types/update.d.ts +20 -0
  42. package/package.json +115 -11
  43. package/src/client/SkillHubSettingsCard.tsx +161 -0
  44. package/src/client/api-config-scope.test.ts +79 -0
  45. package/src/client/api-config-scope.ts +119 -0
  46. package/src/client/api.ts +327 -0
  47. package/src/client/css-modules.d.ts +5 -0
  48. package/src/client/grouping.test.ts +109 -0
  49. package/src/client/grouping.ts +120 -0
  50. package/src/client/helpers.ts +23 -0
  51. package/src/client/index.tsx +104 -0
  52. package/src/client/locales.ts +432 -0
  53. package/src/client/market-catalog.ts +22 -0
  54. package/src/client/panel/DisabledRow.tsx +32 -0
  55. package/src/client/panel/GroupSummary.tsx +27 -0
  56. package/src/client/panel/MarketView.tsx +173 -0
  57. package/src/client/panel/ScenesView.tsx +75 -0
  58. package/src/client/panel/SkillDetailView.tsx +95 -0
  59. package/src/client/panel/SkillHubPanel.tsx +347 -0
  60. package/src/client/panel/SkillRow.tsx +54 -0
  61. package/src/client/panel/SourceStatusBadge.tsx +61 -0
  62. package/src/client/panel/SourcesView.tsx +104 -0
  63. package/src/client/panel/TagEditorView.tsx +89 -0
  64. package/src/client/panel/dialogs.tsx +185 -0
  65. package/src/client/panel/format.ts +31 -0
  66. package/src/client/panel/panel.module.css +219 -0
  67. package/src/client/panel/useSkillHub.ts +797 -0
  68. package/src/client/settings-card.module.css +52 -0
  69. package/src/client/settings-card.tsx +237 -0
  70. package/src/client/settings-form.ts +227 -0
  71. package/src/client/settings-nav-icon.ts +42 -0
  72. package/src/index.ts +181 -0
  73. package/src/invariant.ts +4 -0
  74. package/src/protocol.ts +622 -0
  75. package/src/provider.test.ts +112 -0
  76. package/src/provider.ts +168 -0
  77. package/src/repo.test.ts +212 -0
  78. package/src/repo.ts +528 -0
  79. package/src/routes.test.ts +786 -0
  80. package/src/routes.ts +1252 -0
  81. package/src/skillfs.test.ts +290 -0
  82. package/src/skillfs.ts +306 -0
  83. package/src/stats.test.ts +168 -0
  84. package/src/stats.ts +124 -0
  85. package/src/store.test.ts +253 -0
  86. package/src/store.ts +628 -0
  87. package/src/update.test.ts +73 -0
  88. package/src/update.ts +136 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * The dsh-skill-hub plugin settings card: bridges the hub's own config route
3
+ * (ApiConfigScope → /api/skill-hub/config) onto the family-style staged card
4
+ * form (enabled master switch + agent announcement). Registered into the
5
+ * official `settings.plugin.item` slot so the plugin shows up in Settings →
6
+ * 插件. The scope is a FormScope, so the card never touches the settings
7
+ * service (the host refuses third-party namespaces).
8
+ */
9
+
10
+ import type { ReactElement } from 'react'
11
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
12
+ import { ColorField, PluginSettingsCard, SwitchField } from './settings-card.tsx'
13
+ import { booleanField, CardForm, colorField, type CardShell, type FieldState, type FormScope } from './settings-form.ts'
14
+ import { DEFAULT_DOT_MODEL_COLOR, DEFAULT_DOT_USER_COLOR } from './api-config-scope.ts'
15
+
16
+ /** The card's projected state. */
17
+ export interface SkillHubSettingsState extends CardShell {
18
+ enabled: FieldState
19
+ announceToAgent: FieldState
20
+ dotModelColor: FieldState
21
+ dotUserColor: FieldState
22
+ showUseCount: FieldState
23
+ showUseTime: FieldState
24
+ showGroupSummary: FieldState
25
+ }
26
+
27
+ /** Props the slot renderer receives (locale copy + injected form actions). */
28
+ export interface SkillHubSettingsCardProps {
29
+ t: (key: string) => string
30
+ useSkillHubSettingsCard: <T>(selector: (snapshot: SkillHubSettingsState) => T) => T
31
+ save: () => void
32
+ discard: () => void
33
+ edit: (field: string, text: string) => void
34
+ resetField: (field: string) => void
35
+ }
36
+
37
+ /** Bridges the hub's config scope onto the card's staged form. */
38
+ export class SkillHubSettingsCardController {
39
+ private readonly form: CardForm
40
+ private readonly store: SnapshotStore<SkillHubSettingsState>
41
+
42
+ /** @param scope - the hub config scope the card edits (ApiConfigScope). */
43
+ constructor(scope: FormScope) {
44
+ this.form = new CardForm(scope, [booleanField('enabled'), booleanField('announceToAgent'), colorField('dotModelColor'), colorField('dotUserColor'), booleanField('showUseCount'), booleanField('showUseTime'), booleanField('showGroupSummary')])
45
+ this.store = this.form.bind(() => this.projection())
46
+ }
47
+
48
+ private projection(): SkillHubSettingsState {
49
+ return {
50
+ ...this.form.shell(),
51
+ enabled: this.form.field('enabled'),
52
+ announceToAgent: this.form.field('announceToAgent'),
53
+ dotModelColor: this.form.field('dotModelColor'),
54
+ dotUserColor: this.form.field('dotUserColor'),
55
+ showUseCount: this.form.field('showUseCount'),
56
+ showUseTime: this.form.field('showUseTime'),
57
+ showGroupSummary: this.form.field('showGroupSummary'),
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Build the face the card's slot registration injects.
63
+ * @returns the card's snapshot hook and its form actions.
64
+ */
65
+ inject(): { hooks: { skillHubSettingsCard: SnapshotStore<SkillHubSettingsState> } } & ReturnType<CardForm['actions']> {
66
+ return {
67
+ hooks: { skillHubSettingsCard: this.store },
68
+ ...this.form.actions(),
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Render the dsh-skill-hub card.
75
+ * @param props - locale copy, the card snapshot, and its form actions.
76
+ * @returns the card.
77
+ */
78
+ export function SkillHubSettingsCard(props: SkillHubSettingsCardProps): ReactElement {
79
+ const { t } = props
80
+ const state = props.useSkillHubSettingsCard((snapshot) => snapshot)
81
+ const disabled = !state.writable
82
+ const fieldProps = {
83
+ overriddenLabel: t('settings.overridden'),
84
+ resetLabel: t('settings.reset'),
85
+ disabled,
86
+ }
87
+ return (
88
+ <PluginSettingsCard
89
+ t={t}
90
+ titleKey='settings.title'
91
+ descriptionKey='settings.description'
92
+ state={state}
93
+ onSave={props.save}
94
+ onDiscard={props.discard}
95
+ >
96
+ <SwitchField
97
+ label={t('settings.enabled')}
98
+ hint={t('settings.enabledHint')}
99
+ {...fieldProps}
100
+ {...state.enabled}
101
+ onEdit={(text) => { props.edit('enabled', text) }}
102
+ onReset={() => { props.resetField('enabled') }}
103
+ />
104
+ <SwitchField
105
+ label={t('settings.announceToAgent')}
106
+ hint={t('settings.announceToAgentHint')}
107
+ {...fieldProps}
108
+ {...state.announceToAgent}
109
+ onEdit={(text) => { props.edit('announceToAgent', text) }}
110
+ onReset={() => { props.resetField('announceToAgent') }}
111
+ />
112
+ <ColorField
113
+ id='skill-hub-dot-model-color'
114
+ label={t('settings.dotModelColor')}
115
+ hint={t('settings.dotModelColorHint')}
116
+ inheritLabel={t('settings.inherit')}
117
+ defaultColor={DEFAULT_DOT_MODEL_COLOR}
118
+ {...fieldProps}
119
+ {...state.dotModelColor}
120
+ onEdit={(text) => { props.edit('dotModelColor', text) }}
121
+ onReset={() => { props.resetField('dotModelColor') }}
122
+ />
123
+ <ColorField
124
+ id='skill-hub-dot-user-color'
125
+ label={t('settings.dotUserColor')}
126
+ hint={t('settings.dotUserColorHint')}
127
+ inheritLabel={t('settings.inherit')}
128
+ defaultColor={DEFAULT_DOT_USER_COLOR}
129
+ {...fieldProps}
130
+ {...state.dotUserColor}
131
+ onEdit={(text) => { props.edit('dotUserColor', text) }}
132
+ onReset={() => { props.resetField('dotUserColor') }}
133
+ />
134
+ <SwitchField
135
+ label={t('settings.showUseCount')}
136
+ hint={t('settings.showUseCountHint')}
137
+ {...fieldProps}
138
+ {...state.showUseCount}
139
+ onEdit={(text) => { props.edit('showUseCount', text) }}
140
+ onReset={() => { props.resetField('showUseCount') }}
141
+ />
142
+ <SwitchField
143
+ label={t('settings.showUseTime')}
144
+ hint={t('settings.showUseTimeHint')}
145
+ {...fieldProps}
146
+ {...state.showUseTime}
147
+ onEdit={(text) => { props.edit('showUseTime', text) }}
148
+ onReset={() => { props.resetField('showUseTime') }}
149
+ />
150
+ <SwitchField
151
+ label={t('settings.showGroupSummary')}
152
+ hint={t('settings.showGroupSummaryHint')}
153
+ {...fieldProps}
154
+ {...state.showGroupSummary}
155
+ onEdit={(text) => { props.edit('showGroupSummary', text) }}
156
+ onReset={() => { props.resetField('showGroupSummary') }}
157
+ />
158
+ </PluginSettingsCard>
159
+ )
160
+ }
161
+
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { ConfigRequest, ConfigResponse, HubConfig } from '../protocol.ts'
3
+ import type { SkillHubApi } from './api.ts'
4
+ import { ApiConfigScope } from './api-config-scope.ts'
5
+
6
+ function deferredReady(scope: ApiConfigScope): Promise<void> {
7
+ return new Promise((resolve) => setTimeout(resolve, 0))
8
+ }
9
+
10
+ describe('ApiConfigScope', () => {
11
+ it('persists the panel display toggles (showUseCount etc.)', async () => {
12
+ const saved: Partial<HubConfig> = {}
13
+ const writes: ConfigRequest[] = []
14
+ const api = {
15
+ config: async (): Promise<ConfigResponse> => ({
16
+ ok: true,
17
+ config: {
18
+ enabled: true,
19
+ announceToAgent: true,
20
+ showUseCount: true,
21
+ showUseTime: true,
22
+ showGroupSummary: true,
23
+ },
24
+ saved,
25
+ }),
26
+ saveConfig: async (patch: ConfigRequest): Promise<ConfigResponse> => {
27
+ writes.push(patch)
28
+ for (const [key, value] of Object.entries(patch)) {
29
+ if (value === null) delete saved[key as keyof HubConfig]
30
+ else saved[key as keyof HubConfig] = value as never
31
+ }
32
+ return {
33
+ ok: true,
34
+ config: {
35
+ enabled: true,
36
+ announceToAgent: true,
37
+ showUseCount: saved.showUseCount !== false,
38
+ showUseTime: saved.showUseTime !== false,
39
+ showGroupSummary: saved.showGroupSummary !== false,
40
+ },
41
+ saved,
42
+ }
43
+ },
44
+ } as unknown as SkillHubApi
45
+
46
+ const scope = new ApiConfigScope(api)
47
+ await deferredReady(scope)
48
+
49
+ await scope.set('showUseCount', false)
50
+ expect(writes).toContainEqual({ showUseCount: false })
51
+ expect(scope.getSnapshot().value).toMatchObject({ showUseCount: false })
52
+ expect(scope.getSnapshot().user).toMatchObject({ showUseCount: false })
53
+
54
+ await scope.unset('showUseCount')
55
+ expect(writes).toContainEqual({ showUseCount: null })
56
+ expect(scope.getSnapshot().user).not.toHaveProperty('showUseCount')
57
+ expect(scope.getSnapshot().value).toMatchObject({ showUseCount: true })
58
+ })
59
+
60
+ it('ignores unknown fields', async () => {
61
+ const writes: ConfigRequest[] = []
62
+ const api = {
63
+ config: async (): Promise<ConfigResponse> => ({
64
+ ok: true,
65
+ config: { enabled: true, announceToAgent: true },
66
+ saved: {},
67
+ }),
68
+ saveConfig: async (patch: ConfigRequest): Promise<ConfigResponse> => {
69
+ writes.push(patch)
70
+ return { ok: true, config: { enabled: true, announceToAgent: true }, saved: {} }
71
+ },
72
+ } as unknown as SkillHubApi
73
+
74
+ const scope = new ApiConfigScope(api)
75
+ await deferredReady(scope)
76
+ await scope.set('not-a-field', true)
77
+ expect(writes).toHaveLength(0)
78
+ })
79
+ })
@@ -0,0 +1,119 @@
1
+ /**
2
+ * ApiConfigScope — a FormScope-compatible view over the hub's own config
3
+ * route (/api/skill-hub/config). The host's settings service refuses to
4
+ * expose third-party namespaces to the web client (dsh-host-apiproxy's
5
+ * allowlist), so the settings card talks to this plugin-owned route instead
6
+ * of the settings transport.
7
+ *
8
+ * The scope mirrors the settings-scope contract the vendored CardForm
9
+ * consumes: `value` is the effective config, `base` the built-in defaults,
10
+ * and `user` the raw saved overrides (a field's presence there marks it
11
+ * overridden). Writes go straight through saveConfig and re-seed the
12
+ * snapshot from the route's response, so the card's save-verification
13
+ * (user layer equals the written value) sees the persisted state.
14
+ */
15
+
16
+ import type { HubConfig } from '../protocol.ts'
17
+ import type { SkillHubApi } from './api.ts'
18
+ import type { FormScope } from './settings-form.ts'
19
+
20
+ /** Model-invocable dot color default. Single source for the TS side; the
21
+ * panel's CSS mirrors it via --hub-model (panel.module.css). */
22
+ export const DEFAULT_DOT_MODEL_COLOR = '#2f81f7'
23
+ /** User-invocable dot color default. Single source for the TS side; the
24
+ * panel's CSS mirrors it via --hub-user (panel.module.css). */
25
+ export const DEFAULT_DOT_USER_COLOR = '#3fb950'
26
+
27
+ /** Built-in defaults every field inherits until the user overrides it. */
28
+ const DEFAULTS: HubConfig = {
29
+ enabled: true,
30
+ announceToAgent: true,
31
+ showUseCount: true,
32
+ showUseTime: true,
33
+ showGroupSummary: true,
34
+ }
35
+
36
+ /** Boolean config fields (color fields are strings). */
37
+ const BOOLEAN_FIELDS = new Set<keyof HubConfig>([
38
+ 'enabled', 'announceToAgent', 'showUseCount', 'showUseTime', 'showGroupSummary',
39
+ ])
40
+
41
+ /** Config fields the card edits (used to guard set/unset field names). */
42
+ const FIELDS = new Set<keyof HubConfig>([
43
+ 'enabled', 'announceToAgent', 'dotModelColor', 'dotUserColor',
44
+ 'showUseCount', 'showUseTime', 'showGroupSummary',
45
+ ])
46
+
47
+ /** FormScope-compatible reactive config handle (see module doc). */
48
+ export class ApiConfigScope implements FormScope {
49
+ private status: 'loading' | 'ready' | 'unavailable' = 'loading'
50
+ private value: HubConfig = { ...DEFAULTS }
51
+ private saved: Partial<HubConfig> = {}
52
+ private readonly listeners = new Set<() => void>()
53
+
54
+ /** @param api - the browser-half API client (shared with the skill panel). */
55
+ constructor(private readonly api: SkillHubApi) {
56
+ void this.load()
57
+ }
58
+
59
+ private async load(): Promise<void> {
60
+ try {
61
+ const response = await this.api.config()
62
+ this.value = response.config
63
+ this.saved = { ...response.saved }
64
+ this.status = 'ready'
65
+ } catch {
66
+ // The route is missing or unreachable (e.g. host half not yet updated):
67
+ // the card shows its not-exposed state instead of breaking the GUI.
68
+ this.status = 'unavailable'
69
+ }
70
+ this.publish()
71
+ }
72
+
73
+ subscribe(listener: () => void): () => void {
74
+ this.listeners.add(listener)
75
+ return () => { this.listeners.delete(listener) }
76
+ }
77
+
78
+ getSnapshot(): { status: string; writable: boolean; value?: Record<string, unknown>; base?: unknown; user?: unknown } {
79
+ return {
80
+ status: this.status,
81
+ writable: true,
82
+ value: { ...this.value } as unknown as Record<string, unknown>,
83
+ base: { ...DEFAULTS },
84
+ user: { ...this.saved },
85
+ }
86
+ }
87
+
88
+ /** Write one field through the config route and adopt the persisted state. */
89
+ async set(field: string, value: unknown): Promise<void> {
90
+ if (!FIELDS.has(field as keyof HubConfig)) return
91
+ const patch = BOOLEAN_FIELDS.has(field as keyof HubConfig)
92
+ ? { [field]: Boolean(value) }
93
+ : { [field]: typeof value === 'string' ? value : undefined }
94
+ await this.apply(patch as Partial<HubConfig>)
95
+ }
96
+
97
+ /** Clear one field's override so it re-inherits the default. */
98
+ async unset(field: string): Promise<void> {
99
+ if (!FIELDS.has(field as keyof HubConfig)) return
100
+ await this.apply({ [field]: undefined } as Partial<HubConfig>)
101
+ }
102
+
103
+ /** Persist a patch, then re-seed from the route's fresh response. */
104
+ private async apply(patch: Partial<HubConfig>): Promise<void> {
105
+ // null on the wire clears a saved override (see ConfigRequest).
106
+ const request: Record<string, boolean | string | null> = {}
107
+ for (const [key, value] of Object.entries(patch)) {
108
+ request[key] = value === undefined ? null : value
109
+ }
110
+ const response = await this.api.saveConfig(request as never)
111
+ this.value = response.config
112
+ this.saved = { ...response.saved }
113
+ this.publish()
114
+ }
115
+
116
+ private publish(): void {
117
+ for (const listener of this.listeners) listener()
118
+ }
119
+ }
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Browser-side API client for the /api/skill-hub route family. The only
3
+ * data access path the panel uses — plain fetch, same origin.
4
+ */
5
+ import {
6
+ SKILL_HUB_API,
7
+ type CatalogResponse,
8
+ type ConfigRequest,
9
+ type ConfigResponse,
10
+ type CreateRequest,
11
+ type CreateResponse,
12
+ type MarketCheckResponse,
13
+ type MarketSourceRefRequest,
14
+ type MarketSourceRequest,
15
+ type MarketSourceResponse,
16
+ type MarketSourcesResponse,
17
+ type MarketSyncResponse,
18
+ type RepoDiscoverResponse,
19
+ type RepoImportRequest,
20
+ type RepoImportResponse,
21
+ type SkillDetail,
22
+ type SkillDetailResponse,
23
+ type SkillDeleteRequest,
24
+ type SkillDeleteResponse,
25
+ type SourceCheckRequest,
26
+ type SourceCheckResponse,
27
+ type SourceDeleteRequest,
28
+ type SourceDeleteResponse,
29
+ type SourceRestoreRequest,
30
+ type SourceRestoreResponse,
31
+ type SourceTrashClearResponse,
32
+ type SourcesResponse,
33
+ type SourceSyncRequest,
34
+ type SourceSyncResponse,
35
+ type StatsResponse,
36
+ type ToggleBatchRequest,
37
+ type ToggleBatchResponse,
38
+ type UpdateCheckResponse,
39
+ } from '../protocol.ts'
40
+
41
+ /** Error carrying the route's JSON error message. */
42
+ export class SkillHubApiError extends Error {
43
+ constructor(message: string) {
44
+ super(message)
45
+ this.name = 'SkillHubApiError'
46
+ }
47
+ }
48
+
49
+ /** Parse a JSON response or throw a SkillHubApiError. */
50
+ async function readJson<T>(response: Response): Promise<T> {
51
+ let body: unknown
52
+ try {
53
+ body = await response.json()
54
+ } catch {
55
+ throw new SkillHubApiError('HTTP ' + response.status + ': invalid JSON response')
56
+ }
57
+ if (!response.ok) {
58
+ const message = typeof body === 'object' && body !== null && typeof (body as { error?: unknown }).error === 'string'
59
+ ? (body as { error: string }).error
60
+ : 'HTTP ' + response.status
61
+ throw new SkillHubApiError(message)
62
+ }
63
+ return body as T
64
+ }
65
+
66
+ /**
67
+ * Fetch with a hard timeout: a hung connection (proxy, stalled keep-alive,
68
+ * host wedged) must never leave the panel stuck on "loading" forever — it
69
+ * fails fast and surfaces the error banner instead.
70
+ */
71
+ async function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit, ms = 10_000): Promise<Response> {
72
+ const controller = new AbortController()
73
+ const timer = setTimeout(() => controller.abort(), ms)
74
+ try {
75
+ return await fetch(input, { ...init, signal: controller.signal })
76
+ } finally {
77
+ clearTimeout(timer)
78
+ }
79
+ }
80
+
81
+ /** The browser half's only data entry point. */
82
+ export class SkillHubApi {
83
+ async catalog(): Promise<CatalogResponse> {
84
+ const response = await fetchWithTimeout(SKILL_HUB_API.catalog)
85
+ return readJson<CatalogResponse>(response)
86
+ }
87
+
88
+ async skill(name: string): Promise<SkillDetail> {
89
+ const url = SKILL_HUB_API.skill + '?name=' + encodeURIComponent(name)
90
+ const response = await fetchWithTimeout(url)
91
+ const body = await readJson<SkillDetailResponse>(response)
92
+ return body.skill
93
+ }
94
+
95
+ /** Move one writable skill into the restorable trash. */
96
+ async deleteSkill(name: string): Promise<SkillDeleteResponse> {
97
+ const response = await fetchWithTimeout(SKILL_HUB_API.skillDelete, {
98
+ method: 'POST',
99
+ headers: { 'content-type': 'application/json' },
100
+ body: JSON.stringify({ name } satisfies SkillDeleteRequest),
101
+ })
102
+ return readJson<SkillDeleteResponse>(response)
103
+ }
104
+
105
+ /** Toggle one skill; resolves with the fresh catalog from the route. */
106
+ async toggle(name: string, enabled: boolean): Promise<CatalogResponse> {
107
+ const response = await fetchWithTimeout(SKILL_HUB_API.toggle, {
108
+ method: 'POST',
109
+ headers: { 'content-type': 'application/json' },
110
+ body: JSON.stringify({ name, enabled } satisfies import('../protocol.ts').ToggleRequest),
111
+ })
112
+ const body = await readJson<import('../protocol.ts').ToggleResponse>(response)
113
+ return body.catalog
114
+ }
115
+
116
+ async create(payload: CreateRequest): Promise<CreateResponse> {
117
+ const response = await fetchWithTimeout(SKILL_HUB_API.create, {
118
+ method: 'POST',
119
+ headers: { 'content-type': 'application/json' },
120
+ body: JSON.stringify(payload),
121
+ })
122
+ return readJson<CreateResponse>(response)
123
+ }
124
+
125
+ async stats(): Promise<StatsResponse> {
126
+ const response = await fetchWithTimeout(SKILL_HUB_API.stats)
127
+ return readJson<StatsResponse>(response)
128
+ }
129
+
130
+ /** Toggle a whole group in one write; resolves with the fresh catalog + failures. */
131
+ async toggleBatch(names: string[], enabled: boolean): Promise<ToggleBatchResponse> {
132
+ const payload: ToggleBatchRequest = { names, enabled }
133
+ const response = await fetchWithTimeout(SKILL_HUB_API.toggleBatch, {
134
+ method: 'POST',
135
+ headers: { 'content-type': 'application/json' },
136
+ body: JSON.stringify(payload),
137
+ })
138
+ return readJson<ToggleBatchResponse>(response)
139
+ }
140
+
141
+ /** The user's added market sources (codex-style repo slugs). */
142
+ async market(): Promise<MarketSourcesResponse> {
143
+ const response = await fetchWithTimeout(SKILL_HUB_API.market)
144
+ return readJson<MarketSourcesResponse>(response)
145
+ }
146
+
147
+ /** Add a market source repo; resolves with the fresh list. */
148
+ async addMarketSource(repo: string): Promise<MarketSourcesResponse> {
149
+ const response = await fetchWithTimeout(SKILL_HUB_API.marketSource, {
150
+ method: 'POST',
151
+ headers: { 'content-type': 'application/json' },
152
+ body: JSON.stringify({ repo } satisfies MarketSourceRequest),
153
+ })
154
+ return readJson<MarketSourceResponse>(response)
155
+ }
156
+
157
+ /** Remove a market source repo; resolves with the fresh list. */
158
+ async removeMarketSource(repo: string): Promise<MarketSourcesResponse> {
159
+ const response = await fetchWithTimeout(SKILL_HUB_API.marketSourceDelete, {
160
+ method: 'POST',
161
+ headers: { 'content-type': 'application/json' },
162
+ body: JSON.stringify({ repo } satisfies MarketSourceRequest),
163
+ })
164
+ return readJson<MarketSourceResponse>(response)
165
+ }
166
+
167
+ /** Check every market source for updates (throttled server-side). */
168
+ async marketCheck(): Promise<MarketCheckResponse> {
169
+ const response = await fetchWithTimeout(SKILL_HUB_API.marketCheck)
170
+ return readJson<MarketCheckResponse>(response)
171
+ }
172
+
173
+ /** Align a market source to its pinned ref; resolves with tracked skills. */
174
+ async marketSync(repo: string): Promise<MarketSyncResponse> {
175
+ const response = await fetchWithTimeout(SKILL_HUB_API.marketSync, {
176
+ method: 'POST',
177
+ headers: { 'content-type': 'application/json' },
178
+ body: JSON.stringify({ repo }),
179
+ })
180
+ return readJson<MarketSyncResponse>(response)
181
+ }
182
+
183
+ /** Pin a market source to an explicit ref (branch picker). */
184
+ async setMarketSourceRef(repo: string, ref: string): Promise<MarketSourcesResponse> {
185
+ const response = await fetchWithTimeout(SKILL_HUB_API.marketSourceRef, {
186
+ method: 'POST',
187
+ headers: { 'content-type': 'application/json' },
188
+ body: JSON.stringify({ repo, ref } satisfies MarketSourceRefRequest),
189
+ })
190
+ return readJson<MarketSourceResponse>(response)
191
+ }
192
+
193
+ /** Check the hub's own latest GitHub release. */
194
+ async updateCheck(): Promise<UpdateCheckResponse> {
195
+ const response = await fetchWithTimeout(SKILL_HUB_API.update)
196
+ return readJson<UpdateCheckResponse>(response)
197
+ }
198
+
199
+ /** Discover importable skills in a GitHub repo. */
200
+ async repoDiscover(repo: string): Promise<RepoDiscoverResponse> {
201
+ const url = SKILL_HUB_API.repo + '?repo=' + encodeURIComponent(repo)
202
+ const response = await fetchWithTimeout(url)
203
+ return readJson<RepoDiscoverResponse>(response)
204
+ }
205
+
206
+ /** Install selected repo skills into the user-dsh root (records the source). */
207
+ async repoImport(repo: string, paths: string[], ref?: string): Promise<RepoImportResponse> {
208
+ const payload: RepoImportRequest = { repo, paths, ...(ref !== undefined && ref !== '' ? { ref } : {}) }
209
+ const response = await fetchWithTimeout(SKILL_HUB_API.repoImport, {
210
+ method: 'POST',
211
+ headers: { 'content-type': 'application/json' },
212
+ body: JSON.stringify(payload),
213
+ })
214
+ return readJson<RepoImportResponse>(response)
215
+ }
216
+
217
+ /** Read the hub's runtime config (effective values + saved overrides). */
218
+ async config(): Promise<ConfigResponse> {
219
+ const response = await fetchWithTimeout(SKILL_HUB_API.config)
220
+ return readJson<ConfigResponse>(response)
221
+ }
222
+
223
+ /** Patch the hub's runtime config (null clears a saved override). */
224
+ async saveConfig(patch: ConfigRequest): Promise<ConfigResponse> {
225
+ const response = await fetchWithTimeout(SKILL_HUB_API.config, {
226
+ method: 'POST',
227
+ headers: { 'content-type': 'application/json' },
228
+ body: JSON.stringify(patch),
229
+ })
230
+ return readJson<ConfigResponse>(response)
231
+ }
232
+
233
+ /** 用户 tag 分组 + 系统集合组 + origin 映射。 */
234
+ async groups(): Promise<import('../protocol.ts').GroupsResponse> {
235
+ const response = await fetchWithTimeout(SKILL_HUB_API.groups)
236
+ return readJson<import('../protocol.ts').GroupsResponse>(response)
237
+ }
238
+
239
+ /** 新建(无 id)或重命名(带 id)一个 tag 分组;返回全部 tag。 */
240
+ async saveTag(payload: import('../protocol.ts').TagSaveRequest): Promise<import('../protocol.ts').SkillTag[]> {
241
+ const response = await fetchWithTimeout(SKILL_HUB_API.tag, {
242
+ method: 'POST',
243
+ headers: { 'content-type': 'application/json' },
244
+ body: JSON.stringify(payload),
245
+ })
246
+ const body = await readJson<import('../protocol.ts').TagSaveResponse>(response)
247
+ return body.tags
248
+ }
249
+
250
+ /** 删除一个 tag 分组;返回全部 tag。 */
251
+ async deleteTag(id: string): Promise<import('../protocol.ts').SkillTag[]> {
252
+ const response = await fetchWithTimeout(SKILL_HUB_API.tagDelete, {
253
+ method: 'POST',
254
+ headers: { 'content-type': 'application/json' },
255
+ body: JSON.stringify({ id } satisfies import('../protocol.ts').TagDeleteRequest),
256
+ })
257
+ const body = await readJson<import('../protocol.ts').TagDeleteResponse>(response)
258
+ return body.tags
259
+ }
260
+
261
+ /** 直接设置某 tag 的完整成员列表;返回全部 tag。 */
262
+ async setTagMembers(id: string, skillNames: string[]): Promise<import('../protocol.ts').SkillTag[]> {
263
+ const response = await fetchWithTimeout(SKILL_HUB_API.tagMembers, {
264
+ method: 'POST',
265
+ headers: { 'content-type': 'application/json' },
266
+ body: JSON.stringify({ id, skillNames } satisfies import('../protocol.ts').TagMembersRequest),
267
+ })
268
+ const body = await readJson<import('../protocol.ts').TagMembersResponse>(response)
269
+ return body.tags
270
+ }
271
+
272
+ /** 来源列表 + 派生 origin 映射 + 集合组 + 回收站。 */
273
+ async sources(): Promise<SourcesResponse> {
274
+ const response = await fetchWithTimeout(SKILL_HUB_API.sources)
275
+ return readJson<SourcesResponse>(response)
276
+ }
277
+
278
+ /** 检查指定(或全部)来源的上游更新。 */
279
+ async checkSources(repo?: string): Promise<SourceCheckResponse> {
280
+ const payload: SourceCheckRequest = repo !== undefined ? { repo } : {}
281
+ const response = await fetchWithTimeout(SKILL_HUB_API.sourceCheck, {
282
+ method: 'POST',
283
+ headers: { 'content-type': 'application/json' },
284
+ body: JSON.stringify(payload),
285
+ })
286
+ return readJson<SourceCheckResponse>(response)
287
+ }
288
+
289
+ /** 同步一个来源的所选(或全部)技能到上游最新版本。 */
290
+ async syncSource(repo: string, skills?: string[]): Promise<SourceSyncResponse> {
291
+ const payload: SourceSyncRequest = skills !== undefined ? { repo, skills } : { repo }
292
+ const response = await fetchWithTimeout(SKILL_HUB_API.sourceSync, {
293
+ method: 'POST',
294
+ headers: { 'content-type': 'application/json' },
295
+ body: JSON.stringify(payload),
296
+ })
297
+ return readJson<SourceSyncResponse>(response)
298
+ }
299
+
300
+ /** 跟进上游删除:把所选技能移入回收站。 */
301
+ async confirmDeleteSource(repo: string, skills: string[]): Promise<SourceDeleteResponse> {
302
+ const payload: SourceDeleteRequest = { repo, skills }
303
+ const response = await fetchWithTimeout(SKILL_HUB_API.sourceDelete, {
304
+ method: 'POST',
305
+ headers: { 'content-type': 'application/json' },
306
+ body: JSON.stringify(payload),
307
+ })
308
+ return readJson<SourceDeleteResponse>(response)
309
+ }
310
+
311
+ /** 从回收站恢复一个技能。 */
312
+ async restoreSource(name: string): Promise<SourceRestoreResponse> {
313
+ const payload: SourceRestoreRequest = { name }
314
+ const response = await fetchWithTimeout(SKILL_HUB_API.sourceRestore, {
315
+ method: 'POST',
316
+ headers: { 'content-type': 'application/json' },
317
+ body: JSON.stringify(payload),
318
+ })
319
+ return readJson<SourceRestoreResponse>(response)
320
+ }
321
+
322
+ /** Permanently delete every skill currently in the trash. */
323
+ async clearTrash(): Promise<SourceTrashClearResponse> {
324
+ const response = await fetchWithTimeout(SKILL_HUB_API.sourceTrashClear, { method: 'POST' })
325
+ return readJson<SourceTrashClearResponse>(response)
326
+ }
327
+ }
@@ -0,0 +1,5 @@
1
+ declare module '*.module.css' {
2
+ const classes: Record<string, string>
3
+ export default classes
4
+ }
5
+