@plaited/development-skills 0.3.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/.claude/commands/lsp-analyze.md +66 -0
- package/.claude/commands/lsp-find.md +51 -0
- package/.claude/commands/lsp-hover.md +48 -0
- package/.claude/commands/lsp-refs.md +55 -0
- package/.claude/commands/scaffold-rules.md +221 -0
- package/.claude/commands/validate-skill.md +29 -0
- package/.claude/rules/accuracy.md +64 -0
- package/.claude/rules/bun-apis.md +80 -0
- package/.claude/rules/code-review.md +276 -0
- package/.claude/rules/git-workflow.md +66 -0
- package/.claude/rules/github.md +154 -0
- package/.claude/rules/testing.md +125 -0
- package/.claude/settings.local.json +47 -0
- package/.claude/skills/code-documentation/SKILL.md +47 -0
- package/.claude/skills/code-documentation/references/internal-templates.md +113 -0
- package/.claude/skills/code-documentation/references/maintenance.md +164 -0
- package/.claude/skills/code-documentation/references/public-api-templates.md +100 -0
- package/.claude/skills/code-documentation/references/type-documentation.md +116 -0
- package/.claude/skills/code-documentation/references/workflow.md +60 -0
- package/.claude/skills/scaffold-rules/SKILL.md +97 -0
- package/.claude/skills/typescript-lsp/SKILL.md +239 -0
- package/.claude/skills/validate-skill/SKILL.md +105 -0
- package/LICENSE +15 -0
- package/README.md +149 -0
- package/bin/cli.ts +109 -0
- package/package.json +57 -0
- package/src/lsp-analyze.ts +223 -0
- package/src/lsp-client.ts +400 -0
- package/src/lsp-find.ts +100 -0
- package/src/lsp-hover.ts +87 -0
- package/src/lsp-references.ts +83 -0
- package/src/lsp-symbols.ts +73 -0
- package/src/resolve-file-path.ts +28 -0
- package/src/scaffold-rules.ts +435 -0
- package/src/tests/fixtures/sample.ts +27 -0
- package/src/tests/lsp-client.spec.ts +180 -0
- package/src/tests/resolve-file-path.spec.ts +33 -0
- package/src/tests/scaffold-rules.spec.ts +286 -0
- package/src/tests/validate-skill.spec.ts +231 -0
- package/src/validate-skill.ts +492 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const scriptsDir = join(import.meta.dir, '..')
|
|
7
|
+
|
|
8
|
+
describe('validate-skill', () => {
|
|
9
|
+
let tempDir: string
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
tempDir = await mkdtemp(join(tmpdir(), 'validate-skill-test-'))
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const createSkill = async (name: string, frontmatter: string, body = '# Test Skill') => {
|
|
20
|
+
const skillDir = join(tempDir, name)
|
|
21
|
+
await Bun.$`mkdir -p ${skillDir}`.quiet()
|
|
22
|
+
await Bun.write(join(skillDir, 'SKILL.md'), `---\n${frontmatter}\n---\n\n${body}`)
|
|
23
|
+
return skillDir
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const runValidation = async (path: string, json = true) => {
|
|
27
|
+
const args = json ? ['--json'] : []
|
|
28
|
+
const result = await Bun.$`bun ${scriptsDir}/validate-skill.ts ${path} ${args}`.quiet().nothrow()
|
|
29
|
+
if (json) {
|
|
30
|
+
return JSON.parse(result.text())
|
|
31
|
+
}
|
|
32
|
+
return result
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('single skill validation', () => {
|
|
36
|
+
test('validates skill with required fields only', async () => {
|
|
37
|
+
const skillDir = await createSkill('valid-skill', 'name: valid-skill\ndescription: A test skill')
|
|
38
|
+
|
|
39
|
+
const [result] = await runValidation(skillDir)
|
|
40
|
+
|
|
41
|
+
expect(result.valid).toBe(true)
|
|
42
|
+
expect(result.errors).toHaveLength(0)
|
|
43
|
+
expect(result.properties?.name).toBe('valid-skill')
|
|
44
|
+
expect(result.properties?.description).toBe('A test skill')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('validates skill with all optional fields', async () => {
|
|
48
|
+
const skillDir = await createSkill(
|
|
49
|
+
'full-skill',
|
|
50
|
+
`name: full-skill
|
|
51
|
+
description: A complete skill
|
|
52
|
+
license: MIT
|
|
53
|
+
compatibility: Requires bun
|
|
54
|
+
allowed-tools: Bash Read Write
|
|
55
|
+
metadata:
|
|
56
|
+
author: test
|
|
57
|
+
version: "1.0"`,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const [result] = await runValidation(skillDir)
|
|
61
|
+
|
|
62
|
+
expect(result.valid).toBe(true)
|
|
63
|
+
expect(result.errors).toHaveLength(0)
|
|
64
|
+
expect(result.properties?.license).toBe('MIT')
|
|
65
|
+
expect(result.properties?.compatibility).toBe('Requires bun')
|
|
66
|
+
expect(result.properties?.['allowed-tools']).toBe('Bash Read Write')
|
|
67
|
+
expect(result.properties?.metadata).toEqual({ author: 'test', version: '1.0' })
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('reports error for missing name', async () => {
|
|
71
|
+
const skillDir = await createSkill('no-name', 'description: Missing name field')
|
|
72
|
+
|
|
73
|
+
const [result] = await runValidation(skillDir)
|
|
74
|
+
|
|
75
|
+
expect(result.valid).toBe(false)
|
|
76
|
+
expect(result.errors).toContain("Missing required field in frontmatter: 'name'")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('reports error for missing description', async () => {
|
|
80
|
+
const skillDir = await createSkill('no-desc', 'name: no-desc')
|
|
81
|
+
|
|
82
|
+
const [result] = await runValidation(skillDir)
|
|
83
|
+
|
|
84
|
+
expect(result.valid).toBe(false)
|
|
85
|
+
expect(result.errors).toContain("Missing required field in frontmatter: 'description'")
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('reports error for name exceeding 64 characters', async () => {
|
|
89
|
+
const longName = 'a'.repeat(65)
|
|
90
|
+
const skillDir = await createSkill(longName, `name: ${longName}\ndescription: Too long name`)
|
|
91
|
+
|
|
92
|
+
const [result] = await runValidation(skillDir)
|
|
93
|
+
|
|
94
|
+
expect(result.valid).toBe(false)
|
|
95
|
+
expect(result.errors.some((e: string) => e.includes('64 character limit'))).toBe(true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('reports error for description exceeding 1024 characters', async () => {
|
|
99
|
+
const longDesc = 'a'.repeat(1025)
|
|
100
|
+
const skillDir = await createSkill('long-desc', `name: long-desc\ndescription: ${longDesc}`)
|
|
101
|
+
|
|
102
|
+
const [result] = await runValidation(skillDir)
|
|
103
|
+
|
|
104
|
+
expect(result.valid).toBe(false)
|
|
105
|
+
expect(result.errors.some((e: string) => e.includes('1024 character limit'))).toBe(true)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('reports error for uppercase name', async () => {
|
|
109
|
+
const skillDir = await createSkill('Upper-Case', 'name: Upper-Case\ndescription: Uppercase name')
|
|
110
|
+
|
|
111
|
+
const [result] = await runValidation(skillDir)
|
|
112
|
+
|
|
113
|
+
expect(result.valid).toBe(false)
|
|
114
|
+
expect(result.errors.some((e: string) => e.includes('lowercase'))).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('reports error for consecutive hyphens', async () => {
|
|
118
|
+
const skillDir = await createSkill('bad--name', 'name: bad--name\ndescription: Consecutive hyphens')
|
|
119
|
+
|
|
120
|
+
const [result] = await runValidation(skillDir)
|
|
121
|
+
|
|
122
|
+
expect(result.valid).toBe(false)
|
|
123
|
+
expect(result.errors.some((e: string) => e.includes('consecutive hyphens'))).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('reports error for name starting with hyphen', async () => {
|
|
127
|
+
const skillDir = await createSkill('leadhyphen', 'name: -lead-hyphen\ndescription: Leading hyphen')
|
|
128
|
+
|
|
129
|
+
const [result] = await runValidation(skillDir)
|
|
130
|
+
|
|
131
|
+
expect(result.valid).toBe(false)
|
|
132
|
+
expect(result.errors.some((e: string) => e.includes('start or end with a hyphen'))).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('reports error for name not matching directory', async () => {
|
|
136
|
+
const skillDir = await createSkill('dir-name', 'name: different-name\ndescription: Mismatched names')
|
|
137
|
+
|
|
138
|
+
const [result] = await runValidation(skillDir)
|
|
139
|
+
|
|
140
|
+
expect(result.valid).toBe(false)
|
|
141
|
+
expect(result.errors.some((e: string) => e.includes('must match skill name'))).toBe(true)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('reports error for missing SKILL.md', async () => {
|
|
145
|
+
const skillDir = join(tempDir, 'empty-skill')
|
|
146
|
+
await Bun.$`mkdir -p ${skillDir}`.quiet()
|
|
147
|
+
|
|
148
|
+
const [result] = await runValidation(skillDir)
|
|
149
|
+
|
|
150
|
+
expect(result.valid).toBe(false)
|
|
151
|
+
expect(result.errors).toContain('Missing required file: SKILL.md')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('reports error for non-existent directory', async () => {
|
|
155
|
+
const [result] = await runValidation(join(tempDir, 'nonexistent'))
|
|
156
|
+
|
|
157
|
+
expect(result.valid).toBe(false)
|
|
158
|
+
expect(result.errors.some((e: string) => e.includes('does not exist'))).toBe(true)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('reports error for compatibility exceeding 500 characters', async () => {
|
|
162
|
+
const longCompat = 'a'.repeat(501)
|
|
163
|
+
const skillDir = await createSkill(
|
|
164
|
+
'long-compat',
|
|
165
|
+
`name: long-compat\ndescription: Test\ncompatibility: ${longCompat}`,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
const [result] = await runValidation(skillDir)
|
|
169
|
+
|
|
170
|
+
expect(result.valid).toBe(false)
|
|
171
|
+
expect(result.errors.some((e: string) => e.includes('500 character limit'))).toBe(true)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('multiple skills validation', () => {
|
|
176
|
+
test('validates multiple skills in directory', async () => {
|
|
177
|
+
const multiDir = join(tempDir, 'multi')
|
|
178
|
+
await Bun.$`mkdir -p ${multiDir}`.quiet()
|
|
179
|
+
|
|
180
|
+
await createSkill('multi/valid-one', 'name: valid-one\ndescription: First valid skill')
|
|
181
|
+
await createSkill('multi/valid-two', 'name: valid-two\ndescription: Second valid skill')
|
|
182
|
+
|
|
183
|
+
const results = await runValidation(multiDir)
|
|
184
|
+
|
|
185
|
+
expect(results).toHaveLength(2)
|
|
186
|
+
expect(results.every((r: { valid: boolean }) => r.valid)).toBe(true)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('reports mixed valid and invalid skills', async () => {
|
|
190
|
+
const mixedDir = join(tempDir, 'mixed')
|
|
191
|
+
await Bun.$`mkdir -p ${mixedDir}`.quiet()
|
|
192
|
+
|
|
193
|
+
await createSkill('mixed/good-skill', 'name: good-skill\ndescription: Valid skill')
|
|
194
|
+
await createSkill('mixed/bad-skill', 'description: Missing name')
|
|
195
|
+
|
|
196
|
+
const results = await runValidation(mixedDir)
|
|
197
|
+
|
|
198
|
+
expect(results).toHaveLength(2)
|
|
199
|
+
const valid = results.filter((r: { valid: boolean }) => r.valid)
|
|
200
|
+
const invalid = results.filter((r: { valid: boolean }) => !r.valid)
|
|
201
|
+
expect(valid).toHaveLength(1)
|
|
202
|
+
expect(invalid).toHaveLength(1)
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('CLI output', () => {
|
|
207
|
+
test('exits with code 1 on validation errors', async () => {
|
|
208
|
+
const skillDir = await createSkill('cli-exit', 'description: No name field')
|
|
209
|
+
|
|
210
|
+
const proc = Bun.spawn(['bun', `${scriptsDir}/validate-skill.ts`, skillDir], {
|
|
211
|
+
stderr: 'pipe',
|
|
212
|
+
stdout: 'pipe',
|
|
213
|
+
})
|
|
214
|
+
const exitCode = await proc.exited
|
|
215
|
+
|
|
216
|
+
expect(exitCode).toBe(1)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('exits with code 0 on valid skills', async () => {
|
|
220
|
+
const skillDir = await createSkill('cli-success', 'name: cli-success\ndescription: Valid skill')
|
|
221
|
+
|
|
222
|
+
const proc = Bun.spawn(['bun', `${scriptsDir}/validate-skill.ts`, skillDir], {
|
|
223
|
+
stderr: 'pipe',
|
|
224
|
+
stdout: 'pipe',
|
|
225
|
+
})
|
|
226
|
+
const exitCode = await proc.exited
|
|
227
|
+
|
|
228
|
+
expect(exitCode).toBe(0)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
})
|