dsh-skill-importer 0.2.3 → 0.2.5
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 +20 -8
- package/README.zh.md +19 -7
- package/lib/client.js +874 -110
- package/lib/index.js +199 -41
- package/lib/types/client/SkillImporterSection.d.ts +1 -1
- package/lib/types/client/SkillsPicker.d.ts +1 -1
- package/lib/types/client/effectiveSkills.d.ts +3 -0
- package/lib/types/client/index.d.ts +12 -4
- package/lib/types/client/locales.d.ts +80 -2
- package/lib/types/index.d.ts +1 -0
- package/lib/types/server.d.ts +6 -2
- package/lib/types/types.d.ts +23 -3
- package/lib/types/update.d.ts +6 -0
- package/package.json +4 -1
- package/src/server.ts +85 -37
- package/src/types.ts +25 -3
- package/tests/batch-import.test.mjs +5 -5
- package/tests/effective-skills.test.mjs +25 -0
- package/tests/skill-list.test.mjs +50 -0
- package/tests/update.test.mjs +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-importer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Web UI plugin for DeepSeek Harness: import skills from local Markdown files or URLs into the skill roots the harness discovers automatically",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -47,6 +47,9 @@
|
|
|
47
47
|
"build": "rm -rf lib && tsc -p tsconfig.json && tsdown",
|
|
48
48
|
"test:batch": "node --experimental-strip-types tests/batch-import.test.mjs",
|
|
49
49
|
"test:atomic-delete": "node --experimental-strip-types tests/atomic-skill-delete.test.mjs",
|
|
50
|
+
"test:update": "node --experimental-strip-types tests/update.test.mjs",
|
|
51
|
+
"test:list": "node --experimental-strip-types tests/skill-list.test.mjs",
|
|
52
|
+
"test:effective": "node --experimental-strip-types tests/effective-skills.test.mjs",
|
|
50
53
|
"watch": "tsdown --watch"
|
|
51
54
|
},
|
|
52
55
|
"license": "MIT",
|
package/src/server.ts
CHANGED
|
@@ -18,7 +18,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
|
18
18
|
import { isValidSkillName, parseSkillFile } from './frontmatter.ts'
|
|
19
19
|
import type {
|
|
20
20
|
BatchCommitEntry, BatchScanEntry, BatchScanRequest,
|
|
21
|
-
ImportRequest, ImportTarget, ImportUrlRequest, SkillListEntry,
|
|
21
|
+
ImportRequest, ImportTarget, ImportUrlRequest, SkillListEntry, SkillListIssue,
|
|
22
22
|
} from './types.ts'
|
|
23
23
|
|
|
24
24
|
/** Hard cap for one imported skill body (matches the client preview limit). */
|
|
@@ -44,18 +44,31 @@ export function dshHomeDir(): string {
|
|
|
44
44
|
/** Absolute skill root for one target under one workspace. */
|
|
45
45
|
export function skillRoot(target: ImportTarget, workspacePath: string): string {
|
|
46
46
|
switch (target) {
|
|
47
|
-
case '
|
|
48
|
-
return join(
|
|
47
|
+
case 'project-dsh':
|
|
48
|
+
return join(workspacePath, '.dsh', 'skills')
|
|
49
49
|
case 'project-agents':
|
|
50
50
|
return join(workspacePath, '.agents', 'skills')
|
|
51
|
+
case 'user-dsh':
|
|
52
|
+
return join(dshHomeDir(), 'skills')
|
|
53
|
+
case 'user-agents':
|
|
54
|
+
return join(process.env.DSH_AGENTS_HOME ?? join(homedir(), '.agents'), 'skills')
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/** Discovery rank per root (lower wins), mirroring dsh-skill-filesystem. */
|
|
55
|
-
const ROOT_RANK: Record<ImportTarget, number> = {
|
|
59
|
+
const ROOT_RANK: Record<ImportTarget, number> = {
|
|
60
|
+
'project-dsh': 100,
|
|
61
|
+
'project-agents': 200,
|
|
62
|
+
'user-dsh': 400,
|
|
63
|
+
'user-agents': 500,
|
|
64
|
+
}
|
|
56
65
|
|
|
57
|
-
/**
|
|
58
|
-
const SCAN_ORDER: readonly ImportTarget[] = ['project-agents', 'user']
|
|
66
|
+
/** Official default-root order (custom rank 300 is not a stable import destination). */
|
|
67
|
+
const SCAN_ORDER: readonly ImportTarget[] = ['project-dsh', 'project-agents', 'user-dsh', 'user-agents']
|
|
68
|
+
|
|
69
|
+
export function isProjectTarget(target: ImportTarget): boolean {
|
|
70
|
+
return target === 'project-dsh' || target === 'project-agents'
|
|
71
|
+
}
|
|
59
72
|
|
|
60
73
|
/**
|
|
61
74
|
* Quote one frontmatter string value when plain YAML would mis-parse it:
|
|
@@ -113,7 +126,7 @@ export function normalizeSkillText(text: string): string {
|
|
|
113
126
|
export function writeSkillFile(name: string, target: ImportTarget, content: string, workspacePath?: string): string {
|
|
114
127
|
if (!KEBAB_CASE.test(name)) throw new Error('技能名必须是 kebab-case(小写字母、数字、短横线)')
|
|
115
128
|
if (Buffer.byteLength(content, 'utf8') > MAX_CONTENT_BYTES) throw new Error(`内容超过 ${MAX_CONTENT_BYTES / 1024} KB`)
|
|
116
|
-
if (target
|
|
129
|
+
if (isProjectTarget(target) && workspacePath === undefined) {
|
|
117
130
|
throw new Error('项目目标需要 workspacePath(当前工作区路径)')
|
|
118
131
|
}
|
|
119
132
|
const normalized = normalizeSkillText(content)
|
|
@@ -127,30 +140,55 @@ export function writeSkillFile(name: string, target: ImportTarget, content: stri
|
|
|
127
140
|
}
|
|
128
141
|
|
|
129
142
|
/** Scan one skill root and fold its skills into the name-keyed map (rank-aware). */
|
|
130
|
-
function scanRoot(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
function scanRoot(
|
|
144
|
+
root: string,
|
|
145
|
+
source: ImportTarget,
|
|
146
|
+
workspacePath: string | undefined,
|
|
147
|
+
out: Map<string, SkillListEntry>,
|
|
148
|
+
issues: SkillListIssue[],
|
|
149
|
+
): void {
|
|
150
|
+
try {
|
|
151
|
+
if (!existsSync(root)) return
|
|
152
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
153
|
+
const entryPath = join(root, entry.name)
|
|
154
|
+
let file: string | undefined
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
const candidate = join(entryPath, 'SKILL.md')
|
|
157
|
+
if (existsSync(candidate)) file = candidate
|
|
158
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
159
|
+
file = entryPath
|
|
160
|
+
}
|
|
161
|
+
if (file === undefined) continue
|
|
162
|
+
try {
|
|
163
|
+
const { frontmatter } = parseSkillFile(readFileSync(file, 'utf8'))
|
|
164
|
+
if (frontmatter.name === undefined || frontmatter.description === undefined) continue
|
|
165
|
+
if (!isValidSkillName(frontmatter.name)) continue
|
|
166
|
+
const existing = out.get(frontmatter.name)
|
|
167
|
+
if (existing !== undefined && ROOT_RANK[existing.source] <= ROOT_RANK[source]) continue
|
|
168
|
+
out.set(frontmatter.name, {
|
|
169
|
+
name: frontmatter.name,
|
|
170
|
+
description: frontmatter.description,
|
|
171
|
+
...(frontmatter.whenToUse !== undefined ? { whenToUse: frontmatter.whenToUse } : {}),
|
|
172
|
+
modelInvocable: frontmatter.disableModelInvocation !== true,
|
|
173
|
+
userInvocable: frontmatter.userInvocable !== false,
|
|
174
|
+
source,
|
|
175
|
+
...(workspacePath === undefined ? {} : { workspacePath }),
|
|
176
|
+
})
|
|
177
|
+
} catch (error) {
|
|
178
|
+
issues.push({
|
|
179
|
+
source,
|
|
180
|
+
path: file,
|
|
181
|
+
...(workspacePath === undefined ? {} : { workspacePath }),
|
|
182
|
+
message: error instanceof Error ? error.message : String(error),
|
|
183
|
+
})
|
|
184
|
+
}
|
|
140
185
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (frontmatter.name === undefined || frontmatter.description === undefined) continue
|
|
144
|
-
if (!isValidSkillName(frontmatter.name)) continue
|
|
145
|
-
const existing = out.get(frontmatter.name)
|
|
146
|
-
if (existing !== undefined && ROOT_RANK[existing.source] <= ROOT_RANK[source]) continue
|
|
147
|
-
out.set(frontmatter.name, {
|
|
148
|
-
name: frontmatter.name,
|
|
149
|
-
description: frontmatter.description,
|
|
150
|
-
...(frontmatter.whenToUse !== undefined ? { whenToUse: frontmatter.whenToUse } : {}),
|
|
151
|
-
modelInvocable: frontmatter.disableModelInvocation !== true,
|
|
152
|
-
userInvocable: frontmatter.userInvocable !== false,
|
|
186
|
+
} catch (error) {
|
|
187
|
+
issues.push({
|
|
153
188
|
source,
|
|
189
|
+
path: root,
|
|
190
|
+
...(workspacePath === undefined ? {} : { workspacePath }),
|
|
191
|
+
message: error instanceof Error ? error.message : String(error),
|
|
154
192
|
})
|
|
155
193
|
}
|
|
156
194
|
}
|
|
@@ -162,16 +200,26 @@ function scanRoot(root: string, source: ImportTarget, out: Map<string, SkillList
|
|
|
162
200
|
* rank at discovery time). Display order groups by source, then name.
|
|
163
201
|
* @param workspacePaths - canonical paths of the registered workspaces.
|
|
164
202
|
*/
|
|
165
|
-
export function listSkills(workspacePaths: readonly string[]): SkillListEntry[] {
|
|
203
|
+
export function listSkills(workspacePaths: readonly string[]): { skills: SkillListEntry[]; issues: SkillListIssue[] } {
|
|
166
204
|
const rows: SkillListEntry[] = []
|
|
205
|
+
const issues: SkillListIssue[] = []
|
|
167
206
|
for (const target of SCAN_ORDER) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
scanRoot(skillRoot(target,
|
|
207
|
+
if (!isProjectTarget(target)) {
|
|
208
|
+
const out = new Map<string, SkillListEntry>()
|
|
209
|
+
scanRoot(skillRoot(target, ''), target, undefined, out, issues)
|
|
210
|
+
rows.push(...out.values())
|
|
211
|
+
continue
|
|
171
212
|
}
|
|
172
|
-
|
|
213
|
+
for (const workspacePath of workspacePaths) {
|
|
214
|
+
const out = new Map<string, SkillListEntry>()
|
|
215
|
+
scanRoot(skillRoot(target, workspacePath), target, workspacePath, out, issues)
|
|
216
|
+
rows.push(...out.values())
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
skills: rows.sort((a, b) => a.name.localeCompare(b.name)),
|
|
221
|
+
issues,
|
|
173
222
|
}
|
|
174
|
-
return rows.sort((a, b) => a.name.localeCompare(b.name))
|
|
175
223
|
}
|
|
176
224
|
|
|
177
225
|
/**
|
|
@@ -184,7 +232,7 @@ export function listSkills(workspacePaths: readonly string[]): SkillListEntry[]
|
|
|
184
232
|
*/
|
|
185
233
|
export function deleteSkillFile(name: string, source: ImportTarget, workspacePath?: string): boolean {
|
|
186
234
|
if (!KEBAB_CASE.test(name)) throw new Error('技能名必须是 kebab-case(小写字母、数字、短横线)')
|
|
187
|
-
if (source
|
|
235
|
+
if (isProjectTarget(source) && workspacePath === undefined) {
|
|
188
236
|
throw new Error('项目来源需要 workspacePath(当前工作区路径)')
|
|
189
237
|
}
|
|
190
238
|
const root = skillRoot(source, workspacePath ?? '')
|
|
@@ -290,7 +338,7 @@ export function scanBatch(request: BatchScanRequest): BatchScanSession {
|
|
|
290
338
|
if (!isSkillsRoot && !isSkillDirectory) {
|
|
291
339
|
throw new Error('批量导入仅支持名为 skills 的目录或其中的单个技能目录')
|
|
292
340
|
}
|
|
293
|
-
if (request.target
|
|
341
|
+
if (isProjectTarget(request.target) && request.workspacePath === undefined) throw new Error('项目目标需要 workspacePath(当前工作区路径)')
|
|
294
342
|
const sources = batchSources(sourcePath)
|
|
295
343
|
if (sources.length === 0) throw new Error('所选目录中没有找到可导入的技能')
|
|
296
344
|
if (sources.length > MAX_BATCH_SKILLS) throw new Error(`一次最多扫描 ${MAX_BATCH_SKILLS} 个技能`)
|
package/src/types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** Where the imported skill file should land. */
|
|
8
|
-
export type ImportTarget = '
|
|
8
|
+
export type ImportTarget = 'project-dsh' | 'project-agents' | 'user-dsh' | 'user-agents'
|
|
9
9
|
|
|
10
10
|
/** One installed skill row served by `/skill-importer/list`. */
|
|
11
11
|
export interface SkillListEntry {
|
|
@@ -19,8 +19,18 @@ export interface SkillListEntry {
|
|
|
19
19
|
readonly modelInvocable: boolean
|
|
20
20
|
/** False marks a model-only skill (`user-invocable: false`). */
|
|
21
21
|
readonly userInvocable: boolean
|
|
22
|
-
/**
|
|
22
|
+
/** Writable DSH skill root that owns this copy. */
|
|
23
23
|
readonly source: ImportTarget
|
|
24
|
+
/** Exact registered workspace that owns a project-scoped copy. */
|
|
25
|
+
readonly workspacePath?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One skill root or entry that could not be inspected without failing the whole list. */
|
|
29
|
+
export interface SkillListIssue {
|
|
30
|
+
readonly source: ImportTarget
|
|
31
|
+
readonly path: string
|
|
32
|
+
readonly workspacePath?: string
|
|
33
|
+
readonly message: string
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
/** File-import request body. */
|
|
@@ -36,7 +46,7 @@ export interface ImportRequest {
|
|
|
36
46
|
/** Delete-request body. */
|
|
37
47
|
export interface DeleteRequest {
|
|
38
48
|
readonly name: string
|
|
39
|
-
/** Which
|
|
49
|
+
/** Which writable DSH root owns this copy. */
|
|
40
50
|
readonly source: ImportTarget
|
|
41
51
|
/** Canonical workspace path for project sources (host-validated). */
|
|
42
52
|
readonly workspacePath?: string
|
|
@@ -107,6 +117,18 @@ export interface BatchCommitResponse {
|
|
|
107
117
|
export interface SkillListResponse {
|
|
108
118
|
readonly ok: true
|
|
109
119
|
readonly skills: readonly SkillListEntry[]
|
|
120
|
+
readonly issues: readonly SkillListIssue[]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** `/skill-importer/update-status` response. */
|
|
124
|
+
export interface UpdateStatusResponse {
|
|
125
|
+
readonly ok: true
|
|
126
|
+
readonly currentVersion: string
|
|
127
|
+
readonly latestVersion?: string
|
|
128
|
+
readonly updateAvailable: boolean
|
|
129
|
+
readonly command?: string
|
|
130
|
+
/** A failed registry check is non-fatal: the installed version is still useful. */
|
|
131
|
+
readonly error?: string
|
|
110
132
|
}
|
|
111
133
|
|
|
112
134
|
/** Success response of an import. */
|
|
@@ -43,7 +43,7 @@ try {
|
|
|
43
43
|
mkdirSync(existing, { recursive: true })
|
|
44
44
|
writeFileSync(join(existing, 'SKILL.md'), skill('alpha', 'old'))
|
|
45
45
|
|
|
46
|
-
const scan = scanBatch({ sourcePath: source, target: 'user' })
|
|
46
|
+
const scan = scanBatch({ sourcePath: source, target: 'user-dsh' })
|
|
47
47
|
assert.equal(scan.entries.find(row => row.name === 'alpha')?.conflict, true)
|
|
48
48
|
assert.equal(scan.entries.find(row => row.name === 'windows-crlf')?.status, 'ready')
|
|
49
49
|
assert.equal(scan.entries.find(row => row.name === 'invalid')?.status, 'error')
|
|
@@ -63,7 +63,7 @@ try {
|
|
|
63
63
|
const changedSource = join(sandbox, '.codex', 'skills')
|
|
64
64
|
mkdirSync(join(changedSource, 'beta'), { recursive: true })
|
|
65
65
|
writeFileSync(join(changedSource, 'beta', 'SKILL.md'), skill('beta'))
|
|
66
|
-
const changedScan = scanBatch({ sourcePath: changedSource, target: 'user' })
|
|
66
|
+
const changedScan = scanBatch({ sourcePath: changedSource, target: 'user-dsh' })
|
|
67
67
|
writeFileSync(join(changedSource, 'beta', 'SKILL.md'), skill('beta', 'changed after scan'))
|
|
68
68
|
const changedResults = commitBatch(changedScan, new Set())
|
|
69
69
|
assert.equal(changedResults[0]?.status, 'error')
|
|
@@ -74,15 +74,15 @@ try {
|
|
|
74
74
|
const customAgentSource = join(sandbox, 've_hcs_agent', 'skills')
|
|
75
75
|
mkdirSync(join(customAgentSource, 'gamma'), { recursive: true })
|
|
76
76
|
writeFileSync(join(customAgentSource, 'gamma', 'SKILL.md'), skill('gamma'))
|
|
77
|
-
assert.equal(scanBatch({ sourcePath: customAgentSource, target: 'user' }).entries[0]?.name, 'gamma')
|
|
77
|
+
assert.equal(scanBatch({ sourcePath: customAgentSource, target: 'user-dsh' }).entries[0]?.name, 'gamma')
|
|
78
78
|
|
|
79
79
|
const empty = join(sandbox, '.agents', 'skills')
|
|
80
80
|
mkdirSync(empty, { recursive: true })
|
|
81
|
-
assert.throws(() => scanBatch({ sourcePath: empty, target: 'user' }), /没有找到/)
|
|
81
|
+
assert.throws(() => scanBatch({ sourcePath: empty, target: 'user-dsh' }), /没有找到/)
|
|
82
82
|
|
|
83
83
|
const arbitrary = join(sandbox, 'arbitrary')
|
|
84
84
|
mkdirSync(arbitrary)
|
|
85
|
-
assert.throws(() => scanBatch({ sourcePath: arbitrary, target: 'user' }), /名为 skills/)
|
|
85
|
+
assert.throws(() => scanBatch({ sourcePath: arbitrary, target: 'user-dsh' }), /名为 skills/)
|
|
86
86
|
|
|
87
87
|
assert.equal(originAllowed({ headers: {} }), false)
|
|
88
88
|
assert.equal(originAllowed({ headers: { origin: 'https://evil.example' } }), false)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { effectiveSkills } from '../src/client/effectiveSkills.ts'
|
|
3
|
+
|
|
4
|
+
const row = (name, source, workspacePath) => ({
|
|
5
|
+
name,
|
|
6
|
+
source,
|
|
7
|
+
workspacePath,
|
|
8
|
+
description: source,
|
|
9
|
+
modelInvocable: true,
|
|
10
|
+
userInvocable: true,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const effective = effectiveSkills([
|
|
14
|
+
row('shared', 'user-agents'),
|
|
15
|
+
row('shared', 'project-agents', '/project'),
|
|
16
|
+
row('shared', 'user-dsh'),
|
|
17
|
+
row('shared', 'project-dsh', '/project'),
|
|
18
|
+
row('global-only', 'user-dsh'),
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
assert.deepEqual(effective.map((skill) => [skill.name, skill.source]), [
|
|
22
|
+
['global-only', 'user-dsh'],
|
|
23
|
+
['shared', 'project-dsh'],
|
|
24
|
+
])
|
|
25
|
+
console.log('effective skills test: OK')
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { listSkills, skillRoot } from '../src/server.ts'
|
|
6
|
+
|
|
7
|
+
const sandbox = mkdtempSync(join(tmpdir(), 'dsh-skill-list-test-'))
|
|
8
|
+
const previousHome = process.env.DSH_HOME
|
|
9
|
+
const previousAgentsHome = process.env.DSH_AGENTS_HOME
|
|
10
|
+
process.env.DSH_HOME = join(sandbox, 'home')
|
|
11
|
+
process.env.DSH_AGENTS_HOME = join(sandbox, 'agents-home')
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
assert.equal(skillRoot('project-dsh', '/workspace'), join('/workspace', '.dsh', 'skills'))
|
|
15
|
+
assert.equal(skillRoot('project-agents', '/workspace'), join('/workspace', '.agents', 'skills'))
|
|
16
|
+
assert.equal(skillRoot('user-dsh', ''), join(process.env.DSH_HOME, 'skills'))
|
|
17
|
+
assert.equal(skillRoot('user-agents', ''), join(process.env.DSH_AGENTS_HOME, 'skills'))
|
|
18
|
+
|
|
19
|
+
const first = join(sandbox, 'first')
|
|
20
|
+
const second = join(sandbox, 'second')
|
|
21
|
+
const firstRoot = join(first, '.agents', 'skills')
|
|
22
|
+
const secondRoot = join(second, '.agents', 'skills')
|
|
23
|
+
mkdirSync(join(firstRoot, 'shared'), { recursive: true })
|
|
24
|
+
mkdirSync(join(secondRoot, 'shared'), { recursive: true })
|
|
25
|
+
writeFileSync(join(firstRoot, 'shared', 'SKILL.md'), '---\nname: shared\ndescription: first\n---\n')
|
|
26
|
+
writeFileSync(join(secondRoot, 'shared', 'SKILL.md'), '---\nname: shared\ndescription: second\n---\n')
|
|
27
|
+
|
|
28
|
+
const listed = listSkills([first, second])
|
|
29
|
+
assert.equal(listed.issues.length, 0)
|
|
30
|
+
assert.deepEqual(
|
|
31
|
+
listed.skills.map(skill => [skill.name, skill.description, skill.workspacePath]),
|
|
32
|
+
[['shared', 'first', first], ['shared', 'second', second]],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
chmodSync(secondRoot, 0o000)
|
|
36
|
+
const partial = listSkills([first, second])
|
|
37
|
+
assert.equal(partial.skills.length, 1)
|
|
38
|
+
assert.equal(partial.skills[0]?.workspacePath, first)
|
|
39
|
+
assert.equal(partial.issues.length, 1)
|
|
40
|
+
assert.equal(partial.issues[0]?.path, secondRoot)
|
|
41
|
+
chmodSync(secondRoot, 0o755)
|
|
42
|
+
} finally {
|
|
43
|
+
if (previousHome === undefined) delete process.env.DSH_HOME
|
|
44
|
+
else process.env.DSH_HOME = previousHome
|
|
45
|
+
if (previousAgentsHome === undefined) delete process.env.DSH_AGENTS_HOME
|
|
46
|
+
else process.env.DSH_AGENTS_HOME = previousAgentsHome
|
|
47
|
+
rmSync(sandbox, { recursive: true, force: true })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('skill list test: OK')
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
import { compareVersions, currentVersion, updateCommand } from '../src/update.ts'
|
|
4
|
+
|
|
5
|
+
test('compares stable semantic versions', () => {
|
|
6
|
+
assert.equal(compareVersions('0.2.3', '0.2.4'), -1)
|
|
7
|
+
assert.equal(compareVersions('1.0.0', '1.0.0'), 0)
|
|
8
|
+
assert.equal(compareVersions('2.0.0', '1.9.9'), 1)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('orders prereleases before stable releases', () => {
|
|
12
|
+
assert.equal(compareVersions('1.0.0-rc.2', '1.0.0'), -1)
|
|
13
|
+
assert.equal(compareVersions('1.0.0-rc.10', '1.0.0-rc.2'), 1)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('reads package version and produces an exact update command', () => {
|
|
17
|
+
assert.equal(currentVersion(), '0.2.5')
|
|
18
|
+
assert.equal(
|
|
19
|
+
updateCommand('0.2.4'),
|
|
20
|
+
'npx @deepseek-ai/dsh plugin --profile web add dsh-skill-importer@0.2.4',
|
|
21
|
+
)
|
|
22
|
+
})
|