@nimblebrain/mpak 0.1.0 → 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.
- package/LICENSE +10 -198
- package/README.md +50 -383
- package/dist/index.d.ts +0 -2
- package/dist/index.js +2113 -4
- package/dist/index.js.map +1 -1
- package/package.json +32 -29
- package/.claude/settings.local.json +0 -19
- package/.env.example +0 -13
- package/.github/workflows/ci.yml +0 -27
- package/CLAUDE.md +0 -283
- package/dist/commands/config.d.ts +0 -31
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js +0 -129
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/packages/pull.d.ts +0 -11
- package/dist/commands/packages/pull.d.ts.map +0 -1
- package/dist/commands/packages/pull.js +0 -72
- package/dist/commands/packages/pull.js.map +0 -1
- package/dist/commands/packages/run.d.ts +0 -47
- package/dist/commands/packages/run.d.ts.map +0 -1
- package/dist/commands/packages/run.js +0 -419
- package/dist/commands/packages/run.js.map +0 -1
- package/dist/commands/packages/search.d.ts +0 -12
- package/dist/commands/packages/search.d.ts.map +0 -1
- package/dist/commands/packages/search.js +0 -63
- package/dist/commands/packages/search.js.map +0 -1
- package/dist/commands/packages/show.d.ts +0 -8
- package/dist/commands/packages/show.d.ts.map +0 -1
- package/dist/commands/packages/show.js +0 -109
- package/dist/commands/packages/show.js.map +0 -1
- package/dist/commands/search.d.ts +0 -12
- package/dist/commands/search.d.ts.map +0 -1
- package/dist/commands/search.js +0 -144
- package/dist/commands/search.js.map +0 -1
- package/dist/commands/skills/index.d.ts +0 -8
- package/dist/commands/skills/index.d.ts.map +0 -1
- package/dist/commands/skills/index.js +0 -8
- package/dist/commands/skills/index.js.map +0 -1
- package/dist/commands/skills/install.d.ts +0 -9
- package/dist/commands/skills/install.d.ts.map +0 -1
- package/dist/commands/skills/install.js +0 -110
- package/dist/commands/skills/install.js.map +0 -1
- package/dist/commands/skills/list.d.ts +0 -8
- package/dist/commands/skills/list.d.ts.map +0 -1
- package/dist/commands/skills/list.js +0 -89
- package/dist/commands/skills/list.js.map +0 -1
- package/dist/commands/skills/pack.d.ts +0 -22
- package/dist/commands/skills/pack.d.ts.map +0 -1
- package/dist/commands/skills/pack.js +0 -116
- package/dist/commands/skills/pack.js.map +0 -1
- package/dist/commands/skills/pull.d.ts +0 -9
- package/dist/commands/skills/pull.d.ts.map +0 -1
- package/dist/commands/skills/pull.js +0 -68
- package/dist/commands/skills/pull.js.map +0 -1
- package/dist/commands/skills/search.d.ts +0 -14
- package/dist/commands/skills/search.d.ts.map +0 -1
- package/dist/commands/skills/search.js +0 -53
- package/dist/commands/skills/search.js.map +0 -1
- package/dist/commands/skills/show.d.ts +0 -8
- package/dist/commands/skills/show.d.ts.map +0 -1
- package/dist/commands/skills/show.js +0 -64
- package/dist/commands/skills/show.js.map +0 -1
- package/dist/commands/skills/validate.d.ts +0 -25
- package/dist/commands/skills/validate.d.ts.map +0 -1
- package/dist/commands/skills/validate.js +0 -191
- package/dist/commands/skills/validate.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/api/registry-client.d.ts +0 -63
- package/dist/lib/api/registry-client.d.ts.map +0 -1
- package/dist/lib/api/registry-client.js +0 -167
- package/dist/lib/api/registry-client.js.map +0 -1
- package/dist/lib/api/skills-client.d.ts +0 -30
- package/dist/lib/api/skills-client.d.ts.map +0 -1
- package/dist/lib/api/skills-client.js +0 -110
- package/dist/lib/api/skills-client.js.map +0 -1
- package/dist/program.d.ts +0 -12
- package/dist/program.d.ts.map +0 -1
- package/dist/program.js +0 -186
- package/dist/program.js.map +0 -1
- package/dist/schemas/generated/api-responses.d.ts +0 -541
- package/dist/schemas/generated/api-responses.d.ts.map +0 -1
- package/dist/schemas/generated/api-responses.js +0 -313
- package/dist/schemas/generated/api-responses.js.map +0 -1
- package/dist/schemas/generated/auth.d.ts +0 -18
- package/dist/schemas/generated/auth.d.ts.map +0 -1
- package/dist/schemas/generated/auth.js +0 -18
- package/dist/schemas/generated/auth.js.map +0 -1
- package/dist/schemas/generated/index.d.ts +0 -5
- package/dist/schemas/generated/index.d.ts.map +0 -1
- package/dist/schemas/generated/index.js +0 -6
- package/dist/schemas/generated/index.js.map +0 -1
- package/dist/schemas/generated/package.d.ts +0 -43
- package/dist/schemas/generated/package.d.ts.map +0 -1
- package/dist/schemas/generated/package.js +0 -20
- package/dist/schemas/generated/package.js.map +0 -1
- package/dist/schemas/generated/skill.d.ts +0 -381
- package/dist/schemas/generated/skill.d.ts.map +0 -1
- package/dist/schemas/generated/skill.js +0 -216
- package/dist/schemas/generated/skill.js.map +0 -1
- package/dist/utils/config-manager.d.ts +0 -66
- package/dist/utils/config-manager.d.ts.map +0 -1
- package/dist/utils/config-manager.js +0 -193
- package/dist/utils/config-manager.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -12
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -27
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/version.d.ts +0 -5
- package/dist/utils/version.d.ts.map +0 -1
- package/dist/utils/version.js +0 -19
- package/dist/utils/version.js.map +0 -1
- package/eslint.config.js +0 -63
- package/src/commands/config.ts +0 -162
- package/src/commands/packages/pull.ts +0 -96
- package/src/commands/packages/run.test.ts +0 -261
- package/src/commands/packages/run.ts +0 -536
- package/src/commands/packages/search.ts +0 -83
- package/src/commands/packages/show.ts +0 -128
- package/src/commands/search.ts +0 -191
- package/src/commands/skills/index.ts +0 -7
- package/src/commands/skills/install.ts +0 -129
- package/src/commands/skills/list.ts +0 -116
- package/src/commands/skills/pack.test.ts +0 -260
- package/src/commands/skills/pack.ts +0 -145
- package/src/commands/skills/pull.ts +0 -88
- package/src/commands/skills/search.ts +0 -73
- package/src/commands/skills/show.ts +0 -72
- package/src/commands/skills/validate.test.ts +0 -466
- package/src/commands/skills/validate.ts +0 -227
- package/src/index.ts +0 -11
- package/src/lib/api/registry-client.ts +0 -223
- package/src/lib/api/schema.d.ts +0 -520
- package/src/lib/api/skills-client.ts +0 -148
- package/src/program.test.ts +0 -22
- package/src/program.ts +0 -226
- package/src/schemas/config.v1.schema.json +0 -37
- package/src/schemas/generated/api-responses.ts +0 -386
- package/src/schemas/generated/auth.ts +0 -21
- package/src/schemas/generated/index.ts +0 -5
- package/src/schemas/generated/package.ts +0 -29
- package/src/schemas/generated/skill.ts +0 -271
- package/src/utils/config-manager.test.ts +0 -330
- package/src/utils/config-manager.ts +0 -272
- package/src/utils/errors.test.ts +0 -25
- package/src/utils/errors.ts +0 -33
- package/src/utils/version.test.ts +0 -16
- package/src/utils/version.ts +0 -18
- package/test/integration/registry-client.test.ts +0 -180
- package/tsconfig.check.json +0 -9
- package/tsconfig.json +0 -25
- package/vitest.config.ts +0 -14
|
@@ -1,466 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { tmpdir } from 'os';
|
|
5
|
-
import { validateSkillDirectory, formatValidationResult } from './validate.js';
|
|
6
|
-
|
|
7
|
-
describe('validateSkillDirectory', () => {
|
|
8
|
-
let testDir: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
testDir = join(tmpdir(), `skill-test-${Date.now()}`);
|
|
12
|
-
mkdirSync(testDir, { recursive: true });
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('directory checks', () => {
|
|
20
|
-
it('fails for non-existent directory', () => {
|
|
21
|
-
const result = validateSkillDirectory('/non/existent/path');
|
|
22
|
-
expect(result.valid).toBe(false);
|
|
23
|
-
expect(result.errors).toContain('Directory not found: /non/existent/path');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('fails for file instead of directory', () => {
|
|
27
|
-
const filePath = join(testDir, 'not-a-dir');
|
|
28
|
-
writeFileSync(filePath, 'content');
|
|
29
|
-
|
|
30
|
-
const result = validateSkillDirectory(filePath);
|
|
31
|
-
expect(result.valid).toBe(false);
|
|
32
|
-
expect(result.errors[0]).toMatch(/Path is not a directory/);
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('SKILL.md checks', () => {
|
|
37
|
-
it('fails when SKILL.md is missing', () => {
|
|
38
|
-
const skillDir = join(testDir, 'test-skill');
|
|
39
|
-
mkdirSync(skillDir);
|
|
40
|
-
|
|
41
|
-
const result = validateSkillDirectory(skillDir);
|
|
42
|
-
expect(result.valid).toBe(false);
|
|
43
|
-
expect(result.errors).toContain('SKILL.md not found');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('fails when frontmatter is missing', () => {
|
|
47
|
-
const skillDir = join(testDir, 'test-skill');
|
|
48
|
-
mkdirSync(skillDir);
|
|
49
|
-
writeFileSync(join(skillDir, 'SKILL.md'), '# Just content\nNo frontmatter here');
|
|
50
|
-
|
|
51
|
-
const result = validateSkillDirectory(skillDir);
|
|
52
|
-
expect(result.valid).toBe(false);
|
|
53
|
-
expect(result.errors).toContain('No frontmatter found in SKILL.md');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('fails when frontmatter is empty', () => {
|
|
57
|
-
const skillDir = join(testDir, 'test-skill');
|
|
58
|
-
mkdirSync(skillDir);
|
|
59
|
-
writeFileSync(join(skillDir, 'SKILL.md'), '---\n---\n# Content');
|
|
60
|
-
|
|
61
|
-
const result = validateSkillDirectory(skillDir);
|
|
62
|
-
expect(result.valid).toBe(false);
|
|
63
|
-
expect(result.errors).toContain('No frontmatter found in SKILL.md');
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('frontmatter validation', () => {
|
|
68
|
-
it('fails when name is missing', () => {
|
|
69
|
-
const skillDir = join(testDir, 'test-skill');
|
|
70
|
-
mkdirSync(skillDir);
|
|
71
|
-
writeFileSync(
|
|
72
|
-
join(skillDir, 'SKILL.md'),
|
|
73
|
-
`---
|
|
74
|
-
description: A test skill
|
|
75
|
-
---
|
|
76
|
-
# Test`
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const result = validateSkillDirectory(skillDir);
|
|
80
|
-
expect(result.valid).toBe(false);
|
|
81
|
-
expect(result.errors.some((e) => e.includes('name'))).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('fails when description is missing', () => {
|
|
85
|
-
const skillDir = join(testDir, 'test-skill');
|
|
86
|
-
mkdirSync(skillDir);
|
|
87
|
-
writeFileSync(
|
|
88
|
-
join(skillDir, 'SKILL.md'),
|
|
89
|
-
`---
|
|
90
|
-
name: test-skill
|
|
91
|
-
---
|
|
92
|
-
# Test`
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
const result = validateSkillDirectory(skillDir);
|
|
96
|
-
expect(result.valid).toBe(false);
|
|
97
|
-
expect(result.errors.some((e) => e.includes('description'))).toBe(true);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('fails when name format is invalid', () => {
|
|
101
|
-
const skillDir = join(testDir, 'test-skill');
|
|
102
|
-
mkdirSync(skillDir);
|
|
103
|
-
writeFileSync(
|
|
104
|
-
join(skillDir, 'SKILL.md'),
|
|
105
|
-
`---
|
|
106
|
-
name: Test_Skill
|
|
107
|
-
description: A test skill
|
|
108
|
-
---
|
|
109
|
-
# Test`
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const result = validateSkillDirectory(skillDir);
|
|
113
|
-
expect(result.valid).toBe(false);
|
|
114
|
-
expect(result.errors.some((e) => e.toLowerCase().includes('lowercase'))).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('fails when name has uppercase', () => {
|
|
118
|
-
const skillDir = join(testDir, 'test-skill');
|
|
119
|
-
mkdirSync(skillDir);
|
|
120
|
-
writeFileSync(
|
|
121
|
-
join(skillDir, 'SKILL.md'),
|
|
122
|
-
`---
|
|
123
|
-
name: TestSkill
|
|
124
|
-
description: A test skill
|
|
125
|
-
---
|
|
126
|
-
# Test`
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const result = validateSkillDirectory(skillDir);
|
|
130
|
-
expect(result.valid).toBe(false);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('fails when name starts with hyphen', () => {
|
|
134
|
-
const skillDir = join(testDir, '-test-skill');
|
|
135
|
-
mkdirSync(skillDir);
|
|
136
|
-
writeFileSync(
|
|
137
|
-
join(skillDir, 'SKILL.md'),
|
|
138
|
-
`---
|
|
139
|
-
name: -test-skill
|
|
140
|
-
description: A test skill
|
|
141
|
-
---
|
|
142
|
-
# Test`
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
const result = validateSkillDirectory(skillDir);
|
|
146
|
-
expect(result.valid).toBe(false);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('fails when name does not match directory', () => {
|
|
150
|
-
const skillDir = join(testDir, 'dir-name');
|
|
151
|
-
mkdirSync(skillDir);
|
|
152
|
-
writeFileSync(
|
|
153
|
-
join(skillDir, 'SKILL.md'),
|
|
154
|
-
`---
|
|
155
|
-
name: different-name
|
|
156
|
-
description: A test skill
|
|
157
|
-
---
|
|
158
|
-
# Test`
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const result = validateSkillDirectory(skillDir);
|
|
162
|
-
expect(result.valid).toBe(false);
|
|
163
|
-
expect(result.errors.some((e) => e.includes('does not match directory'))).toBe(true);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('valid skills', () => {
|
|
168
|
-
it('validates minimal skill', () => {
|
|
169
|
-
const skillDir = join(testDir, 'test-skill');
|
|
170
|
-
mkdirSync(skillDir);
|
|
171
|
-
writeFileSync(
|
|
172
|
-
join(skillDir, 'SKILL.md'),
|
|
173
|
-
`---
|
|
174
|
-
name: test-skill
|
|
175
|
-
description: A test skill for validation
|
|
176
|
-
---
|
|
177
|
-
# Test Skill
|
|
178
|
-
|
|
179
|
-
Instructions here.`
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
const result = validateSkillDirectory(skillDir);
|
|
183
|
-
expect(result.valid).toBe(true);
|
|
184
|
-
expect(result.name).toBe('test-skill');
|
|
185
|
-
expect(result.frontmatter?.description).toBe('A test skill for validation');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('validates skill with optional fields', () => {
|
|
189
|
-
const skillDir = join(testDir, 'full-skill');
|
|
190
|
-
mkdirSync(skillDir);
|
|
191
|
-
writeFileSync(
|
|
192
|
-
join(skillDir, 'SKILL.md'),
|
|
193
|
-
`---
|
|
194
|
-
name: full-skill
|
|
195
|
-
description: A fully featured test skill
|
|
196
|
-
license: MIT
|
|
197
|
-
compatibility: Works with Claude Code
|
|
198
|
-
allowed-tools: Read Grep Bash
|
|
199
|
-
---
|
|
200
|
-
# Full Skill`
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
const result = validateSkillDirectory(skillDir);
|
|
204
|
-
expect(result.valid).toBe(true);
|
|
205
|
-
expect(result.frontmatter?.license).toBe('MIT');
|
|
206
|
-
expect(result.frontmatter?.compatibility).toBe('Works with Claude Code');
|
|
207
|
-
expect(result.frontmatter?.['allowed-tools']).toBe('Read Grep Bash');
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('validates skill with metadata', () => {
|
|
211
|
-
const skillDir = join(testDir, 'meta-skill');
|
|
212
|
-
mkdirSync(skillDir);
|
|
213
|
-
writeFileSync(
|
|
214
|
-
join(skillDir, 'SKILL.md'),
|
|
215
|
-
`---
|
|
216
|
-
name: meta-skill
|
|
217
|
-
description: A skill with metadata
|
|
218
|
-
metadata:
|
|
219
|
-
version: "1.0.0"
|
|
220
|
-
category: development
|
|
221
|
-
tags:
|
|
222
|
-
- testing
|
|
223
|
-
- validation
|
|
224
|
-
surfaces:
|
|
225
|
-
- claude-code
|
|
226
|
-
author:
|
|
227
|
-
name: Test Author
|
|
228
|
-
---
|
|
229
|
-
# Meta Skill`
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
const result = validateSkillDirectory(skillDir);
|
|
233
|
-
expect(result.valid).toBe(true);
|
|
234
|
-
expect(result.frontmatter?.metadata?.version).toBe('1.0.0');
|
|
235
|
-
expect(result.frontmatter?.metadata?.category).toBe('development');
|
|
236
|
-
expect(result.frontmatter?.metadata?.tags).toEqual(['testing', 'validation']);
|
|
237
|
-
expect(result.frontmatter?.metadata?.surfaces).toEqual(['claude-code']);
|
|
238
|
-
expect(result.frontmatter?.metadata?.author?.name).toBe('Test Author');
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
describe('warnings', () => {
|
|
243
|
-
it('warns when metadata is missing', () => {
|
|
244
|
-
const skillDir = join(testDir, 'basic-skill');
|
|
245
|
-
mkdirSync(skillDir);
|
|
246
|
-
writeFileSync(
|
|
247
|
-
join(skillDir, 'SKILL.md'),
|
|
248
|
-
`---
|
|
249
|
-
name: basic-skill
|
|
250
|
-
description: A basic skill without metadata
|
|
251
|
-
---
|
|
252
|
-
# Basic Skill`
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
const result = validateSkillDirectory(skillDir);
|
|
256
|
-
expect(result.valid).toBe(true);
|
|
257
|
-
expect(result.warnings.some((w) => w.includes('No metadata field'))).toBe(true);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('warns when version is missing from metadata', () => {
|
|
261
|
-
const skillDir = join(testDir, 'no-version-skill');
|
|
262
|
-
mkdirSync(skillDir);
|
|
263
|
-
writeFileSync(
|
|
264
|
-
join(skillDir, 'SKILL.md'),
|
|
265
|
-
`---
|
|
266
|
-
name: no-version-skill
|
|
267
|
-
description: A skill without version
|
|
268
|
-
metadata:
|
|
269
|
-
category: development
|
|
270
|
-
---
|
|
271
|
-
# No Version Skill`
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
const result = validateSkillDirectory(skillDir);
|
|
275
|
-
expect(result.valid).toBe(true);
|
|
276
|
-
expect(result.warnings.some((w) => w.includes('No version'))).toBe(true);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('warns when tags are missing', () => {
|
|
280
|
-
const skillDir = join(testDir, 'no-tags-skill');
|
|
281
|
-
mkdirSync(skillDir);
|
|
282
|
-
writeFileSync(
|
|
283
|
-
join(skillDir, 'SKILL.md'),
|
|
284
|
-
`---
|
|
285
|
-
name: no-tags-skill
|
|
286
|
-
description: A skill without tags
|
|
287
|
-
metadata:
|
|
288
|
-
version: "1.0.0"
|
|
289
|
-
---
|
|
290
|
-
# No Tags Skill`
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
const result = validateSkillDirectory(skillDir);
|
|
294
|
-
expect(result.valid).toBe(true);
|
|
295
|
-
expect(result.warnings.some((w) => w.includes('No tags'))).toBe(true);
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it('warns about invalid optional directory', () => {
|
|
299
|
-
const skillDir = join(testDir, 'file-as-dir-skill');
|
|
300
|
-
mkdirSync(skillDir);
|
|
301
|
-
writeFileSync(
|
|
302
|
-
join(skillDir, 'SKILL.md'),
|
|
303
|
-
`---
|
|
304
|
-
name: file-as-dir-skill
|
|
305
|
-
description: A skill with scripts as file
|
|
306
|
-
---
|
|
307
|
-
# Skill`
|
|
308
|
-
);
|
|
309
|
-
writeFileSync(join(skillDir, 'scripts'), 'not a directory');
|
|
310
|
-
|
|
311
|
-
const result = validateSkillDirectory(skillDir);
|
|
312
|
-
expect(result.valid).toBe(true);
|
|
313
|
-
expect(result.warnings.some((w) => w.includes('scripts'))).toBe(true);
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
describe('optional directories', () => {
|
|
318
|
-
it('accepts valid optional directories', () => {
|
|
319
|
-
const skillDir = join(testDir, 'dirs-skill');
|
|
320
|
-
mkdirSync(skillDir);
|
|
321
|
-
mkdirSync(join(skillDir, 'scripts'));
|
|
322
|
-
mkdirSync(join(skillDir, 'references'));
|
|
323
|
-
mkdirSync(join(skillDir, 'assets'));
|
|
324
|
-
writeFileSync(
|
|
325
|
-
join(skillDir, 'SKILL.md'),
|
|
326
|
-
`---
|
|
327
|
-
name: dirs-skill
|
|
328
|
-
description: A skill with all optional dirs
|
|
329
|
-
---
|
|
330
|
-
# Skill`
|
|
331
|
-
);
|
|
332
|
-
|
|
333
|
-
const result = validateSkillDirectory(skillDir);
|
|
334
|
-
expect(result.valid).toBe(true);
|
|
335
|
-
expect(result.warnings.filter((w) => w.includes('not a directory'))).toHaveLength(0);
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
describe('formatValidationResult', () => {
|
|
341
|
-
it('formats valid result correctly', () => {
|
|
342
|
-
const result = {
|
|
343
|
-
valid: true,
|
|
344
|
-
name: 'test-skill',
|
|
345
|
-
path: '/path/to/skill',
|
|
346
|
-
frontmatter: {
|
|
347
|
-
name: 'test-skill',
|
|
348
|
-
description: 'A test skill description',
|
|
349
|
-
},
|
|
350
|
-
errors: [],
|
|
351
|
-
warnings: [],
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
const output = formatValidationResult(result);
|
|
355
|
-
expect(output).toContain('✓ Valid: test-skill');
|
|
356
|
-
expect(output).toContain('✓ SKILL.md found');
|
|
357
|
-
expect(output).toContain('name: test-skill');
|
|
358
|
-
expect(output).toContain('description:');
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('formats invalid result correctly', () => {
|
|
362
|
-
const result = {
|
|
363
|
-
valid: false,
|
|
364
|
-
name: null,
|
|
365
|
-
path: '/path/to/skill',
|
|
366
|
-
frontmatter: null,
|
|
367
|
-
errors: ['SKILL.md not found'],
|
|
368
|
-
warnings: [],
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
const output = formatValidationResult(result);
|
|
372
|
-
expect(output).toContain('✗ Invalid: /path/to/skill');
|
|
373
|
-
expect(output).toContain('Errors:');
|
|
374
|
-
expect(output).toContain('SKILL.md not found');
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('formats warnings correctly', () => {
|
|
378
|
-
const result = {
|
|
379
|
-
valid: true,
|
|
380
|
-
name: 'test-skill',
|
|
381
|
-
path: '/path/to/skill',
|
|
382
|
-
frontmatter: {
|
|
383
|
-
name: 'test-skill',
|
|
384
|
-
description: 'A test skill',
|
|
385
|
-
},
|
|
386
|
-
errors: [],
|
|
387
|
-
warnings: ['No metadata field'],
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
const output = formatValidationResult(result);
|
|
391
|
-
expect(output).toContain('Warnings:');
|
|
392
|
-
expect(output).toContain('No metadata field');
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('formats optional fields', () => {
|
|
396
|
-
const result = {
|
|
397
|
-
valid: true,
|
|
398
|
-
name: 'test-skill',
|
|
399
|
-
path: '/path/to/skill',
|
|
400
|
-
frontmatter: {
|
|
401
|
-
name: 'test-skill',
|
|
402
|
-
description: 'A test skill',
|
|
403
|
-
license: 'MIT',
|
|
404
|
-
compatibility: 'Claude Code',
|
|
405
|
-
'allowed-tools': 'Read Grep',
|
|
406
|
-
},
|
|
407
|
-
errors: [],
|
|
408
|
-
warnings: [],
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
const output = formatValidationResult(result);
|
|
412
|
-
expect(output).toContain('license: MIT');
|
|
413
|
-
expect(output).toContain('compatibility: Claude Code');
|
|
414
|
-
expect(output).toContain('allowed-tools: Read Grep');
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it('formats metadata correctly', () => {
|
|
418
|
-
const result = {
|
|
419
|
-
valid: true,
|
|
420
|
-
name: 'test-skill',
|
|
421
|
-
path: '/path/to/skill',
|
|
422
|
-
frontmatter: {
|
|
423
|
-
name: 'test-skill',
|
|
424
|
-
description: 'A test skill',
|
|
425
|
-
metadata: {
|
|
426
|
-
version: '1.0.0',
|
|
427
|
-
category: 'development' as const,
|
|
428
|
-
tags: ['test', 'validation'],
|
|
429
|
-
triggers: ['test trigger'],
|
|
430
|
-
surfaces: ['claude-code' as const],
|
|
431
|
-
author: { name: 'Test Author' },
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
errors: [],
|
|
435
|
-
warnings: [],
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
const output = formatValidationResult(result);
|
|
439
|
-
expect(output).toContain('Discovery metadata');
|
|
440
|
-
expect(output).toContain('version: 1.0.0');
|
|
441
|
-
expect(output).toContain('category: development');
|
|
442
|
-
expect(output).toContain('tags: [test, validation]');
|
|
443
|
-
expect(output).toContain('triggers: 1 defined');
|
|
444
|
-
expect(output).toContain('surfaces: [claude-code]');
|
|
445
|
-
expect(output).toContain('author: Test Author');
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it('truncates long descriptions', () => {
|
|
449
|
-
const longDescription = 'A'.repeat(100);
|
|
450
|
-
const result = {
|
|
451
|
-
valid: true,
|
|
452
|
-
name: 'test-skill',
|
|
453
|
-
path: '/path/to/skill',
|
|
454
|
-
frontmatter: {
|
|
455
|
-
name: 'test-skill',
|
|
456
|
-
description: longDescription,
|
|
457
|
-
},
|
|
458
|
-
errors: [],
|
|
459
|
-
warnings: [],
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
const output = formatValidationResult(result);
|
|
463
|
-
expect(output).toContain('...');
|
|
464
|
-
expect(output).toContain('(100 chars)');
|
|
465
|
-
});
|
|
466
|
-
});
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
-
import { join, basename } from 'path';
|
|
3
|
-
import matter from 'gray-matter';
|
|
4
|
-
import { SkillFrontmatterSchema } from '../../schemas/generated/skill.js';
|
|
5
|
-
import type { SkillFrontmatter } from '../../schemas/generated/skill.js';
|
|
6
|
-
|
|
7
|
-
export interface ValidationResult {
|
|
8
|
-
valid: boolean;
|
|
9
|
-
name: string | null;
|
|
10
|
-
path: string;
|
|
11
|
-
frontmatter: SkillFrontmatter | null;
|
|
12
|
-
errors: string[];
|
|
13
|
-
warnings: string[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Validate a skill directory against the Agent Skills spec
|
|
18
|
-
*/
|
|
19
|
-
export function validateSkillDirectory(skillPath: string): ValidationResult {
|
|
20
|
-
const result: ValidationResult = {
|
|
21
|
-
valid: true,
|
|
22
|
-
name: null,
|
|
23
|
-
path: skillPath,
|
|
24
|
-
frontmatter: null,
|
|
25
|
-
errors: [],
|
|
26
|
-
warnings: [],
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// Check directory exists
|
|
30
|
-
if (!existsSync(skillPath)) {
|
|
31
|
-
result.valid = false;
|
|
32
|
-
result.errors.push(`Directory not found: ${skillPath}`);
|
|
33
|
-
return result;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const stats = statSync(skillPath);
|
|
37
|
-
if (!stats.isDirectory()) {
|
|
38
|
-
result.valid = false;
|
|
39
|
-
result.errors.push(`Path is not a directory: ${skillPath}`);
|
|
40
|
-
return result;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Check SKILL.md exists
|
|
44
|
-
const skillMdPath = join(skillPath, 'SKILL.md');
|
|
45
|
-
if (!existsSync(skillMdPath)) {
|
|
46
|
-
result.valid = false;
|
|
47
|
-
result.errors.push('SKILL.md not found');
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Read and parse SKILL.md
|
|
52
|
-
let content: string;
|
|
53
|
-
try {
|
|
54
|
-
content = readFileSync(skillMdPath, 'utf-8');
|
|
55
|
-
} catch (err) {
|
|
56
|
-
result.valid = false;
|
|
57
|
-
result.errors.push(`Failed to read SKILL.md: ${err}`);
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Parse frontmatter
|
|
62
|
-
let parsed: matter.GrayMatterFile<string>;
|
|
63
|
-
try {
|
|
64
|
-
parsed = matter(content);
|
|
65
|
-
} catch (err) {
|
|
66
|
-
result.valid = false;
|
|
67
|
-
result.errors.push(`Failed to parse frontmatter: ${err}`);
|
|
68
|
-
return result;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (!parsed.data || Object.keys(parsed.data).length === 0) {
|
|
72
|
-
result.valid = false;
|
|
73
|
-
result.errors.push('No frontmatter found in SKILL.md');
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Validate against schema
|
|
78
|
-
const validation = SkillFrontmatterSchema.safeParse(parsed.data);
|
|
79
|
-
|
|
80
|
-
if (!validation.success) {
|
|
81
|
-
result.valid = false;
|
|
82
|
-
for (const issue of validation.error.issues) {
|
|
83
|
-
const path = issue.path.join('.');
|
|
84
|
-
result.errors.push(`${path}: ${issue.message}`);
|
|
85
|
-
}
|
|
86
|
-
return result;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
result.frontmatter = validation.data;
|
|
90
|
-
result.name = validation.data.name;
|
|
91
|
-
|
|
92
|
-
// Validate name matches directory name
|
|
93
|
-
const dirName = basename(skillPath);
|
|
94
|
-
if (validation.data.name !== dirName) {
|
|
95
|
-
result.valid = false;
|
|
96
|
-
result.errors.push(
|
|
97
|
-
`Skill name "${validation.data.name}" does not match directory name "${dirName}"`
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Check for optional directories and files
|
|
102
|
-
const contents = readdirSync(skillPath);
|
|
103
|
-
|
|
104
|
-
// Standard optional directories
|
|
105
|
-
const optionalDirs = ['scripts', 'references', 'assets'];
|
|
106
|
-
for (const dir of optionalDirs) {
|
|
107
|
-
if (contents.includes(dir)) {
|
|
108
|
-
const dirPath = join(skillPath, dir);
|
|
109
|
-
if (!statSync(dirPath).isDirectory()) {
|
|
110
|
-
result.warnings.push(`"${dir}" exists but is not a directory`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Check for discovery metadata (not required, but recommended)
|
|
116
|
-
if (!validation.data.metadata) {
|
|
117
|
-
result.warnings.push('No metadata field - consider adding for better discovery');
|
|
118
|
-
} else {
|
|
119
|
-
const meta = validation.data.metadata;
|
|
120
|
-
if (!meta.tags || meta.tags.length === 0) {
|
|
121
|
-
result.warnings.push('No tags in metadata - consider adding for better discovery');
|
|
122
|
-
}
|
|
123
|
-
if (!meta.category) {
|
|
124
|
-
result.warnings.push('No category in metadata - consider adding for better discovery');
|
|
125
|
-
}
|
|
126
|
-
if (!meta.version) {
|
|
127
|
-
result.warnings.push('No version in metadata - required for registry publishing');
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return result;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Format validation result for CLI output
|
|
136
|
-
*/
|
|
137
|
-
export function formatValidationResult(result: ValidationResult): string {
|
|
138
|
-
const lines: string[] = [];
|
|
139
|
-
|
|
140
|
-
if (result.valid) {
|
|
141
|
-
lines.push(`\u2713 Valid: ${result.name || result.path}`);
|
|
142
|
-
} else {
|
|
143
|
-
lines.push(`\u2717 Invalid: ${result.name || result.path}`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
lines.push('');
|
|
147
|
-
|
|
148
|
-
if (result.frontmatter) {
|
|
149
|
-
lines.push('\u2713 SKILL.md found');
|
|
150
|
-
lines.push('\u2713 Required fields');
|
|
151
|
-
lines.push(` \u251c\u2500 name: ${result.frontmatter.name}`);
|
|
152
|
-
lines.push(
|
|
153
|
-
` \u2514\u2500 description: ${result.frontmatter.description.slice(0, 60)}${result.frontmatter.description.length > 60 ? '...' : ''} (${result.frontmatter.description.length} chars)`
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
// Optional fields
|
|
157
|
-
const optionalFields: string[] = [];
|
|
158
|
-
if (result.frontmatter.license) optionalFields.push(`license: ${result.frontmatter.license}`);
|
|
159
|
-
if (result.frontmatter.compatibility)
|
|
160
|
-
optionalFields.push(`compatibility: ${result.frontmatter.compatibility}`);
|
|
161
|
-
if (result.frontmatter['allowed-tools'])
|
|
162
|
-
optionalFields.push(`allowed-tools: ${result.frontmatter['allowed-tools']}`);
|
|
163
|
-
|
|
164
|
-
if (optionalFields.length > 0) {
|
|
165
|
-
lines.push('');
|
|
166
|
-
lines.push('\u2713 Optional fields');
|
|
167
|
-
optionalFields.forEach((field, i) => {
|
|
168
|
-
const prefix = i === optionalFields.length - 1 ? '\u2514\u2500' : '\u251c\u2500';
|
|
169
|
-
lines.push(` ${prefix} ${field}`);
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Discovery metadata
|
|
174
|
-
if (result.frontmatter.metadata) {
|
|
175
|
-
const meta = result.frontmatter.metadata;
|
|
176
|
-
lines.push('');
|
|
177
|
-
lines.push('\u2713 Discovery metadata (metadata:)');
|
|
178
|
-
if (meta.tags) lines.push(` \u251c\u2500 tags: [${meta.tags.join(', ')}]`);
|
|
179
|
-
if (meta.category) lines.push(` \u251c\u2500 category: ${meta.category}`);
|
|
180
|
-
if (meta.triggers) lines.push(` \u251c\u2500 triggers: ${meta.triggers.length} defined`);
|
|
181
|
-
if (meta.version) lines.push(` \u251c\u2500 version: ${meta.version}`);
|
|
182
|
-
if (meta.surfaces) lines.push(` \u251c\u2500 surfaces: [${meta.surfaces.join(', ')}]`);
|
|
183
|
-
if (meta.author) lines.push(` \u2514\u2500 author: ${meta.author.name}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (result.errors.length > 0) {
|
|
188
|
-
lines.push('');
|
|
189
|
-
lines.push('Errors:');
|
|
190
|
-
result.errors.forEach((err) => lines.push(` \u2717 ${err}`));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (result.warnings.length > 0) {
|
|
194
|
-
lines.push('');
|
|
195
|
-
lines.push('Warnings:');
|
|
196
|
-
result.warnings.forEach((warn) => lines.push(` \u26a0 ${warn}`));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return lines.join('\n');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export interface ValidateOptions {
|
|
203
|
-
json?: boolean;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Handle the validate command
|
|
208
|
-
*/
|
|
209
|
-
export async function handleSkillValidate(
|
|
210
|
-
skillPath: string,
|
|
211
|
-
options: ValidateOptions
|
|
212
|
-
): Promise<void> {
|
|
213
|
-
const result = validateSkillDirectory(skillPath);
|
|
214
|
-
|
|
215
|
-
if (options.json) {
|
|
216
|
-
console.log(JSON.stringify(result, null, 2));
|
|
217
|
-
} else {
|
|
218
|
-
console.log('');
|
|
219
|
-
console.log(`Validating ${skillPath}...`);
|
|
220
|
-
console.log('');
|
|
221
|
-
console.log(formatValidationResult(result));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (!result.valid) {
|
|
225
|
-
process.exit(1);
|
|
226
|
-
}
|
|
227
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { createProgram } from './program.js';
|
|
4
|
-
import { handleError } from './utils/errors.js';
|
|
5
|
-
|
|
6
|
-
async function main() {
|
|
7
|
-
const program = createProgram();
|
|
8
|
-
await program.parseAsync(process.argv);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
main().catch(handleError);
|