@codemcp/agentskills-cli 0.0.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.
Files changed (46) hide show
  1. package/LICENSE +19 -0
  2. package/dist/__tests__/cli.test.d.ts +2 -0
  3. package/dist/__tests__/cli.test.d.ts.map +1 -0
  4. package/dist/__tests__/cli.test.js +210 -0
  5. package/dist/__tests__/cli.test.js.map +1 -0
  6. package/dist/cli.d.ts +6 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +66 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/commands/__tests__/add.test.d.ts +2 -0
  11. package/dist/commands/__tests__/add.test.d.ts.map +1 -0
  12. package/dist/commands/__tests__/add.test.js +363 -0
  13. package/dist/commands/__tests__/add.test.js.map +1 -0
  14. package/dist/commands/__tests__/install.test.d.ts +2 -0
  15. package/dist/commands/__tests__/install.test.d.ts.map +1 -0
  16. package/dist/commands/__tests__/install.test.js +587 -0
  17. package/dist/commands/__tests__/install.test.js.map +1 -0
  18. package/dist/commands/__tests__/list.test.d.ts +2 -0
  19. package/dist/commands/__tests__/list.test.d.ts.map +1 -0
  20. package/dist/commands/__tests__/list.test.js +69 -0
  21. package/dist/commands/__tests__/list.test.js.map +1 -0
  22. package/dist/commands/__tests__/validate.test.d.ts +2 -0
  23. package/dist/commands/__tests__/validate.test.d.ts.map +1 -0
  24. package/dist/commands/__tests__/validate.test.js +858 -0
  25. package/dist/commands/__tests__/validate.test.js.map +1 -0
  26. package/dist/commands/add.d.ts +28 -0
  27. package/dist/commands/add.d.ts.map +1 -0
  28. package/dist/commands/add.js +65 -0
  29. package/dist/commands/add.js.map +1 -0
  30. package/dist/commands/install.d.ts +16 -0
  31. package/dist/commands/install.d.ts.map +1 -0
  32. package/dist/commands/install.js +125 -0
  33. package/dist/commands/install.js.map +1 -0
  34. package/dist/commands/list.d.ts +4 -0
  35. package/dist/commands/list.d.ts.map +1 -0
  36. package/dist/commands/list.js +41 -0
  37. package/dist/commands/list.js.map +1 -0
  38. package/dist/commands/validate.d.ts +26 -0
  39. package/dist/commands/validate.d.ts.map +1 -0
  40. package/dist/commands/validate.js +244 -0
  41. package/dist/commands/validate.js.map +1 -0
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +10 -0
  45. package/dist/index.js.map +1 -0
  46. package/package.json +57 -0
