@boostkit-dsh/skill-market 0.3.1

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/index.js ADDED
@@ -0,0 +1,323 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { dirname, join, resolve } from 'node:path'
3
+
4
+ import { DEFAULT_CATALOG_PACKAGE, DEFAULT_REGISTRIES, downloadSkillPackage, loadRemoteCatalog } from './catalog.js'
5
+ import { createSkillInstaller } from './installer.js'
6
+
7
+ export const name = 'boostkit-skill-market'
8
+ export const inject = ['authManagement']
9
+
10
+ const API_PREFIX = '/boostkit-skill-market'
11
+ const REQUEST_BYTES = 4096
12
+ const NETWORK_TIMEOUT_MS = 20_000
13
+ const BOOSTKIT_SKILLS_REPOSITORY = 'https://gitcode.com/boostkit/skills'
14
+
15
+ function sameOrigin(request) {
16
+ const origin = request.headers.origin
17
+ const host = request.headers.host
18
+ if (origin === undefined || host === undefined) return false
19
+ const remote = request.socket?.remoteAddress
20
+ const loopback = remote === '::1' || remote === '127.0.0.1' || (typeof remote === 'string' && remote.startsWith('127.')) || (typeof remote === 'string' && remote.startsWith('::ffff:127.'))
21
+ if (!loopback) return false
22
+ try { return new URL(origin).host === host } catch { return false }
23
+ }
24
+
25
+ function sendJson(response, status, payload) {
26
+ if (response.destroyed || response.writableEnded) return
27
+ response.writeHead(status, { 'cache-control': 'no-store', 'content-type': 'application/json; charset=utf-8' })
28
+ response.end(JSON.stringify(payload))
29
+ }
30
+
31
+ function sendProtectedError(response, error) {
32
+ if (error?.code === 'AUTH_REQUIRED') return sendJson(response, 401, { ok: false, error: { code: 'AUTH_REQUIRED', message: String(error.message) } })
33
+ if (error?.code === 'AUTH_UNAVAILABLE') return sendJson(response, 503, { ok: false, error: { code: 'AUTH_UNAVAILABLE', message: String(error.message) } })
34
+ if (error?.code === 'busy') return sendJson(response, 409, { ok: false, agentsBusy: true, runningAgents: error.agents ?? [], error: String(error.message) })
35
+ sendJson(response, 400, { ok: false, error: String(error?.message ?? error) })
36
+ }
37
+
38
+ async function readJsonBody(request) {
39
+ const chunks = []
40
+ let size = 0
41
+ for await (const chunk of request) {
42
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
43
+ size += buffer.length
44
+ if (size > REQUEST_BYTES) throw new Error('request body too large')
45
+ chunks.push(buffer)
46
+ }
47
+ const text = Buffer.concat(chunks).toString('utf8').trim()
48
+ return text === '' ? {} : JSON.parse(text)
49
+ }
50
+
51
+ function requestedName(body) {
52
+ return typeof body?.name === 'string' && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(body.name) ? body.name : null
53
+ }
54
+
55
+ function requestedWorkspaceId(value) {
56
+ return typeof value === 'string' && value.length > 0 && value.length <= 200 ? value : null
57
+ }
58
+
59
+ /** Match the filesystem Skill provider's upward `.git` project-root discovery. */
60
+ function dshProjectRoot(cwd) {
61
+ const fallback = resolve(cwd)
62
+ let current = fallback
63
+ while (true) {
64
+ if (existsSync(join(current, '.git'))) return current
65
+ const parent = dirname(current)
66
+ if (parent === current) return fallback
67
+ current = parent
68
+ }
69
+ }
70
+
71
+ /** Resolve a client-selected, Host-registered workspace to the root DSH scans. */
72
+ function workspaceProjectRoot(ctx, config, workspaceId) {
73
+ let registry
74
+ try { registry = ctx.get?.('workspaceRegistry') } catch { registry = undefined }
75
+ if (workspaceId !== null) {
76
+ const workspace = registry?.get?.(workspaceId)
77
+ if (workspace === undefined || typeof workspace.path !== 'string') throw new Error('所选 DSH 工作区不存在,请重新选择。')
78
+ return dshProjectRoot(workspace.path)
79
+ }
80
+ if (config.projectRoot !== undefined) return dshProjectRoot(resolve(config.projectRoot))
81
+ const workspaces = registry?.list?.()
82
+ if (Array.isArray(workspaces) && workspaces.length === 1 && typeof workspaces[0]?.path === 'string') return dshProjectRoot(workspaces[0].path)
83
+ throw new Error('请选择 Skill 的安装工作区。')
84
+ }
85
+
86
+ function runningAgentIds(ctx) {
87
+ let agents
88
+ try { agents = ctx.get?.('agents') } catch { return [] }
89
+ if (agents === undefined) return []
90
+ let listed
91
+ try { listed = agents.list() } catch { return [] }
92
+ if (!Array.isArray(listed)) return []
93
+ return [...new Set(listed.filter((agent) => agent?.status === 'running').map((agent) => typeof agent.id === 'string' && agent.id !== '' ? agent.id : 'agent'))]
94
+ }
95
+
96
+ function mergeState(catalog, installed) {
97
+ const local = new Map(installed.map((entry) => [entry.name, entry]))
98
+ const result = catalog.skills.map((entry) => {
99
+ const current = local.get(entry.name)
100
+ local.delete(entry.name)
101
+ return {
102
+ ...entry,
103
+ installed: current !== undefined,
104
+ present: current?.present ?? false,
105
+ enabled: current?.enabled ?? true,
106
+ currentVersion: current?.version ?? '',
107
+ updateAvailable: current !== undefined && (current.version !== entry.version || current.integrity !== entry.integrity),
108
+ managed: current !== undefined,
109
+ sourceUrl: `${entry.source.repository.replace(/\/$/, '')}/tree/${entry.commit}/${entry.source.path}`,
110
+ }
111
+ })
112
+ for (const current of local.values()) {
113
+ result.push({
114
+ ...current,
115
+ displayName: { zh: current.name },
116
+ description: '此 Skill 已安装,但当前 npm catalog 中不存在。',
117
+ category: 'unlisted',
118
+ tags: [],
119
+ installed: true,
120
+ enabled: current.enabled ?? true,
121
+ currentVersion: current.version,
122
+ updateAvailable: false,
123
+ managed: true,
124
+ listed: false,
125
+ })
126
+ }
127
+ return result.sort((left, right) => left.name.localeCompare(right.name))
128
+ }
129
+
130
+ /** Create the host service used by the BoostKit Skill Market page. */
131
+ export function createSkillMarketService(options = {}) {
132
+ const defaultProjectRoot = resolve(options.projectRoot ?? process.cwd())
133
+ const installers = new Map()
134
+ const fetchImpl = options.fetchImpl ?? fetch
135
+ const catalogPackage = options.catalogPackage ?? DEFAULT_CATALOG_PACKAGE
136
+ const registries = options.registries ?? DEFAULT_REGISTRIES
137
+ const timeoutMs = options.timeoutMs ?? NETWORK_TIMEOUT_MS
138
+ let cachedCatalog = null
139
+ let activeOperation = null
140
+ let disposed = false
141
+ const controllers = new Set()
142
+
143
+ function installerFor(projectRoot = defaultProjectRoot) {
144
+ const root = resolve(projectRoot)
145
+ let installer = installers.get(root)
146
+ if (installer === undefined) {
147
+ installer = createSkillInstaller({ projectRoot: root, now: options.now })
148
+ installers.set(root, installer)
149
+ }
150
+ return installer
151
+ }
152
+
153
+ async function network(task) {
154
+ if (disposed) throw new Error('BoostKit Skill 市场服务已释放')
155
+ const controller = new AbortController()
156
+ const timer = setTimeout(() => controller.abort(new Error('npm 请求超时')), timeoutMs)
157
+ controllers.add(controller)
158
+ try { return await task(controller.signal) } finally { clearTimeout(timer); controllers.delete(controller) }
159
+ }
160
+
161
+ async function catalog(refresh = false) {
162
+ if (!refresh && cachedCatalog !== null) return cachedCatalog
163
+ const loaded = await network((signal) => loadRemoteCatalog({ fetchImpl, packageName: catalogPackage, registries, signal }))
164
+ cachedCatalog = loaded
165
+ return loaded
166
+ }
167
+
168
+ async function state(refresh = false, projectRoot = defaultProjectRoot) {
169
+ const installer = installerFor(projectRoot)
170
+ const installed = installer.list()
171
+ try {
172
+ const remote = await catalog(refresh)
173
+ return {
174
+ connected: true,
175
+ release: remote.release,
176
+ catalogPackage,
177
+ repositoryUrl: BOOSTKIT_SKILLS_REPOSITORY,
178
+ skills: mergeState(remote, installed),
179
+ }
180
+ } catch (error) {
181
+ return {
182
+ connected: false,
183
+ error: String(error?.message ?? error),
184
+ release: '',
185
+ catalogPackage,
186
+ repositoryUrl: BOOSTKIT_SKILLS_REPOSITORY,
187
+ skills: mergeState({ skills: [] }, installed),
188
+ }
189
+ }
190
+ }
191
+
192
+ async function mutate(action, skillName, projectRoot = defaultProjectRoot) {
193
+ if (disposed) throw new Error('BoostKit Skill 市场服务已释放')
194
+ if (activeOperation !== null) throw Object.assign(new Error(`正在处理 ${activeOperation}`), { code: 'busy' })
195
+ activeOperation = `${action}:${skillName}`
196
+ try {
197
+ const installer = installerFor(projectRoot)
198
+ if (action === 'remove') return { action, skill: installer.remove(skillName) }
199
+ if (action === 'enable' || action === 'disable') return { action, skill: installer.setEnabled(skillName, action === 'enable') }
200
+ if (action !== 'install' && action !== 'update') throw new Error(`Skill 操作非法:${action}`)
201
+ const remote = await catalog(true)
202
+ const entry = remote.skills.find((skill) => skill.name === skillName)
203
+ if (!entry) throw new Error(`npm catalog 中不存在 ${skillName}`)
204
+ const tarball = await network((signal) => downloadSkillPackage(entry, { fetchImpl, registries, signal }))
205
+ const skill = installer.install(entry, tarball, action)
206
+ return { action, skill }
207
+ } finally {
208
+ activeOperation = null
209
+ }
210
+ }
211
+
212
+ return {
213
+ projectRoot: defaultProjectRoot,
214
+ state,
215
+ mutate,
216
+ detail(name, projectRoot = defaultProjectRoot) { return installerFor(projectRoot).readSkill(name) },
217
+ refresh() { cachedCatalog = null },
218
+ dispose() {
219
+ disposed = true
220
+ cachedCatalog = null
221
+ for (const controller of controllers) controller.abort()
222
+ controllers.clear()
223
+ },
224
+ }
225
+ }
226
+
227
+ async function handleMutation(ctx, auth, service, config, action, request, response) {
228
+ if (request.method !== 'POST') {
229
+ response.writeHead(405, { allow: 'POST' })
230
+ response.end()
231
+ return
232
+ }
233
+ if (!sameOrigin(request)) return sendJson(response, 403, { ok: false, error: 'same-origin required' })
234
+ await auth.requireActor()
235
+ const body = await readJsonBody(request)
236
+ const skillName = requestedName(body)
237
+ if (skillName === null) return sendJson(response, 400, { ok: false, error: 'invalid Skill name' })
238
+ try {
239
+ const result = await auth.executeAudited({
240
+ action: `skill.${action}`,
241
+ target: { type: 'skill', id: skillName },
242
+ workspaceId: requestedWorkspaceId(body.workspaceId) ?? undefined,
243
+ }, () => {
244
+ const agents = runningAgentIds(ctx)
245
+ if (agents.length > 0) throw Object.assign(new Error(`有 Agent 正在运行(${agents.join(', ')}),请等待任务停止后重试。`), { code: 'busy', agents })
246
+ const projectRoot = workspaceProjectRoot(ctx, config, requestedWorkspaceId(body.workspaceId))
247
+ return service.mutate(action, skillName, projectRoot)
248
+ })
249
+ sendJson(response, 200, { ok: true, ...result })
250
+ } catch (error) {
251
+ sendProtectedError(response, error)
252
+ }
253
+ }
254
+
255
+ /** Register the Skill market service, HTTP endpoints and lifecycle cleanup. */
256
+ export function apply(ctx, config = {}) {
257
+ const auth = ctx.get?.('authManagement') ?? ctx.authManagement
258
+ if (auth === undefined) throw new Error('boostkit-skill-market 需要 authManagement 服务')
259
+ const service = createSkillMarketService({
260
+ projectRoot: config.projectRoot === undefined ? process.cwd() : resolve(config.projectRoot),
261
+ catalogPackage: config.catalogPackage,
262
+ registries: config.registries,
263
+ fetchImpl: config.fetchImpl,
264
+ })
265
+ ctx.provide('boostkitSkillMarket', service)
266
+ ctx.effect?.(() => () => service.dispose(), 'boostkit-skill-market.dispose')
267
+ ctx.inject(['webServer'], (host) => {
268
+ host.effect(() => {
269
+ const routes = [
270
+ {
271
+ kind: 'exact',
272
+ path: `${API_PREFIX}/catalog`,
273
+ handler: (request, response) => {
274
+ if (request.method !== 'GET') { response.writeHead(405, { allow: 'GET' }); response.end(); return }
275
+ void auth.requireActor().then(() => {
276
+ let url
277
+ try { url = new URL(request.url ?? '', 'http://localhost') } catch { sendJson(response, 400, { connected: false, error: 'invalid request URL', skills: [] }); return null }
278
+ let projectRoot
279
+ try { projectRoot = workspaceProjectRoot(ctx, config, requestedWorkspaceId(url.searchParams.get('workspaceId'))) } catch (error) { sendJson(response, 400, { connected: false, error: String(error?.message ?? error), skills: [] }); return null }
280
+ return service.state(url.searchParams.has('refresh'), projectRoot).then((value) => sendJson(response, 200, value))
281
+ }).catch((error) => sendProtectedError(response, error))
282
+ },
283
+ },
284
+ {
285
+ kind: 'exact',
286
+ path: `${API_PREFIX}/detail`,
287
+ handler: (request, response) => {
288
+ if (request.method !== 'GET') { response.writeHead(405, { allow: 'GET' }); response.end(); return }
289
+ void auth.requireActor().then(() => {
290
+ let skillName = null
291
+ let workspaceId = null
292
+ try {
293
+ const url = new URL(request.url ?? '', 'http://localhost')
294
+ skillName = requestedName({ name: url.searchParams.get('name') })
295
+ workspaceId = requestedWorkspaceId(url.searchParams.get('workspaceId'))
296
+ } catch { /* invalid request URL */ }
297
+ if (skillName === null) { sendJson(response, 400, { ok: false, error: 'invalid Skill name' }); return }
298
+ const projectRoot = workspaceProjectRoot(ctx, config, workspaceId)
299
+ sendJson(response, 200, { ok: true, name: skillName, ...service.detail(skillName, projectRoot) })
300
+ }).catch((error) => sendProtectedError(response, error))
301
+ },
302
+ },
303
+ ...['install', 'update', 'remove', 'enable', 'disable'].map((action) => ({
304
+ kind: 'exact',
305
+ path: `${API_PREFIX}/${action}`,
306
+ handler: (request, response) => { void handleMutation(ctx, auth, service, config, action, request, response).catch((error) => sendProtectedError(response, error)) },
307
+ })),
308
+ ]
309
+ const disposers = routes.map((route) => host.webServer.register(route))
310
+ return () => { for (const dispose of disposers) dispose() }
311
+ }, 'boostkit-skill-market.http')
312
+ })
313
+ }
314
+
315
+ export const __testing = {
316
+ mergeState,
317
+ dshProjectRoot,
318
+ requestedName,
319
+ requestedWorkspaceId,
320
+ runningAgentIds,
321
+ sameOrigin,
322
+ workspaceProjectRoot,
323
+ }
package/installer.js ADDED
@@ -0,0 +1,224 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
3
+ import { join, relative, resolve, sep } from 'node:path'
4
+
5
+ import { extractPackage } from './archive.js'
6
+
7
+ const LOCK_VERSION = 1
8
+ const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
9
+ const MAX_SKILL_FILE_BYTES = 1024 * 1024
10
+
11
+ function displayPath(projectRoot, target) {
12
+ return relative(projectRoot, target).split(sep).join('/')
13
+ }
14
+
15
+ function parseJson(file) {
16
+ return JSON.parse(readFileSync(file, 'utf8'))
17
+ }
18
+
19
+ function emptyLock() {
20
+ return { version: LOCK_VERSION, skills: {} }
21
+ }
22
+
23
+ function validateLock(value) {
24
+ if (value?.version !== LOCK_VERSION || value.skills === null || typeof value.skills !== 'object' || Array.isArray(value.skills)) throw new Error('BoostKit Skill lock 文件格式非法')
25
+ for (const [name, entry] of Object.entries(value.skills)) {
26
+ if (!SKILL_NAME_RE.test(name) || entry?.name !== name || typeof entry.package !== 'string' || typeof entry.version !== 'string' || typeof entry.commit !== 'string' || typeof entry.integrity !== 'string' || typeof entry.path !== 'string' || (entry.enabled !== undefined && typeof entry.enabled !== 'boolean')) throw new Error(`BoostKit Skill lock 条目非法:${name}`)
27
+ }
28
+ return value
29
+ }
30
+
31
+ function frontmatterName(text) {
32
+ const normalized = text.replace(/\r\n/g, '\n')
33
+ const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(normalized)
34
+ if (!match) throw new Error('SKILL.md 缺少 YAML frontmatter')
35
+ const name = /^name:\s*['"]?([^'"\n]+?)['"]?\s*$/m.exec(match[1])?.[1]?.trim()
36
+ if (!name) throw new Error('SKILL.md frontmatter 缺少 name')
37
+ return name
38
+ }
39
+
40
+ function writeLockAtomic(file, value) {
41
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`
42
+ const backup = `${file}.${process.pid}.${randomUUID()}.bak`
43
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 })
44
+ let backedUp = false
45
+ try {
46
+ if (existsSync(file)) {
47
+ renameSync(file, backup)
48
+ backedUp = true
49
+ }
50
+ renameSync(temporary, file)
51
+ } catch (error) {
52
+ rmSync(temporary, { force: true })
53
+ if (backedUp && !existsSync(file)) renameSync(backup, file)
54
+ throw error
55
+ }
56
+ if (backedUp) {
57
+ try { rmSync(backup, { force: true }) } catch { /* committed lock remains authoritative */ }
58
+ }
59
+ }
60
+
61
+ /** Create a project-scoped, no-script BoostKit Skill installer. */
62
+ export function createSkillInstaller(options = {}) {
63
+ const projectRoot = resolve(options.projectRoot ?? process.cwd())
64
+ const dshRoot = join(projectRoot, '.dsh')
65
+ const skillsRoot = join(dshRoot, 'skills')
66
+ const disabledRoot = join(skillsRoot, '.disabled')
67
+ const lockFile = join(skillsRoot, '.boostkit-lock.json')
68
+ const now = options.now ?? (() => new Date())
69
+
70
+ function skillPath(name, enabled) {
71
+ return enabled ? join(skillsRoot, name) : join(disabledRoot, name)
72
+ }
73
+
74
+ function readLock() {
75
+ if (!existsSync(lockFile)) return emptyLock()
76
+ return validateLock(parseJson(lockFile))
77
+ }
78
+
79
+ function list() {
80
+ const lock = readLock()
81
+ return Object.values(lock.skills).sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
82
+ const enabled = entry.enabled !== false
83
+ return {
84
+ ...entry,
85
+ enabled,
86
+ present: existsSync(skillPath(entry.name, enabled)),
87
+ }
88
+ })
89
+ }
90
+
91
+ function install(entry, tarball, action = 'install') {
92
+ if (action !== 'install' && action !== 'update') throw new Error(`Skill 操作非法:${action}`)
93
+ if (!SKILL_NAME_RE.test(entry?.name ?? '')) throw new Error('Skill 名称非法')
94
+ mkdirSync(skillsRoot, { recursive: true })
95
+ const lock = readLock()
96
+ const current = lock.skills[entry.name]
97
+ const enabled = current?.enabled !== false
98
+ const target = skillPath(entry.name, enabled)
99
+ if (action === 'install' && current !== undefined) throw new Error(`${entry.name} 已安装`)
100
+ if (action === 'update' && current === undefined) throw new Error(`${entry.name} 尚未安装`)
101
+ if (action === 'update' && current.package !== entry.package) throw new Error(`${entry.name} 的 npm 包身份发生变化,拒绝自动更新`)
102
+ if (current === undefined && existsSync(target)) throw new Error(`${entry.name} 是未由 BoostKit Skill 市场管理的本地 Skill,禁止覆盖`)
103
+ if (action === 'update' && current.version === entry.version && current.integrity === entry.integrity && existsSync(target)) throw new Error(`${entry.name} 已是 catalog 版本`)
104
+
105
+ const staging = mkdtempSync(join(dshRoot, `.boostkit-stage-${entry.name}-`))
106
+ const backup = join(dshRoot, `.boostkit-backup-${entry.name}-${randomUUID()}`)
107
+ let targetBackedUp = false
108
+ let targetInstalled = false
109
+ try {
110
+ const files = extractPackage(tarball, staging)
111
+ const skillFile = join(staging, 'SKILL.md')
112
+ if (!existsSync(skillFile)) throw new Error(`${entry.package} npm 包根目录缺少 SKILL.md`)
113
+ if (frontmatterName(readFileSync(skillFile, 'utf8')) !== entry.name) throw new Error(`${entry.package} 的 SKILL.md name 与 catalog 不一致`)
114
+ if (existsSync(target)) {
115
+ renameSync(target, backup)
116
+ targetBackedUp = true
117
+ }
118
+ renameSync(staging, target)
119
+ targetInstalled = true
120
+ const installed = {
121
+ name: entry.name,
122
+ package: entry.package,
123
+ version: entry.version,
124
+ commit: entry.commit,
125
+ integrity: entry.integrity,
126
+ path: displayPath(projectRoot, target),
127
+ enabled,
128
+ installedAt: now().toISOString(),
129
+ source: entry.source,
130
+ files: files.sort(),
131
+ }
132
+ writeLockAtomic(lockFile, { version: LOCK_VERSION, skills: { ...lock.skills, [entry.name]: installed } })
133
+ if (targetBackedUp) {
134
+ try { rmSync(backup, { recursive: true, force: true }) } catch { /* committed update remains valid */ }
135
+ }
136
+ return installed
137
+ } catch (error) {
138
+ if (targetInstalled) rmSync(target, { recursive: true, force: true })
139
+ if (targetBackedUp && existsSync(backup)) renameSync(backup, target)
140
+ throw error
141
+ } finally {
142
+ try { rmSync(staging, { recursive: true, force: true }) } catch { /* best-effort staging cleanup */ }
143
+ if (!targetBackedUp) {
144
+ try { rmSync(backup, { recursive: true, force: true }) } catch { /* path normally does not exist */ }
145
+ }
146
+ }
147
+ }
148
+
149
+ function remove(name) {
150
+ if (!SKILL_NAME_RE.test(name ?? '')) throw new Error('Skill 名称非法')
151
+ const lock = readLock()
152
+ const current = lock.skills[name]
153
+ if (current === undefined) throw new Error(`${name} 未由 BoostKit Skill 市场管理`)
154
+ const target = skillPath(name, current.enabled !== false)
155
+ const backup = join(dshRoot, `.boostkit-remove-${name}-${randomUUID()}`)
156
+ let moved = false
157
+ try {
158
+ if (existsSync(target)) {
159
+ renameSync(target, backup)
160
+ moved = true
161
+ }
162
+ const skills = { ...lock.skills }
163
+ delete skills[name]
164
+ writeLockAtomic(lockFile, { version: LOCK_VERSION, skills })
165
+ if (moved) {
166
+ try { rmSync(backup, { recursive: true, force: true }) } catch { /* committed removal remains valid */ }
167
+ }
168
+ return current
169
+ } catch (error) {
170
+ if (moved && existsSync(backup) && !existsSync(target)) renameSync(backup, target)
171
+ throw error
172
+ }
173
+ }
174
+
175
+ function setEnabled(name, enabled) {
176
+ if (!SKILL_NAME_RE.test(name ?? '')) throw new Error('Skill 名称非法')
177
+ if (typeof enabled !== 'boolean') throw new Error('Skill 启用状态非法')
178
+ const lock = readLock()
179
+ const current = lock.skills[name]
180
+ if (current === undefined) throw new Error(`${name} 未由 BoostKit Skill 市场管理`)
181
+ const wasEnabled = current.enabled !== false
182
+ if (wasEnabled === enabled) return { ...current, enabled: wasEnabled }
183
+ const source = skillPath(name, wasEnabled)
184
+ const target = skillPath(name, enabled)
185
+ if (!existsSync(source)) throw new Error(`${name} 的 Skill 文件缺失`)
186
+ if (existsSync(target)) throw new Error(`${name} 的目标目录已存在,无法切换状态`)
187
+ mkdirSync(enabled ? skillsRoot : disabledRoot, { recursive: true })
188
+ renameSync(source, target)
189
+ try {
190
+ const updated = { ...current, enabled, path: displayPath(projectRoot, target) }
191
+ writeLockAtomic(lockFile, { version: LOCK_VERSION, skills: { ...lock.skills, [name]: updated } })
192
+ return updated
193
+ } catch (error) {
194
+ renameSync(target, source)
195
+ throw error
196
+ }
197
+ }
198
+
199
+ function readSkill(name) {
200
+ if (!SKILL_NAME_RE.test(name ?? '')) throw new Error('Skill 名称非法')
201
+ const lock = readLock()
202
+ const current = lock.skills[name]
203
+ if (current === undefined) throw new Error(`${name} 未由 BoostKit Skill 市场管理`)
204
+ const enabled = current.enabled !== false
205
+ const file = join(skillPath(name, enabled), 'SKILL.md')
206
+ if (!existsSync(file)) throw new Error(`${name} 的 SKILL.md 文件缺失`)
207
+ if (statSync(file).size > MAX_SKILL_FILE_BYTES) throw new Error(`${name} 的 SKILL.md 超过 1 MiB 展示上限`)
208
+ return { content: readFileSync(file, 'utf8'), enabled }
209
+ }
210
+
211
+ return {
212
+ projectRoot,
213
+ skillsRoot,
214
+ lockFile,
215
+ install,
216
+ list,
217
+ readSkill,
218
+ readLock,
219
+ remove,
220
+ setEnabled,
221
+ }
222
+ }
223
+
224
+ export const skillLockVersion = LOCK_VERSION
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@boostkit-dsh/skill-market",
3
+ "version": "0.3.1",
4
+ "type": "module",
5
+ "description": "在 DSH 中浏览、安装、启停、更新和卸载 BoostKit 项目级 Skill",
6
+ "main": "index.js",
7
+ "files": [
8
+ "CHANGELOG.md",
9
+ "README.md",
10
+ "archive.js",
11
+ "catalog.js",
12
+ "client.js",
13
+ "cordis.patch.yml",
14
+ "index.js",
15
+ "installer.js"
16
+ ],
17
+ "license": "UNLICENSED",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://gitcode.com/lujinxing/kunpeng-dsh-demo.git",
21
+ "directory": "plugins/skill-market"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "registry": "https://registry.npmjs.org"
26
+ },
27
+ "exports": {
28
+ ".": "./index.js",
29
+ "./client": "./client.js",
30
+ "./package.json": "./package.json"
31
+ },
32
+ "scripts": {
33
+ "test": "node --test test/*.test.mjs"
34
+ },
35
+ "dependencies": {
36
+ "@boostkit-dsh/auth-management": "0.1.1"
37
+ },
38
+ "dsh": {
39
+ "bundle": {
40
+ "patch": "./cordis.patch.yml"
41
+ },
42
+ "client": {
43
+ "platform": "web",
44
+ "inject": [
45
+ "@deepseek-ai/dsh-api-workspace-controller",
46
+ "@deepseek-ai/dsh-client-locale",
47
+ "@deepseek-ai/dsh-client-ui-layout",
48
+ "@deepseek-ai/dsh-client-ui-sidebar"
49
+ ]
50
+ },
51
+ "market": {
52
+ "displayName": {
53
+ "zh": "BoostKit Skill 市场",
54
+ "en": "BoostKit Skill Market"
55
+ },
56
+ "category": "kunpeng-rd",
57
+ "maintainers": [
58
+ "@lujinxing"
59
+ ],
60
+ "compatibility": {
61
+ "dsh": "0.1.7-alpha.1"
62
+ },
63
+ "allowedBuilds": []
64
+ }
65
+ }
66
+ }