@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,492 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Validate skill directories against AgentSkills specification.
|
|
4
|
+
*
|
|
5
|
+
* Usage: bun validate-skill.ts [paths...] [--json]
|
|
6
|
+
*
|
|
7
|
+
* @see https://agentskills.io/specification
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { basename, join } from 'node:path'
|
|
11
|
+
import { parseArgs } from 'node:util'
|
|
12
|
+
import { Glob } from 'bun'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Properties extracted from SKILL.md frontmatter.
|
|
16
|
+
*/
|
|
17
|
+
type SkillProperties = {
|
|
18
|
+
name: string
|
|
19
|
+
description: string
|
|
20
|
+
license?: string
|
|
21
|
+
compatibility?: string
|
|
22
|
+
'allowed-tools'?: string
|
|
23
|
+
metadata?: Record<string, string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Result of validating a skill directory.
|
|
28
|
+
*/
|
|
29
|
+
type ValidationResult = {
|
|
30
|
+
valid: boolean
|
|
31
|
+
path: string
|
|
32
|
+
errors: string[]
|
|
33
|
+
warnings: string[]
|
|
34
|
+
properties?: SkillProperties
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validation constants from AgentSkills specification
|
|
39
|
+
* @see https://agentskills.io/specification
|
|
40
|
+
*/
|
|
41
|
+
const ALLOWED_FIELDS = new Set(['name', 'description', 'license', 'compatibility', 'allowed-tools', 'metadata'])
|
|
42
|
+
const REQUIRED_FIELDS = ['name', 'description'] as const
|
|
43
|
+
/** Must be lowercase alphanumeric with optional hyphens (no consecutive hyphens, no leading/trailing) */
|
|
44
|
+
const NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
45
|
+
/** Spec: name must be 1-64 characters */
|
|
46
|
+
const MAX_NAME_LENGTH = 64
|
|
47
|
+
/** Spec: description must be 1-1024 characters */
|
|
48
|
+
const MAX_DESCRIPTION_LENGTH = 1024
|
|
49
|
+
/** Spec: compatibility must be 1-500 characters if provided */
|
|
50
|
+
const MAX_COMPATIBILITY_LENGTH = 500
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse YAML frontmatter from SKILL.md content.
|
|
54
|
+
*
|
|
55
|
+
* @returns Tuple of [metadata, body] or throws on parse error
|
|
56
|
+
*/
|
|
57
|
+
const parseFrontmatter = (content: string): [Record<string, unknown>, string] => {
|
|
58
|
+
const trimmed = content.trim()
|
|
59
|
+
if (!trimmed.startsWith('---')) {
|
|
60
|
+
throw new Error('SKILL.md must start with YAML frontmatter (---)')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const endIndex = trimmed.indexOf('---', 3)
|
|
64
|
+
if (endIndex === -1) {
|
|
65
|
+
throw new Error('YAML frontmatter not properly closed with ---')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const yamlContent = trimmed.slice(3, endIndex).trim()
|
|
69
|
+
const body = trimmed.slice(endIndex + 3).trim()
|
|
70
|
+
|
|
71
|
+
// Simple YAML parser for frontmatter (handles key: value and key: "value")
|
|
72
|
+
const metadata: Record<string, unknown> = {}
|
|
73
|
+
let currentKey: string | null = null
|
|
74
|
+
let inMetadata = false
|
|
75
|
+
const metadataObj: Record<string, string> = {}
|
|
76
|
+
|
|
77
|
+
for (const line of yamlContent.split('\n')) {
|
|
78
|
+
const trimmedLine = line.trim()
|
|
79
|
+
if (!trimmedLine) continue
|
|
80
|
+
|
|
81
|
+
// Check for metadata block
|
|
82
|
+
if (trimmedLine === 'metadata:') {
|
|
83
|
+
inMetadata = true
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle metadata entries (indented key: value pairs)
|
|
88
|
+
if (inMetadata && line.startsWith(' ')) {
|
|
89
|
+
const metaMatch = trimmedLine.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/)
|
|
90
|
+
if (metaMatch?.[1] && metaMatch[2] !== undefined) {
|
|
91
|
+
const key = metaMatch[1]
|
|
92
|
+
const value = metaMatch[2]
|
|
93
|
+
metadataObj[key] = value.replace(/^["']|["']$/g, '')
|
|
94
|
+
}
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Exit metadata block when encountering non-indented line
|
|
99
|
+
if (inMetadata && !line.startsWith(' ')) {
|
|
100
|
+
inMetadata = false
|
|
101
|
+
if (Object.keys(metadataObj).length > 0) {
|
|
102
|
+
metadata.metadata = { ...metadataObj }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Parse regular key: value pairs
|
|
107
|
+
const match = trimmedLine.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/)
|
|
108
|
+
if (match?.[1]) {
|
|
109
|
+
const key = match[1]
|
|
110
|
+
const value = match[2] ?? ''
|
|
111
|
+
currentKey = key
|
|
112
|
+
|
|
113
|
+
// Handle multi-line strings (value on same line)
|
|
114
|
+
if (value) {
|
|
115
|
+
metadata[key] = value.replace(/^["']|["']$/g, '')
|
|
116
|
+
}
|
|
117
|
+
} else if (currentKey && trimmedLine) {
|
|
118
|
+
// Handle continuation of previous value
|
|
119
|
+
const prev = metadata[currentKey]
|
|
120
|
+
metadata[currentKey] = typeof prev === 'string' ? `${prev} ${trimmedLine}` : trimmedLine
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Capture any remaining metadata
|
|
125
|
+
if (Object.keys(metadataObj).length > 0 && !metadata.metadata) {
|
|
126
|
+
metadata.metadata = { ...metadataObj }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return [metadata, body]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validate skill name according to AgentSkills specification.
|
|
134
|
+
*/
|
|
135
|
+
const validateName = (name: unknown, dirName: string): string[] => {
|
|
136
|
+
const errors: string[] = []
|
|
137
|
+
|
|
138
|
+
if (typeof name !== 'string' || !name) {
|
|
139
|
+
errors.push("Field 'name' must be a non-empty string")
|
|
140
|
+
return errors
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (name.length > MAX_NAME_LENGTH) {
|
|
144
|
+
errors.push(`Skill name exceeds ${MAX_NAME_LENGTH} character limit`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (name !== name.toLowerCase()) {
|
|
148
|
+
errors.push('Skill name must be lowercase')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (name.startsWith('-') || name.endsWith('-')) {
|
|
152
|
+
errors.push('Skill name cannot start or end with a hyphen')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (name.includes('--')) {
|
|
156
|
+
errors.push('Skill name cannot contain consecutive hyphens')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!NAME_PATTERN.test(name)) {
|
|
160
|
+
errors.push('Skill name contains invalid characters (only lowercase alphanumeric and hyphens allowed)')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (name !== dirName) {
|
|
164
|
+
errors.push(`Directory name '${dirName}' must match skill name '${name}'`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return errors
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Validate skill description according to AgentSkills specification.
|
|
172
|
+
*/
|
|
173
|
+
const validateDescription = (description: unknown): string[] => {
|
|
174
|
+
const errors: string[] = []
|
|
175
|
+
|
|
176
|
+
if (typeof description !== 'string' || !description) {
|
|
177
|
+
errors.push("Field 'description' must be a non-empty string")
|
|
178
|
+
return errors
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
182
|
+
errors.push(`Description exceeds ${MAX_DESCRIPTION_LENGTH} character limit`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return errors
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Validate optional compatibility field.
|
|
190
|
+
*/
|
|
191
|
+
const validateCompatibility = (compatibility: unknown): string[] => {
|
|
192
|
+
const errors: string[] = []
|
|
193
|
+
|
|
194
|
+
if (compatibility === undefined) return errors
|
|
195
|
+
|
|
196
|
+
if (typeof compatibility !== 'string') {
|
|
197
|
+
errors.push("Field 'compatibility' must be a string")
|
|
198
|
+
return errors
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (compatibility.length > MAX_COMPATIBILITY_LENGTH) {
|
|
202
|
+
errors.push(`Compatibility exceeds ${MAX_COMPATIBILITY_LENGTH} character limit`)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return errors
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Validate metadata fields are all strings.
|
|
210
|
+
*/
|
|
211
|
+
const validateMetadata = (metadata: unknown): string[] => {
|
|
212
|
+
const errors: string[] = []
|
|
213
|
+
|
|
214
|
+
if (metadata === undefined) return errors
|
|
215
|
+
|
|
216
|
+
if (typeof metadata !== 'object' || metadata === null) {
|
|
217
|
+
errors.push("Field 'metadata' must be an object")
|
|
218
|
+
return errors
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
222
|
+
if (typeof value !== 'string') {
|
|
223
|
+
errors.push(`Metadata field '${key}' must be a string`)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return errors
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Check for unexpected fields in frontmatter.
|
|
232
|
+
*/
|
|
233
|
+
const validateFields = (metadata: Record<string, unknown>): string[] => {
|
|
234
|
+
const warnings: string[] = []
|
|
235
|
+
|
|
236
|
+
for (const key of Object.keys(metadata)) {
|
|
237
|
+
if (!ALLOWED_FIELDS.has(key)) {
|
|
238
|
+
warnings.push(`Unexpected field in frontmatter: '${key}'`)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return warnings
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Find SKILL.md file in a directory (case-insensitive).
|
|
247
|
+
*/
|
|
248
|
+
const findSkillMd = async (skillDir: string): Promise<string | null> => {
|
|
249
|
+
// Prefer uppercase SKILL.md
|
|
250
|
+
const upperPath = join(skillDir, 'SKILL.md')
|
|
251
|
+
if (await Bun.file(upperPath).exists()) {
|
|
252
|
+
return upperPath
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Fall back to lowercase
|
|
256
|
+
const lowerPath = join(skillDir, 'skill.md')
|
|
257
|
+
if (await Bun.file(lowerPath).exists()) {
|
|
258
|
+
return lowerPath
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Validate a single skill directory.
|
|
266
|
+
*
|
|
267
|
+
* @param skillDir - Path to the skill directory
|
|
268
|
+
* @returns Validation result with errors and warnings
|
|
269
|
+
*/
|
|
270
|
+
const validateSkillDirectory = async (skillDir: string): Promise<ValidationResult> => {
|
|
271
|
+
const result: ValidationResult = {
|
|
272
|
+
valid: false,
|
|
273
|
+
path: skillDir,
|
|
274
|
+
errors: [],
|
|
275
|
+
warnings: [],
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Check directory exists
|
|
279
|
+
try {
|
|
280
|
+
const stat = await Bun.$`test -d ${skillDir}`.quiet()
|
|
281
|
+
if (stat.exitCode !== 0) {
|
|
282
|
+
result.errors.push(`Path is not a directory: ${skillDir}`)
|
|
283
|
+
return result
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
result.errors.push(`Directory does not exist: ${skillDir}`)
|
|
287
|
+
return result
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Find SKILL.md
|
|
291
|
+
const skillMdPath = await findSkillMd(skillDir)
|
|
292
|
+
if (!skillMdPath) {
|
|
293
|
+
result.errors.push('Missing required file: SKILL.md')
|
|
294
|
+
return result
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Read and parse content
|
|
298
|
+
let content: string
|
|
299
|
+
try {
|
|
300
|
+
content = await Bun.file(skillMdPath).text()
|
|
301
|
+
} catch (error) {
|
|
302
|
+
result.errors.push(`Failed to read SKILL.md: ${error}`)
|
|
303
|
+
return result
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Parse frontmatter
|
|
307
|
+
let metadata: Record<string, unknown>
|
|
308
|
+
try {
|
|
309
|
+
;[metadata] = parseFrontmatter(content)
|
|
310
|
+
} catch (error) {
|
|
311
|
+
result.errors.push(`Failed to parse frontmatter: ${error}`)
|
|
312
|
+
return result
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check for required fields
|
|
316
|
+
for (const field of REQUIRED_FIELDS) {
|
|
317
|
+
if (!(field in metadata)) {
|
|
318
|
+
result.errors.push(`Missing required field in frontmatter: '${field}'`)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (result.errors.length > 0) {
|
|
323
|
+
return result
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Validate individual fields
|
|
327
|
+
const dirName = basename(skillDir)
|
|
328
|
+
|
|
329
|
+
result.errors.push(...validateName(metadata.name, dirName))
|
|
330
|
+
result.errors.push(...validateDescription(metadata.description))
|
|
331
|
+
result.errors.push(...validateCompatibility(metadata.compatibility))
|
|
332
|
+
result.errors.push(...validateMetadata(metadata.metadata))
|
|
333
|
+
result.warnings.push(...validateFields(metadata))
|
|
334
|
+
|
|
335
|
+
// If valid, extract properties
|
|
336
|
+
if (result.errors.length === 0) {
|
|
337
|
+
result.valid = true
|
|
338
|
+
const props: SkillProperties = {
|
|
339
|
+
name: metadata.name as string,
|
|
340
|
+
description: metadata.description as string,
|
|
341
|
+
}
|
|
342
|
+
if (typeof metadata.license === 'string') props.license = metadata.license
|
|
343
|
+
if (typeof metadata.compatibility === 'string') props.compatibility = metadata.compatibility
|
|
344
|
+
if (typeof metadata['allowed-tools'] === 'string') props['allowed-tools'] = metadata['allowed-tools']
|
|
345
|
+
if (metadata.metadata && typeof metadata.metadata === 'object') {
|
|
346
|
+
props.metadata = metadata.metadata as Record<string, string>
|
|
347
|
+
}
|
|
348
|
+
result.properties = props
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return result
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Check if a path is an existing directory.
|
|
356
|
+
*/
|
|
357
|
+
const isDirectory = async (path: string): Promise<boolean> => {
|
|
358
|
+
try {
|
|
359
|
+
const stat = await Bun.$`test -d ${path}`.quiet()
|
|
360
|
+
return stat.exitCode === 0
|
|
361
|
+
} catch {
|
|
362
|
+
return false
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Find all skill directories under a root path.
|
|
368
|
+
*
|
|
369
|
+
* @param rootDir - Root directory to search
|
|
370
|
+
* @returns Array of skill directory paths
|
|
371
|
+
*/
|
|
372
|
+
const findSkillDirectories = async (rootDir: string): Promise<string[]> => {
|
|
373
|
+
// Check if directory exists before scanning
|
|
374
|
+
if (!(await isDirectory(rootDir))) {
|
|
375
|
+
return []
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const skillDirs: string[] = []
|
|
379
|
+
const glob = new Glob('**/SKILL.md')
|
|
380
|
+
|
|
381
|
+
for await (const file of glob.scan({ cwd: rootDir, absolute: true })) {
|
|
382
|
+
// Get parent directory of SKILL.md
|
|
383
|
+
const skillDir = file.replace(/\/SKILL\.md$/i, '')
|
|
384
|
+
skillDirs.push(skillDir)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return skillDirs.sort()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Validate all skills under a root directory.
|
|
392
|
+
*
|
|
393
|
+
* @param rootDir - Root directory containing skill folders
|
|
394
|
+
* @returns Array of validation results
|
|
395
|
+
*/
|
|
396
|
+
const validateSkills = async (rootDir: string): Promise<ValidationResult[]> => {
|
|
397
|
+
const skillDirs = await findSkillDirectories(rootDir)
|
|
398
|
+
const results: ValidationResult[] = []
|
|
399
|
+
|
|
400
|
+
for (const skillDir of skillDirs) {
|
|
401
|
+
results.push(await validateSkillDirectory(skillDir))
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return results
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Validate skill directories against AgentSkills specification
|
|
409
|
+
*
|
|
410
|
+
* @param args - Command line arguments
|
|
411
|
+
*/
|
|
412
|
+
export const validateSkill = async (args: string[]) => {
|
|
413
|
+
const { values, positionals } = parseArgs({
|
|
414
|
+
args,
|
|
415
|
+
options: {
|
|
416
|
+
json: {
|
|
417
|
+
type: 'boolean',
|
|
418
|
+
default: false,
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
allowPositionals: true,
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
const cwd = process.cwd()
|
|
425
|
+
const searchPaths = positionals.length > 0 ? positionals : [join(cwd, '.claude/skills')]
|
|
426
|
+
|
|
427
|
+
const allResults: ValidationResult[] = []
|
|
428
|
+
|
|
429
|
+
for (const searchPath of searchPaths) {
|
|
430
|
+
const fullPath = searchPath.startsWith('/') ? searchPath : join(cwd, searchPath)
|
|
431
|
+
|
|
432
|
+
// Check if path is a skill directory or a directory containing skills
|
|
433
|
+
const skillMdPath = await findSkillMd(fullPath)
|
|
434
|
+
|
|
435
|
+
if (skillMdPath) {
|
|
436
|
+
// Direct skill directory
|
|
437
|
+
allResults.push(await validateSkillDirectory(fullPath))
|
|
438
|
+
} else {
|
|
439
|
+
// Directory containing skills
|
|
440
|
+
const results = await validateSkills(fullPath)
|
|
441
|
+
if (results.length > 0) {
|
|
442
|
+
allResults.push(...results)
|
|
443
|
+
} else {
|
|
444
|
+
// No nested skills found - validate path directly (will produce error)
|
|
445
|
+
allResults.push(await validateSkillDirectory(fullPath))
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (values.json) {
|
|
451
|
+
console.log(JSON.stringify(allResults, null, 2))
|
|
452
|
+
} else {
|
|
453
|
+
// Human-readable output
|
|
454
|
+
let hasErrors = false
|
|
455
|
+
|
|
456
|
+
for (const result of allResults) {
|
|
457
|
+
const relativePath = result.path.replace(cwd, '').replace(/^\//, '')
|
|
458
|
+
|
|
459
|
+
if (result.valid) {
|
|
460
|
+
console.log(`✓ ${relativePath}`)
|
|
461
|
+
} else {
|
|
462
|
+
console.log(`✗ ${relativePath}`)
|
|
463
|
+
hasErrors = true
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
for (const error of result.errors) {
|
|
467
|
+
console.log(` ERROR: ${error}`)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
for (const warning of result.warnings) {
|
|
471
|
+
console.log(` WARN: ${warning}`)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (allResults.length === 0) {
|
|
476
|
+
console.log('No skills found to validate')
|
|
477
|
+
} else {
|
|
478
|
+
const valid = allResults.filter((r) => r.valid).length
|
|
479
|
+
const total = allResults.length
|
|
480
|
+
console.log(`\n${valid}/${total} skills valid`)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (hasErrors) {
|
|
484
|
+
process.exit(1)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Keep executable entry point for direct execution
|
|
490
|
+
if (import.meta.main) {
|
|
491
|
+
await validateSkill(Bun.argv.slice(2))
|
|
492
|
+
}
|