@@ -0,0 +1,858 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import { promises as fs } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { validateCommand } from "../validate.js";
6
+ /**
7
+ * Comprehensive test suite for validate command
8
+ *
9
+ * Following TDD approach:
10
+ * - Write tests first to define the command interface and behavior
11
+ * - Tests define expected user experience before implementation
12
+ * - Clear test structure with arrange-act-assert
13
+ * - Minimal mocking - use real file system with temp directories
14
+ *
15
+ * Coverage:
16
+ * 1. Single skill validation (valid, invalid parse, invalid validation, warnings)
17
+ * 2. Directory validation (all valid, some invalid, mixed, no skills)
18
+ * 3. Error handling (missing paths, no SKILL.md, permissions)
19
+ * 4. Output formatting (success, error, warning, summary messages)
20
+ * 5. --strict flag (treats warnings as errors)
21
+ * 6. --fix flag (stub for now, shows not implemented message)
22
+ */
23
+ describe("validate command", () => {
24
+ let testDir;
25
+ let consoleLogSpy;
26
+ let consoleErrorSpy;
27
+ let processExitSpy;
28
+ beforeEach(async () => {
29
+ // Create unique temp directory for each test
30
+ testDir = join(tmpdir(), `agentskills-test-${Date.now()}-${Math.random()}`);
31
+ await fs.mkdir(testDir, { recursive: true });
32
+ // Mock console and process.exit
33
+ consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => { });
34
+ consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
35
+ processExitSpy = vi
36
+ .spyOn(process, "exit")
37
+ .mockImplementation((() => { }));
38
+ });
39
+ afterEach(async () => {
40
+ // Clean up temp directory
41
+ try {
42
+ await fs.rm(testDir, { recursive: true, force: true });
43
+ }
44
+ catch (error) {
45
+ // Ignore cleanup errors
46
+ }
47
+ // Restore mocks
48
+ consoleLogSpy.mockRestore();
49
+ consoleErrorSpy.mockRestore();
50
+ processExitSpy.mockRestore();
51
+ });
52
+ // Helper function to create a skill file
53
+ async function createSkillFile(dir, content) {
54
+ const skillPath = join(dir, "SKILL.md");
55
+ await fs.writeFile(skillPath, content);
56
+ return skillPath;
57
+ }
58
+ // Helper function to create a valid skill (with proper description length to avoid warnings)
59
+ async function createValidSkill(dir, name = "test-skill") {
60
+ const content = `---
61
+ name: ${name}
62
+ description: A comprehensive test skill for validation with sufficient description length to avoid warnings
63
+ license: MIT
64
+ ---
65
+
66
+ # Test Skill
67
+
68
+ This is a valid skill for testing with no validation warnings.
69
+ `;
70
+ return createSkillFile(dir, content);
71
+ }
72
+ // Helper function to create skill with parse error
73
+ async function createParseErrorSkill(dir) {
74
+ const content = `---
75
+ name: invalid-yaml
76
+ description: "Missing closing quote
77
+ ---
78
+
79
+ # Invalid Skill
80
+ `;
81
+ return createSkillFile(dir, content);
82
+ }
83
+ // Helper function to create skill with validation error
84
+ async function createValidationErrorSkill(dir) {
85
+ const content = `---
86
+ name: ""
87
+ description: This skill has actual validation errors - empty name field
88
+ ---
89
+
90
+ # Invalid Skill
91
+
92
+ This skill has validation errors (empty name).
93
+ `;
94
+ return createSkillFile(dir, content);
95
+ }
96
+ // Helper function to create skill with warnings
97
+ async function createWarningSkill(dir) {
98
+ const content = `---
99
+ name: warning-skill
100
+ description: Short
101
+ ---
102
+
103
+ # Warning Skill
104
+
105
+ This skill has a very short description which should trigger a warning.
106
+ `;
107
+ return createSkillFile(dir, content);
108
+ }
109
+ describe("Single Skill Validation", () => {
110
+ describe("Valid skill", () => {
111
+ it("should display success message for valid skill", async () => {
112
+ // Arrange
113
+ const skillDir = join(testDir, "valid-skill");
114
+ await fs.mkdir(skillDir, { recursive: true });
115
+ await createValidSkill(skillDir);
116
+ // Act
117
+ await validateCommand(skillDir, {});
118
+ // Assert
119
+ expect(consoleLogSpy).toHaveBeenCalled();
120
+ const output = consoleLogSpy.mock.calls
121
+ .map((call) => call.join(" "))
122
+ .join("\n");
123
+ expect(output).toMatch(/✓.*test-skill.*valid/i);
124
+ expect(processExitSpy).toHaveBeenCalledWith(0);
125
+ });
126
+ it("should exit with code 0 for valid skill", async () => {
127
+ // Arrange
128
+ const skillDir = join(testDir, "valid-skill");
129
+ await fs.mkdir(skillDir, { recursive: true });
130
+ await createValidSkill(skillDir);
131
+ // Act
132
+ await validateCommand(skillDir, {});
133
+ // Assert
134
+ expect(processExitSpy).toHaveBeenCalledWith(0);
135
+ });
136
+ it("should validate when given direct path to SKILL.md", async () => {
137
+ // Arrange
138
+ const skillDir = join(testDir, "valid-skill");
139
+ await fs.mkdir(skillDir, { recursive: true });
140
+ const skillPath = await createValidSkill(skillDir);
141
+ // Act
142
+ await validateCommand(skillPath, {});
143
+ // Assert
144
+ expect(processExitSpy).toHaveBeenCalledWith(0);
145
+ const output = consoleLogSpy.mock.calls
146
+ .map((call) => call.join(" "))
147
+ .join("\n");
148
+ expect(output).toMatch(/✓.*test-skill.*valid/i);
149
+ });
150
+ });
151
+ describe("Invalid skill - parse error", () => {
152
+ it("should display error message for parse error", async () => {
153
+ // Arrange
154
+ const skillDir = join(testDir, "invalid-parse");
155
+ await fs.mkdir(skillDir, { recursive: true });
156
+ await createParseErrorSkill(skillDir);
157
+ // Act
158
+ await validateCommand(skillDir, {});
159
+ // Assert
160
+ expect(consoleErrorSpy).toHaveBeenCalled();
161
+ const output = consoleErrorSpy.mock.calls
162
+ .map((call) => call.join(" "))
163
+ .join("\n");
164
+ expect(output).toMatch(/✗/);
165
+ expect(output).toMatch(/failed/i);
166
+ expect(processExitSpy).toHaveBeenCalledWith(1);
167
+ });
168
+ it("should exit with code 1 for parse error", async () => {
169
+ // Arrange
170
+ const skillDir = join(testDir, "invalid-parse");
171
+ await fs.mkdir(skillDir, { recursive: true });
172
+ await createParseErrorSkill(skillDir);
173
+ // Act
174
+ await validateCommand(skillDir, {});
175
+ // Assert
176
+ expect(processExitSpy).toHaveBeenCalledWith(1);
177
+ });
178
+ it("should display detailed error information", async () => {
179
+ // Arrange
180
+ const skillDir = join(testDir, "invalid-parse");
181
+ await fs.mkdir(skillDir, { recursive: true });
182
+ await createParseErrorSkill(skillDir);
183
+ // Act
184
+ await validateCommand(skillDir, {});
185
+ // Assert
186
+ const output = consoleErrorSpy.mock.calls
187
+ .map((call) => call.join(" "))
188
+ .join("\n");
189
+ expect(output).toMatch(/yaml|parse|invalid/i);
190
+ });
191
+ });
192
+ describe("Invalid skill - validation error", () => {
193
+ it("should display error message for validation error", async () => {
194
+ // Arrange
195
+ const skillDir = join(testDir, "invalid-validation");
196
+ await fs.mkdir(skillDir, { recursive: true });
197
+ await createValidationErrorSkill(skillDir);
198
+ // Act
199
+ await validateCommand(skillDir, {});
200
+ // Assert
201
+ expect(consoleErrorSpy).toHaveBeenCalled();
202
+ const output = consoleErrorSpy.mock.calls
203
+ .map((call) => call.join(" "))
204
+ .join("\n");
205
+ expect(output).toMatch(/✗/);
206
+ expect(output).toMatch(/validation/i);
207
+ expect(processExitSpy).toHaveBeenCalledWith(1);
208
+ });
209
+ it("should display all validation errors", async () => {
210
+ // Arrange
211
+ const skillDir = join(testDir, "invalid-validation");
212
+ await fs.mkdir(skillDir, { recursive: true });
213
+ await createValidationErrorSkill(skillDir);
214
+ // Act
215
+ await validateCommand(skillDir, {});
216
+ // Assert
217
+ const output = consoleErrorSpy.mock.calls
218
+ .map((call) => call.join(" "))
219
+ .join("\n");
220
+ // Should show error details
221
+ expect(output.length).toBeGreaterThan(50);
222
+ });
223
+ it("should exit with code 1 for validation error", async () => {
224
+ // Arrange
225
+ const skillDir = join(testDir, "invalid-validation");
226
+ await fs.mkdir(skillDir, { recursive: true });
227
+ await createValidationErrorSkill(skillDir);
228
+ // Act
229
+ await validateCommand(skillDir, {});
230
+ // Assert
231
+ expect(processExitSpy).toHaveBeenCalledWith(1);
232
+ });
233
+ });
234
+ describe("Skill with warnings (no --strict)", () => {
235
+ it("should display warning message", async () => {
236
+ // Arrange
237
+ const skillDir = join(testDir, "warning-skill");
238
+ await fs.mkdir(skillDir, { recursive: true });
239
+ await createWarningSkill(skillDir);
240
+ // Act
241
+ await validateCommand(skillDir, {});
242
+ // Assert
243
+ expect(consoleLogSpy).toHaveBeenCalled();
244
+ const output = consoleLogSpy.mock.calls
245
+ .map((call) => call.join(" "))
246
+ .join("\n");
247
+ expect(output).toMatch(/⚠|warning/i);
248
+ });
249
+ it("should still show success with warnings", async () => {
250
+ // Arrange
251
+ const skillDir = join(testDir, "warning-skill");
252
+ await fs.mkdir(skillDir, { recursive: true });
253
+ await createWarningSkill(skillDir);
254
+ // Act
255
+ await validateCommand(skillDir, {});
256
+ // Assert
257
+ const output = consoleLogSpy.mock.calls
258
+ .map((call) => call.join(" "))
259
+ .join("\n");
260
+ expect(output).toMatch(/✓.*warning-skill/i);
261
+ expect(output).toMatch(/⚠|warning/i);
262
+ });
263
+ it("should exit with code 0 when warnings present but not strict", async () => {
264
+ // Arrange
265
+ const skillDir = join(testDir, "warning-skill");
266
+ await fs.mkdir(skillDir, { recursive: true });
267
+ await createWarningSkill(skillDir);
268
+ // Act
269
+ await validateCommand(skillDir, {});
270
+ // Assert
271
+ expect(processExitSpy).toHaveBeenCalledWith(0);
272
+ });
273
+ });
274
+ describe("Skill with warnings (--strict mode)", () => {
275
+ it("should treat warnings as errors in strict mode", async () => {
276
+ // Arrange
277
+ const skillDir = join(testDir, "warning-skill");
278
+ await fs.mkdir(skillDir, { recursive: true });
279
+ await createWarningSkill(skillDir);
280
+ // Act
281
+ await validateCommand(skillDir, { strict: true });
282
+ // Assert
283
+ expect(consoleErrorSpy).toHaveBeenCalled();
284
+ const output = consoleErrorSpy.mock.calls
285
+ .map((call) => call.join(" "))
286
+ .join("\n");
287
+ expect(output).toMatch(/✗/);
288
+ expect(output).toMatch(/warning|strict/i);
289
+ });
290
+ it("should exit with code 1 in strict mode when warnings present", async () => {
291
+ // Arrange
292
+ const skillDir = join(testDir, "warning-skill");
293
+ await fs.mkdir(skillDir, { recursive: true });
294
+ await createWarningSkill(skillDir);
295
+ // Act
296
+ await validateCommand(skillDir, { strict: true });
297
+ // Assert
298
+ expect(processExitSpy).toHaveBeenCalledWith(1);
299
+ });
300
+ it("should display warning details as errors in strict mode", async () => {
301
+ // Arrange
302
+ const skillDir = join(testDir, "warning-skill");
303
+ await fs.mkdir(skillDir, { recursive: true });
304
+ await createWarningSkill(skillDir);
305
+ // Act
306
+ await validateCommand(skillDir, { strict: true });
307
+ // Assert
308
+ const output = consoleErrorSpy.mock.calls
309
+ .map((call) => call.join(" "))
310
+ .join("\n");
311
+ expect(output).toMatch(/⚠|warning/i);
312
+ expect(output).toMatch(/description/i);
313
+ });
314
+ });
315
+ });
316
+ describe("Directory Validation (All Skills)", () => {
317
+ describe("All valid skills", () => {
318
+ it("should validate all skills in directory", async () => {
319
+ // Arrange
320
+ const skill1 = join(testDir, "skill-1");
321
+ const skill2 = join(testDir, "skill-2");
322
+ await fs.mkdir(skill1, { recursive: true });
323
+ await fs.mkdir(skill2, { recursive: true });
324
+ await createValidSkill(skill1, "skill-one");
325
+ await createValidSkill(skill2, "skill-two");
326
+ // Act
327
+ await validateCommand(testDir, {});
328
+ // Assert
329
+ const output = consoleLogSpy.mock.calls
330
+ .map((call) => call.join(" "))
331
+ .join("\n");
332
+ expect(output).toMatch(/skill-one/i);
333
+ expect(output).toMatch(/skill-two/i);
334
+ expect(processExitSpy).toHaveBeenCalledWith(0);
335
+ });
336
+ it("should display success summary for all valid", async () => {
337
+ // Arrange
338
+ const skill1 = join(testDir, "skill-1");
339
+ const skill2 = join(testDir, "skill-2");
340
+ await fs.mkdir(skill1, { recursive: true });
341
+ await fs.mkdir(skill2, { recursive: true });
342
+ await createValidSkill(skill1, "skill-one");
343
+ await createValidSkill(skill2, "skill-two");
344
+ // Act
345
+ await validateCommand(testDir, {});
346
+ // Assert
347
+ const output = consoleLogSpy.mock.calls
348
+ .map((call) => call.join(" "))
349
+ .join("\n");
350
+ expect(output).toMatch(/validated.*2.*skills/i);
351
+ expect(output).toMatch(/2.*valid/i);
352
+ expect(output).toMatch(/0.*invalid/i);
353
+ });
354
+ it("should exit with code 0 when all valid", async () => {
355
+ // Arrange
356
+ const skill1 = join(testDir, "skill-1");
357
+ const skill2 = join(testDir, "skill-2");
358
+ await fs.mkdir(skill1, { recursive: true });
359
+ await fs.mkdir(skill2, { recursive: true });
360
+ await createValidSkill(skill1, "skill-one");
361
+ await createValidSkill(skill2, "skill-two");
362
+ // Act
363
+ await validateCommand(testDir, {});
364
+ // Assert
365
+ expect(processExitSpy).toHaveBeenCalledWith(0);
366
+ });
367
+ });
368
+ describe("Some invalid skills", () => {
369
+ it("should validate all and report invalid ones", async () => {
370
+ // Arrange
371
+ const skill1 = join(testDir, "skill-1");
372
+ const skill2 = join(testDir, "skill-2");
373
+ await fs.mkdir(skill1, { recursive: true });
374
+ await fs.mkdir(skill2, { recursive: true });
375
+ await createValidSkill(skill1, "skill-one");
376
+ await createParseErrorSkill(skill2);
377
+ // Act
378
+ await validateCommand(testDir, {});
379
+ // Assert
380
+ expect(consoleErrorSpy).toHaveBeenCalled();
381
+ const output = consoleErrorSpy.mock.calls
382
+ .map((call) => call.join(" "))
383
+ .join("\n");
384
+ expect(output).toMatch(/✗/);
385
+ });
386
+ it("should display summary with failure count", async () => {
387
+ // Arrange
388
+ const skill1 = join(testDir, "skill-1");
389
+ const skill2 = join(testDir, "skill-2");
390
+ await fs.mkdir(skill1, { recursive: true });
391
+ await fs.mkdir(skill2, { recursive: true });
392
+ await createValidSkill(skill1, "skill-one");
393
+ await createParseErrorSkill(skill2);
394
+ // Act
395
+ await validateCommand(testDir, {});
396
+ // Assert
397
+ const allOutput = [
398
+ ...consoleLogSpy.mock.calls.map((call) => call.join(" ")),
399
+ ...consoleErrorSpy.mock.calls.map((call) => call.join(" "))
400
+ ].join("\n");
401
+ expect(allOutput).toMatch(/validated.*2.*skills/i);
402
+ expect(allOutput).toMatch(/1.*valid/i);
403
+ expect(allOutput).toMatch(/1.*invalid/i);
404
+ });
405
+ it("should exit with code 1 when some invalid", async () => {
406
+ // Arrange
407
+ const skill1 = join(testDir, "skill-1");
408
+ const skill2 = join(testDir, "skill-2");
409
+ await fs.mkdir(skill1, { recursive: true });
410
+ await fs.mkdir(skill2, { recursive: true });
411
+ await createValidSkill(skill1, "skill-one");
412
+ await createParseErrorSkill(skill2);
413
+ // Act
414
+ await validateCommand(testDir, {});
415
+ // Assert
416
+ expect(processExitSpy).toHaveBeenCalledWith(1);
417
+ });
418
+ });
419
+ describe("Mix of valid, invalid, and warnings", () => {
420
+ it("should report detailed results for mixed scenarios", async () => {
421
+ // Arrange
422
+ const skill1 = join(testDir, "skill-1");
423
+ const skill2 = join(testDir, "skill-2");
424
+ const skill3 = join(testDir, "skill-3");
425
+ await fs.mkdir(skill1, { recursive: true });
426
+ await fs.mkdir(skill2, { recursive: true });
427
+ await fs.mkdir(skill3, { recursive: true });
428
+ await createValidSkill(skill1, "skill-one");
429
+ await createWarningSkill(skill2);
430
+ await createParseErrorSkill(skill3);
431
+ // Act
432
+ await validateCommand(testDir, {});
433
+ // Assert
434
+ const allOutput = [
435
+ ...consoleLogSpy.mock.calls.map((call) => call.join(" ")),
436
+ ...consoleErrorSpy.mock.calls.map((call) => call.join(" "))
437
+ ].join("\n");
438
+ expect(allOutput).toMatch(/✓/); // Valid skill
439
+ expect(allOutput).toMatch(/⚠/); // Warning
440
+ expect(allOutput).toMatch(/✗/); // Error
441
+ });
442
+ it("should exit with code 1 when any invalid present", async () => {
443
+ // Arrange
444
+ const skill1 = join(testDir, "skill-1");
445
+ const skill2 = join(testDir, "skill-2");
446
+ const skill3 = join(testDir, "skill-3");
447
+ await fs.mkdir(skill1, { recursive: true });
448
+ await fs.mkdir(skill2, { recursive: true });
449
+ await fs.mkdir(skill3, { recursive: true });
450
+ await createValidSkill(skill1, "skill-one");
451
+ await createWarningSkill(skill2);
452
+ await createParseErrorSkill(skill3);
453
+ // Act
454
+ await validateCommand(testDir, {});
455
+ // Assert
456
+ expect(processExitSpy).toHaveBeenCalledWith(1);
457
+ });
458
+ it("should treat warnings as errors in strict mode for directory", async () => {
459
+ // Arrange
460
+ const skill1 = join(testDir, "skill-1");
461
+ const skill2 = join(testDir, "skill-2");
462
+ await fs.mkdir(skill1, { recursive: true });
463
+ await fs.mkdir(skill2, { recursive: true });
464
+ await createValidSkill(skill1, "skill-one");
465
+ await createWarningSkill(skill2);
466
+ // Act
467
+ await validateCommand(testDir, { strict: true });
468
+ // Assert
469
+ expect(processExitSpy).toHaveBeenCalledWith(1);
470
+ const allOutput = [
471
+ ...consoleLogSpy.mock.calls.map((call) => call.join(" ")),
472
+ ...consoleErrorSpy.mock.calls.map((call) => call.join(" "))
473
+ ].join("\n");
474
+ expect(allOutput).toMatch(/1.*valid/i);
475
+ expect(allOutput).toMatch(/1.*invalid/i);
476
+ });
477
+ });
478
+ describe("No skills found", () => {
479
+ it("should display appropriate message when no skills found", async () => {
480
+ // Arrange - empty directory
481
+ const emptyDir = join(testDir, "empty");
482
+ await fs.mkdir(emptyDir, { recursive: true });
483
+ // Act
484
+ await validateCommand(emptyDir, {});
485
+ // Assert
486
+ expect(consoleLogSpy).toHaveBeenCalled();
487
+ const output = consoleLogSpy.mock.calls
488
+ .map((call) => call.join(" "))
489
+ .join("\n");
490
+ expect(output).toMatch(/no skills found/i);
491
+ });
492
+ it("should exit with code 0 when no skills found", async () => {
493
+ // Arrange - empty directory
494
+ const emptyDir = join(testDir, "empty");
495
+ await fs.mkdir(emptyDir, { recursive: true });
496
+ // Act
497
+ await validateCommand(emptyDir, {});
498
+ // Assert
499
+ expect(processExitSpy).toHaveBeenCalledWith(0);
500
+ });
501
+ it("should handle directory with only non-skill files", async () => {
502
+ // Arrange
503
+ const dir = join(testDir, "non-skills");
504
+ await fs.mkdir(dir, { recursive: true });
505
+ await fs.writeFile(join(dir, "README.md"), "# Not a skill");
506
+ await fs.writeFile(join(dir, "notes.txt"), "Some notes");
507
+ // Act
508
+ await validateCommand(dir, {});
509
+ // Assert
510
+ const output = consoleLogSpy.mock.calls
511
+ .map((call) => call.join(" "))
512
+ .join("\n");
513
+ expect(output).toMatch(/no skills found/i);
514
+ });
515
+ });
516
+ });
517
+ describe("Error Handling", () => {
518
+ describe("Path does not exist", () => {
519
+ it("should display error message for non-existent path", async () => {
520
+ // Arrange
521
+ const nonExistentPath = join(testDir, "does-not-exist");
522
+ // Act
523
+ await validateCommand(nonExistentPath, {});
524
+ // Assert
525
+ expect(consoleErrorSpy).toHaveBeenCalled();
526
+ const output = consoleErrorSpy.mock.calls
527
+ .map((call) => call.join(" "))
528
+ .join("\n");
529
+ expect(output).toMatch(/not found|does not exist/i);
530
+ });
531
+ it("should exit with code 1 for non-existent path", async () => {
532
+ // Arrange
533
+ const nonExistentPath = join(testDir, "does-not-exist");
534
+ // Act
535
+ await validateCommand(nonExistentPath, {});
536
+ // Assert
537
+ expect(processExitSpy).toHaveBeenCalledWith(1);
538
+ });
539
+ });
540
+ describe("Not a skill directory (no SKILL.md)", () => {
541
+ it("should display error when directory has no SKILL.md", async () => {
542
+ // Arrange
543
+ const noSkillDir = join(testDir, "no-skill");
544
+ await fs.mkdir(noSkillDir, { recursive: true });
545
+ await fs.writeFile(join(noSkillDir, "README.md"), "# Not a skill");
546
+ // Act
547
+ await validateCommand(noSkillDir, {});
548
+ // Assert
549
+ expect(consoleErrorSpy).toHaveBeenCalled();
550
+ const output = consoleErrorSpy.mock.calls
551
+ .map((call) => call.join(" "))
552
+ .join("\n");
553
+ expect(output).toMatch(/no.*skill\.md|not.*skill/i);
554
+ });
555
+ it("should exit with code 1 when no SKILL.md found", async () => {
556
+ // Arrange
557
+ const noSkillDir = join(testDir, "no-skill");
558
+ await fs.mkdir(noSkillDir, { recursive: true });
559
+ await fs.writeFile(join(noSkillDir, "README.md"), "# Not a skill");
560
+ // Act
561
+ await validateCommand(noSkillDir, {});
562
+ // Assert
563
+ expect(processExitSpy).toHaveBeenCalledWith(1);
564
+ });
565
+ });
566
+ describe("Permission errors", () => {
567
+ it("should handle permission errors gracefully", async () => {
568
+ // Arrange
569
+ const restrictedDir = join(testDir, "restricted");
570
+ await fs.mkdir(restrictedDir, { recursive: true });
571
+ await createValidSkill(restrictedDir);
572
+ // Make directory unreadable (Unix-like systems only)
573
+ if (process.platform !== "win32") {
574
+ await fs.chmod(restrictedDir, 0o000);
575
+ }
576
+ else {
577
+ // Skip on Windows
578
+ return;
579
+ }
580
+ // Act
581
+ try {
582
+ await validateCommand(restrictedDir, {});
583
+ // Assert
584
+ expect(consoleErrorSpy).toHaveBeenCalled();
585
+ const output = consoleErrorSpy.mock.calls
586
+ .map((call) => call.join(" "))
587
+ .join("\n");
588
+ expect(output).toMatch(/permission|access/i);
589
+ expect(processExitSpy).toHaveBeenCalledWith(1);
590
+ }
591
+ finally {
592
+ // Restore permissions for cleanup
593
+ await fs.chmod(restrictedDir, 0o755);
594
+ }
595
+ });
596
+ });
597
+ describe("No path provided (should validate default locations)", () => {
598
+ it("should validate skills in default locations when no path provided", async () => {
599
+ // This test requires mocking config to provide default locations
600
+ // For now, we'll test that it doesn't crash
601
+ // Act
602
+ await validateCommand(undefined, {});
603
+ // Assert
604
+ // Should either find skills or report none found, but not crash
605
+ expect(processExitSpy).toHaveBeenCalled();
606
+ });
607
+ });
608
+ });
609
+ describe("Output Formatting", () => {
610
+ describe("Success format", () => {
611
+ it("should use green checkmark for success", async () => {
612
+ // Arrange
613
+ const skillDir = join(testDir, "valid-skill");
614
+ await fs.mkdir(skillDir, { recursive: true });
615
+ await createValidSkill(skillDir);
616
+ // Act
617
+ await validateCommand(skillDir, {});
618
+ // Assert
619
+ const output = consoleLogSpy.mock.calls
620
+ .map((call) => call.join(" "))
621
+ .join("\n");
622
+ expect(output).toMatch(/✓/);
623
+ });
624
+ it("should include skill name in success message", async () => {
625
+ // Arrange
626
+ const skillDir = join(testDir, "valid-skill");
627
+ await fs.mkdir(skillDir, { recursive: true });
628
+ await createValidSkill(skillDir, "my-awesome-skill");
629
+ // Act
630
+ await validateCommand(skillDir, {});
631
+ // Assert
632
+ const output = consoleLogSpy.mock.calls
633
+ .map((call) => call.join(" "))
634
+ .join("\n");
635
+ expect(output).toMatch(/my-awesome-skill/);
636
+ expect(output).toMatch(/valid/i);
637
+ });
638
+ });
639
+ describe("Error format", () => {
640
+ it("should use red X for errors", async () => {
641
+ // Arrange
642
+ const skillDir = join(testDir, "invalid-skill");
643
+ await fs.mkdir(skillDir, { recursive: true });
644
+ await createParseErrorSkill(skillDir);
645
+ // Act
646
+ await validateCommand(skillDir, {});
647
+ // Assert
648
+ const output = consoleErrorSpy.mock.calls
649
+ .map((call) => call.join(" "))
650
+ .join("\n");
651
+ expect(output).toMatch(/✗/);
652
+ });
653
+ it("should include error details with indentation", async () => {
654
+ // Arrange
655
+ const skillDir = join(testDir, "invalid-skill");
656
+ await fs.mkdir(skillDir, { recursive: true });
657
+ await createParseErrorSkill(skillDir);
658
+ // Act
659
+ await validateCommand(skillDir, {});
660
+ // Assert
661
+ const output = consoleErrorSpy.mock.calls
662
+ .map((call) => call.join(" "))
663
+ .join("\n");
664
+ // Should have indented error details (spaces or dashes)
665
+ expect(output).toMatch(/\s{2,}| {2}-/);
666
+ });
667
+ });
668
+ describe("Warning format", () => {
669
+ it("should use yellow warning symbol for warnings", async () => {
670
+ // Arrange
671
+ const skillDir = join(testDir, "warning-skill");
672
+ await fs.mkdir(skillDir, { recursive: true });
673
+ await createWarningSkill(skillDir);
674
+ // Act
675
+ await validateCommand(skillDir, {});
676
+ // Assert
677
+ const output = consoleLogSpy.mock.calls
678
+ .map((call) => call.join(" "))
679
+ .join("\n");
680
+ expect(output).toMatch(/⚠/);
681
+ });
682
+ it("should include warning description", async () => {
683
+ // Arrange
684
+ const skillDir = join(testDir, "warning-skill");
685
+ await fs.mkdir(skillDir, { recursive: true });
686
+ await createWarningSkill(skillDir);
687
+ // Act
688
+ await validateCommand(skillDir, {});
689
+ // Assert
690
+ const output = consoleLogSpy.mock.calls
691
+ .map((call) => call.join(" "))
692
+ .join("\n");
693
+ expect(output).toMatch(/warning/i);
694
+ });
695
+ });
696
+ describe("Summary format", () => {
697
+ it("should display summary with skill counts", async () => {
698
+ // Arrange
699
+ const skill1 = join(testDir, "skill-1");
700
+ const skill2 = join(testDir, "skill-2");
701
+ await fs.mkdir(skill1, { recursive: true });
702
+ await fs.mkdir(skill2, { recursive: true });
703
+ await createValidSkill(skill1);
704
+ await createValidSkill(skill2);
705
+ // Act
706
+ await validateCommand(testDir, {});
707
+ // Assert
708
+ const output = consoleLogSpy.mock.calls
709
+ .map((call) => call.join(" "))
710
+ .join("\n");
711
+ expect(output).toMatch(/validated.*2.*skills/i);
712
+ expect(output).toMatch(/2.*valid/i);
713
+ expect(output).toMatch(/0.*invalid/i);
714
+ });
715
+ it("should use proper pluralization for summary", async () => {
716
+ // Arrange - single skill
717
+ const skill1 = join(testDir, "skill-1");
718
+ await fs.mkdir(skill1, { recursive: true });
719
+ await createValidSkill(skill1);
720
+ // Act
721
+ await validateCommand(testDir, {});
722
+ // Assert
723
+ const output = consoleLogSpy.mock.calls
724
+ .map((call) => call.join(" "))
725
+ .join("\n");
726
+ // Should handle singular properly
727
+ expect(output).toMatch(/1.*skill|skill[^s]|1.*valid/i);
728
+ });
729
+ });
730
+ });
731
+ describe("--fix Flag", () => {
732
+ describe("Not implemented (stub)", () => {
733
+ it("should display not implemented message when --fix used", async () => {
734
+ // Arrange
735
+ const skillDir = join(testDir, "valid-skill");
736
+ await fs.mkdir(skillDir, { recursive: true });
737
+ await createValidSkill(skillDir);
738
+ // Act
739
+ await validateCommand(skillDir, { fix: true });
740
+ // Assert
741
+ expect(consoleLogSpy).toHaveBeenCalled();
742
+ const output = consoleLogSpy.mock.calls
743
+ .map((call) => call.join(" "))
744
+ .join("\n");
745
+ expect(output).toMatch(/auto-fix.*not.*implemented|--fix.*not.*available/i);
746
+ });
747
+ it("should still validate when --fix flag is used", async () => {
748
+ // Arrange
749
+ const skillDir = join(testDir, "valid-skill");
750
+ await fs.mkdir(skillDir, { recursive: true });
751
+ await createValidSkill(skillDir);
752
+ // Act
753
+ await validateCommand(skillDir, { fix: true });
754
+ // Assert
755
+ const output = consoleLogSpy.mock.calls
756
+ .map((call) => call.join(" "))
757
+ .join("\n");
758
+ expect(output).toMatch(/✓.*test-skill/i);
759
+ expect(processExitSpy).toHaveBeenCalledWith(0);
760
+ });
761
+ it("should report issues even with --fix flag", async () => {
762
+ // Arrange
763
+ const skillDir = join(testDir, "invalid-skill");
764
+ await fs.mkdir(skillDir, { recursive: true });
765
+ await createParseErrorSkill(skillDir);
766
+ // Act
767
+ await validateCommand(skillDir, { fix: true });
768
+ // Assert
769
+ expect(consoleErrorSpy).toHaveBeenCalled();
770
+ const output = consoleErrorSpy.mock.calls
771
+ .map((call) => call.join(" "))
772
+ .join("\n");
773
+ expect(output).toMatch(/✗/);
774
+ expect(processExitSpy).toHaveBeenCalledWith(1);
775
+ });
776
+ });
777
+ });
778
+ describe("Combined Flags", () => {
779
+ it("should handle --strict and --fix together", async () => {
780
+ // Arrange
781
+ const skillDir = join(testDir, "warning-skill");
782
+ await fs.mkdir(skillDir, { recursive: true });
783
+ await createWarningSkill(skillDir);
784
+ // Act
785
+ await validateCommand(skillDir, { strict: true, fix: true });
786
+ // Assert
787
+ const allOutput = [
788
+ ...consoleLogSpy.mock.calls.map((call) => call.join(" ")),
789
+ ...consoleErrorSpy.mock.calls.map((call) => call.join(" "))
790
+ ].join("\n");
791
+ expect(allOutput).toMatch(/auto-fix.*not.*implemented/i);
792
+ expect(processExitSpy).toHaveBeenCalledWith(1); // Should fail due to strict mode
793
+ });
794
+ });
795
+ describe("Edge Cases", () => {
796
+ it("should handle skill with very long content", async () => {
797
+ // Arrange
798
+ const skillDir = join(testDir, "long-skill");
799
+ await fs.mkdir(skillDir, { recursive: true });
800
+ const longContent = `---
801
+ name: long-skill
802
+ description: A skill with very long content
803
+ ---
804
+
805
+ # Long Skill
806
+
807
+ ${"Lorem ipsum dolor sit amet. ".repeat(1000)}
808
+ `;
809
+ await createSkillFile(skillDir, longContent);
810
+ // Act
811
+ await validateCommand(skillDir, {});
812
+ // Assert - should handle without crashing
813
+ expect(processExitSpy).toHaveBeenCalled();
814
+ });
815
+ it("should handle skill with special characters in description", async () => {
816
+ // Arrange
817
+ const skillDir = join(testDir, "special-skill");
818
+ await fs.mkdir(skillDir, { recursive: true });
819
+ const content = `---
820
+ name: skill-with-special-chars
821
+ description: Testing special characters émojis 🚀 and unicode 日本語 in skill description field
822
+ license: MIT
823
+ ---
824
+
825
+ # Special Skill
826
+
827
+ This tests special characters in descriptions are allowed.
828
+ `;
829
+ await createSkillFile(skillDir, content);
830
+ // Act
831
+ await validateCommand(skillDir, {});
832
+ // Assert
833
+ const output = consoleLogSpy.mock.calls
834
+ .map((call) => call.join(" "))
835
+ .join("\n");
836
+ expect(output).toMatch(/skill-with-special-chars/);
837
+ expect(output).toMatch(/valid/);
838
+ });
839
+ it("should handle nested directory structures when validating all", async () => {
840
+ // Arrange
841
+ const nested1 = join(testDir, "category1", "skill-1");
842
+ const nested2 = join(testDir, "category2", "skill-2");
843
+ await fs.mkdir(nested1, { recursive: true });
844
+ await fs.mkdir(nested2, { recursive: true });
845
+ await createValidSkill(nested1, "nested-skill-1");
846
+ await createValidSkill(nested2, "nested-skill-2");
847
+ // Act
848
+ await validateCommand(testDir, {});
849
+ // Assert
850
+ const output = consoleLogSpy.mock.calls
851
+ .map((call) => call.join(" "))
852
+ .join("\n");
853
+ expect(output).toMatch(/nested-skill-1/);
854
+ expect(output).toMatch(/nested-skill-2/);
855
+ });
856
+ });
857
+ });
858
+ //# sourceMappingURL=validate.test.js.map